586 lines
22 KiB
TypeScript
586 lines
22 KiB
TypeScript
import { tool } from '@langchain/core/tools'
|
||
import { z } from 'zod'
|
||
import type { SupabaseClient } from '@supabase/supabase-js'
|
||
|
||
/**
|
||
* Extract name from a Supabase join result (could be object or array).
|
||
*/
|
||
function extractName(joined: unknown): string | null {
|
||
if (!joined) return null
|
||
if (Array.isArray(joined)) {
|
||
return joined[0]?.name ?? null
|
||
}
|
||
if (typeof joined === 'object' && 'name' in joined) {
|
||
return (joined as { name: string }).name
|
||
}
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* Resolve the current fiscal period for a user. Falls back to latest period.
|
||
*/
|
||
async function resolveCurrentPeriod(
|
||
supabase: SupabaseClient,
|
||
userId: string,
|
||
fiscalPeriodId?: string
|
||
): Promise<{ id: string; start: string; end: string } | null> {
|
||
if (fiscalPeriodId) {
|
||
const { data } = await supabase
|
||
.from('fiscal_periods')
|
||
.select('id, period_start, period_end')
|
||
.eq('id', fiscalPeriodId)
|
||
.eq('user_id', userId)
|
||
.single()
|
||
if (data) return { id: data.id, start: data.period_start, end: data.period_end }
|
||
}
|
||
|
||
// Default: latest open period, or just the latest period
|
||
const { data } = await supabase
|
||
.from('fiscal_periods')
|
||
.select('id, period_start, period_end, is_closed')
|
||
.eq('user_id', userId)
|
||
.order('period_start', { ascending: false })
|
||
.limit(1)
|
||
.single()
|
||
|
||
if (data) return { id: data.id, start: data.period_start, end: data.period_end }
|
||
return null
|
||
}
|
||
|
||
/**
|
||
* Create all 10 accounting tools bound to a specific Supabase client and user.
|
||
*/
|
||
export function createAccountingTools(supabase: SupabaseClient, userId: string) {
|
||
const getInvoices = tool(
|
||
async ({ status, customer_name, date_from, date_to, limit }) => {
|
||
let query = supabase
|
||
.from('invoices')
|
||
.select('id, invoice_number, invoice_date, due_date, status, total, paid_amount, currency, vat_amount, customer:customers(name)')
|
||
.eq('user_id', userId)
|
||
.order('invoice_date', { ascending: false })
|
||
.limit(limit)
|
||
|
||
if (status) query = query.eq('status', status)
|
||
if (customer_name) query = query.ilike('customers.name', `%${customer_name}%`)
|
||
if (date_from) query = query.gte('invoice_date', date_from)
|
||
if (date_to) query = query.lte('invoice_date', date_to)
|
||
|
||
const { data, error, count } = await supabase
|
||
.from('invoices')
|
||
.select('id', { count: 'exact', head: true })
|
||
.eq('user_id', userId)
|
||
|
||
const { data: invoices, error: fetchError } = await query
|
||
|
||
if (fetchError) return `Fel vid hämtning av fakturor: ${fetchError.message}`
|
||
if (!invoices || invoices.length === 0) return 'Inga fakturor hittades.'
|
||
|
||
const result = invoices.map((inv) => ({
|
||
invoice_number: inv.invoice_number,
|
||
date: inv.invoice_date,
|
||
due_date: inv.due_date,
|
||
status: inv.status,
|
||
total: inv.total,
|
||
paid: inv.paid_amount || 0,
|
||
currency: inv.currency || 'SEK',
|
||
vat: inv.vat_amount || 0,
|
||
customer: extractName(inv.customer) || 'Okänd',
|
||
}))
|
||
|
||
const summary: Record<string, unknown> = { invoices: result }
|
||
if (count && count > limit) {
|
||
summary.note = `Visar ${result.length} av totalt ${count} fakturor.`
|
||
}
|
||
return JSON.stringify(summary)
|
||
},
|
||
{
|
||
name: 'get_invoices',
|
||
description: 'Hämtar användarens försäljningsfakturor (kundfakturor). Kan filtrera på status, kundnamn och datumintervall.',
|
||
schema: z.object({
|
||
status: z.enum(['draft', 'sent', 'paid', 'overdue', 'cancelled']).optional().describe('Filtrera på fakturastatus'),
|
||
customer_name: z.string().optional().describe('Sök på kundnamn (delmatchning)'),
|
||
date_from: z.string().optional().describe('Startdatum (YYYY-MM-DD)'),
|
||
date_to: z.string().optional().describe('Slutdatum (YYYY-MM-DD)'),
|
||
limit: z.number().max(20).default(10).describe('Max antal fakturor att returnera'),
|
||
}),
|
||
}
|
||
)
|
||
|
||
const getSupplierInvoices = tool(
|
||
async ({ status, supplier_name, overdue_only, limit }) => {
|
||
let query = supabase
|
||
.from('supplier_invoices')
|
||
.select('id, supplier_invoice_number, invoice_date, due_date, status, total, remaining_amount, currency, vat_amount, supplier:suppliers(name)')
|
||
.eq('user_id', userId)
|
||
.order('invoice_date', { ascending: false })
|
||
.limit(limit)
|
||
|
||
if (status) query = query.eq('status', status)
|
||
if (overdue_only) query = query.eq('status', 'overdue')
|
||
if (supplier_name) query = query.ilike('suppliers.name', `%${supplier_name}%`)
|
||
|
||
const { data: invoices, error } = await query
|
||
|
||
if (error) return `Fel vid hämtning av leverantörsfakturor: ${error.message}`
|
||
if (!invoices || invoices.length === 0) return 'Inga leverantörsfakturor hittades.'
|
||
|
||
const result = invoices.map((inv) => ({
|
||
number: inv.supplier_invoice_number,
|
||
date: inv.invoice_date,
|
||
due_date: inv.due_date,
|
||
status: inv.status,
|
||
total: inv.total,
|
||
remaining: inv.remaining_amount || 0,
|
||
currency: inv.currency || 'SEK',
|
||
vat: inv.vat_amount || 0,
|
||
supplier: extractName(inv.supplier) || 'Okänd',
|
||
}))
|
||
|
||
return JSON.stringify({ supplier_invoices: result })
|
||
},
|
||
{
|
||
name: 'get_supplier_invoices',
|
||
description: 'Hämtar användarens leverantörsfakturor (inköpsfakturor). Kan filtrera på status, leverantörsnamn och förfallodag.',
|
||
schema: z.object({
|
||
status: z.enum(['registered', 'approved', 'partially_paid', 'paid', 'overdue', 'cancelled']).optional().describe('Filtrera på status'),
|
||
supplier_name: z.string().optional().describe('Sök på leverantörsnamn (delmatchning)'),
|
||
overdue_only: z.boolean().optional().describe('Visa bara förfallna fakturor'),
|
||
limit: z.number().max(20).default(10).describe('Max antal fakturor'),
|
||
}),
|
||
}
|
||
)
|
||
|
||
const getAccountBalances = tool(
|
||
async ({ account_numbers, account_class, fiscal_period_id }) => {
|
||
const period = await resolveCurrentPeriod(supabase, userId, fiscal_period_id)
|
||
if (!period) return 'Ingen räkenskapsperiod hittades.'
|
||
|
||
const { generateTrialBalance } = await import('@/lib/reports/trial-balance')
|
||
const { rows } = await generateTrialBalance(supabase, userId, period.id)
|
||
|
||
let filtered = rows
|
||
if (account_numbers && account_numbers.length > 0) {
|
||
filtered = rows.filter((r) => account_numbers.includes(r.account_number))
|
||
} else if (account_class) {
|
||
filtered = rows.filter((r) => r.account_class === account_class)
|
||
}
|
||
|
||
if (filtered.length === 0) return 'Inga konton med saldo hittades.'
|
||
|
||
const result = filtered.map((r) => ({
|
||
account: r.account_number,
|
||
name: r.account_name,
|
||
debit: r.closing_debit,
|
||
credit: r.closing_credit,
|
||
balance: r.closing_debit - r.closing_credit,
|
||
}))
|
||
|
||
return JSON.stringify({
|
||
period: `${period.start} – ${period.end}`,
|
||
accounts: result,
|
||
total_debit: Math.round(result.reduce((s, r) => s + r.debit, 0) * 100) / 100,
|
||
total_credit: Math.round(result.reduce((s, r) => s + r.credit, 0) * 100) / 100,
|
||
})
|
||
},
|
||
{
|
||
name: 'get_account_balances',
|
||
description: 'Hämtar saldon för BAS-konton. Kan filtrera på kontonummer eller kontoklass (1=tillgångar, 2=skulder, 3=intäkter, 4-7=kostnader, 8=finansiella).',
|
||
schema: z.object({
|
||
account_numbers: z.array(z.string()).optional().describe('Specifika kontonummer att hämta'),
|
||
account_class: z.number().min(1).max(8).optional().describe('Kontoklass 1-8'),
|
||
fiscal_period_id: z.string().optional().describe('Räkenskapsperiod-ID (standard: aktuell period)'),
|
||
}),
|
||
}
|
||
)
|
||
|
||
const getTransactions = tool(
|
||
async ({ uncategorized_only, description, date_from, date_to, limit }) => {
|
||
let query = supabase
|
||
.from('transactions')
|
||
.select('id, date, description, amount, currency, category, is_business, merchant_name, journal_entry_id')
|
||
.eq('user_id', userId)
|
||
.order('date', { ascending: false })
|
||
.limit(limit)
|
||
|
||
if (uncategorized_only) query = query.is('journal_entry_id', null)
|
||
if (description) query = query.ilike('description', `%${description}%`)
|
||
if (date_from) query = query.gte('date', date_from)
|
||
if (date_to) query = query.lte('date', date_to)
|
||
|
||
const { data: transactions, error } = await query
|
||
|
||
if (error) return `Fel vid hämtning av transaktioner: ${error.message}`
|
||
if (!transactions || transactions.length === 0) return 'Inga transaktioner hittades.'
|
||
|
||
const result = transactions.map((tx) => ({
|
||
date: tx.date,
|
||
description: tx.description,
|
||
amount: tx.amount,
|
||
currency: tx.currency || 'SEK',
|
||
category: tx.category,
|
||
is_business: tx.is_business,
|
||
merchant: tx.merchant_name,
|
||
booked: !!tx.journal_entry_id,
|
||
}))
|
||
|
||
return JSON.stringify({ transactions: result })
|
||
},
|
||
{
|
||
name: 'get_transactions',
|
||
description: 'Hämtar användarens banktransaktioner. Kan filtrera på obokförda, beskrivning (textsökning) och datumintervall.',
|
||
schema: z.object({
|
||
uncategorized_only: z.boolean().optional().describe('Visa bara obokförda transaktioner'),
|
||
description: z.string().optional().describe('Sök i beskrivning (delmatchning)'),
|
||
date_from: z.string().optional().describe('Startdatum (YYYY-MM-DD)'),
|
||
date_to: z.string().optional().describe('Slutdatum (YYYY-MM-DD)'),
|
||
limit: z.number().max(20).default(10).describe('Max antal transaktioner'),
|
||
}),
|
||
}
|
||
)
|
||
|
||
const getJournalEntries = tool(
|
||
async ({ limit, fiscal_period_id, account_number, description }) => {
|
||
const period = await resolveCurrentPeriod(supabase, userId, fiscal_period_id)
|
||
|
||
let query = supabase
|
||
.from('journal_entries')
|
||
.select('id, voucher_number, entry_date, description, status, source_type')
|
||
.eq('user_id', userId)
|
||
.eq('status', 'posted')
|
||
.order('voucher_number', { ascending: false })
|
||
.limit(limit)
|
||
|
||
if (period) query = query.eq('fiscal_period_id', period.id)
|
||
if (description) query = query.ilike('description', `%${description}%`)
|
||
|
||
const { data: entries, error } = await query
|
||
|
||
if (error) return `Fel vid hämtning av verifikationer: ${error.message}`
|
||
if (!entries || entries.length === 0) return 'Inga verifikationer hittades.'
|
||
|
||
// Fetch lines for these entries
|
||
const entryIds = entries.map((e) => e.id)
|
||
const { data: lines } = await supabase
|
||
.from('journal_entry_lines')
|
||
.select('journal_entry_id, account_number, debit_amount, credit_amount, line_description')
|
||
.in('journal_entry_id', entryIds)
|
||
|
||
// If filtering by account, only include entries with matching lines
|
||
let filteredEntries = entries
|
||
if (account_number && lines) {
|
||
const matchingEntryIds = new Set(
|
||
lines.filter((l) => l.account_number === account_number).map((l) => l.journal_entry_id)
|
||
)
|
||
filteredEntries = entries.filter((e) => matchingEntryIds.has(e.id))
|
||
}
|
||
|
||
const linesByEntry = new Map<string, typeof lines>()
|
||
for (const line of lines || []) {
|
||
const group = linesByEntry.get(line.journal_entry_id) || []
|
||
group.push(line)
|
||
linesByEntry.set(line.journal_entry_id, group)
|
||
}
|
||
|
||
const result = filteredEntries.map((e) => ({
|
||
voucher: e.voucher_number,
|
||
date: e.entry_date,
|
||
description: e.description,
|
||
source: e.source_type,
|
||
lines: (linesByEntry.get(e.id) || []).map((l) => ({
|
||
account: l.account_number,
|
||
debit: l.debit_amount,
|
||
credit: l.credit_amount,
|
||
text: l.line_description,
|
||
})),
|
||
}))
|
||
|
||
return JSON.stringify({ journal_entries: result })
|
||
},
|
||
{
|
||
name: 'get_journal_entries',
|
||
description: 'Hämtar bokförda verifikationer med konteringsrader. Kan filtrera på kontonummer, beskrivning och räkenskapsperiod.',
|
||
schema: z.object({
|
||
limit: z.number().max(20).default(10).describe('Max antal verifikationer'),
|
||
fiscal_period_id: z.string().optional().describe('Räkenskapsperiod-ID'),
|
||
account_number: z.string().optional().describe('Filtrera på kontonummer i rader'),
|
||
description: z.string().optional().describe('Sök i beskrivning (delmatchning)'),
|
||
}),
|
||
}
|
||
)
|
||
|
||
const getIncomeStatement = tool(
|
||
async ({ fiscal_period_id }) => {
|
||
const period = await resolveCurrentPeriod(supabase, userId, fiscal_period_id)
|
||
if (!period) return 'Ingen räkenskapsperiod hittades.'
|
||
|
||
const { generateIncomeStatement } = await import('@/lib/reports/income-statement')
|
||
const report = await generateIncomeStatement(supabase, userId, period.id)
|
||
|
||
const sections = [
|
||
...report.revenue_sections.map((s) => ({
|
||
category: 'Intäkter',
|
||
title: s.title,
|
||
amount: s.subtotal,
|
||
accounts: s.rows.map((r) => ({ account: r.account_number, name: r.account_name, amount: r.amount })),
|
||
})),
|
||
...report.expense_sections.map((s) => ({
|
||
category: 'Kostnader',
|
||
title: s.title,
|
||
amount: s.subtotal,
|
||
accounts: s.rows.map((r) => ({ account: r.account_number, name: r.account_name, amount: r.amount })),
|
||
})),
|
||
...report.financial_sections.map((s) => ({
|
||
category: 'Finansiella poster',
|
||
title: s.title,
|
||
amount: s.subtotal,
|
||
accounts: s.rows.map((r) => ({ account: r.account_number, name: r.account_name, amount: r.amount })),
|
||
})),
|
||
]
|
||
|
||
return JSON.stringify({
|
||
period: `${period.start} – ${period.end}`,
|
||
total_revenue: report.total_revenue,
|
||
total_expenses: report.total_expenses,
|
||
total_financial: report.total_financial,
|
||
net_result: report.net_result,
|
||
sections,
|
||
})
|
||
},
|
||
{
|
||
name: 'get_income_statement',
|
||
description: 'Hämtar resultaträkning med intäkter, kostnader och årets resultat. Visar alla kontona grupperade i sektioner.',
|
||
schema: z.object({
|
||
fiscal_period_id: z.string().optional().describe('Räkenskapsperiod-ID (standard: aktuell period)'),
|
||
}),
|
||
}
|
||
)
|
||
|
||
const getBalanceSheet = tool(
|
||
async ({ fiscal_period_id }) => {
|
||
const period = await resolveCurrentPeriod(supabase, userId, fiscal_period_id)
|
||
if (!period) return 'Ingen räkenskapsperiod hittades.'
|
||
|
||
const { generateBalanceSheet } = await import('@/lib/reports/balance-sheet')
|
||
const report = await generateBalanceSheet(supabase, userId, period.id)
|
||
|
||
const sections = [
|
||
...report.asset_sections.map((s) => ({
|
||
category: 'Tillgångar',
|
||
title: s.title,
|
||
amount: s.subtotal,
|
||
accounts: s.rows.map((r) => ({ account: r.account_number, name: r.account_name, amount: r.amount })),
|
||
})),
|
||
...report.equity_liability_sections.map((s) => ({
|
||
category: 'Eget kapital & skulder',
|
||
title: s.title,
|
||
amount: s.subtotal,
|
||
accounts: s.rows.map((r) => ({ account: r.account_number, name: r.account_name, amount: r.amount })),
|
||
})),
|
||
]
|
||
|
||
return JSON.stringify({
|
||
period: `${period.start} – ${period.end}`,
|
||
total_assets: report.total_assets,
|
||
total_equity_liabilities: report.total_equity_liabilities,
|
||
balanced: Math.abs(report.total_assets - report.total_equity_liabilities) < 0.01,
|
||
sections,
|
||
})
|
||
},
|
||
{
|
||
name: 'get_balance_sheet',
|
||
description: 'Hämtar balansräkning med tillgångar, eget kapital och skulder.',
|
||
schema: z.object({
|
||
fiscal_period_id: z.string().optional().describe('Räkenskapsperiod-ID (standard: aktuell period)'),
|
||
}),
|
||
}
|
||
)
|
||
|
||
const getVatSummary = tool(
|
||
async ({ fiscal_period_id }) => {
|
||
const period = await resolveCurrentPeriod(supabase, userId, fiscal_period_id)
|
||
if (!period) return 'Ingen räkenskapsperiod hittades.'
|
||
|
||
// Get company settings for moms period type
|
||
const { data: settings } = await supabase
|
||
.from('company_settings')
|
||
.select('moms_period')
|
||
.eq('user_id', userId)
|
||
.single()
|
||
|
||
const periodType = settings?.moms_period || 'quarterly'
|
||
const startDate = new Date(period.start)
|
||
const year = startDate.getFullYear()
|
||
let periodNum = 1
|
||
if (periodType === 'monthly') {
|
||
periodNum = startDate.getMonth() + 1
|
||
} else if (periodType === 'quarterly') {
|
||
periodNum = Math.ceil((startDate.getMonth() + 1) / 3)
|
||
}
|
||
|
||
const { calculateVatDeclaration, getVatDeclarationSummary } = await import('@/lib/reports/vat-declaration')
|
||
const declaration = await calculateVatDeclaration(supabase, userId, periodType, year, periodNum)
|
||
const summary = getVatDeclarationSummary(declaration)
|
||
|
||
return JSON.stringify({
|
||
period: `${period.start} – ${period.end}`,
|
||
output_vat_25: declaration.rutor.ruta05,
|
||
output_vat_12: declaration.rutor.ruta06,
|
||
output_vat_6: declaration.rutor.ruta07,
|
||
total_output_vat: summary.totalOutputVat,
|
||
input_vat: summary.totalInputVat,
|
||
vat_to_pay: summary.vatToPay,
|
||
is_refund: summary.isRefund,
|
||
revenue_basis_25: declaration.rutor.ruta10,
|
||
revenue_basis_12: declaration.rutor.ruta11,
|
||
revenue_basis_6: declaration.rutor.ruta12,
|
||
invoice_count: declaration.invoiceCount,
|
||
transaction_count: declaration.transactionCount,
|
||
})
|
||
},
|
||
{
|
||
name: 'get_vat_summary',
|
||
description: 'Hämtar momssammanställning med utgående moms, ingående moms och moms att betala/återfå.',
|
||
schema: z.object({
|
||
fiscal_period_id: z.string().optional().describe('Räkenskapsperiod-ID (standard: aktuell period)'),
|
||
}),
|
||
}
|
||
)
|
||
|
||
const getCompanyOverview = tool(
|
||
async () => {
|
||
const { data: settings } = await supabase
|
||
.from('company_settings')
|
||
.select('*')
|
||
.eq('user_id', userId)
|
||
.single()
|
||
|
||
if (!settings) return 'Inga företagsinställningar hittades.'
|
||
|
||
// Get quick KPIs
|
||
const period = await resolveCurrentPeriod(supabase, userId)
|
||
|
||
const [
|
||
{ count: invoiceCount },
|
||
{ count: unpaidCount },
|
||
{ count: txCount },
|
||
{ count: unbookedCount },
|
||
] = await Promise.all([
|
||
supabase.from('invoices').select('id', { count: 'exact', head: true }).eq('user_id', userId),
|
||
supabase.from('invoices').select('id', { count: 'exact', head: true }).eq('user_id', userId).in('status', ['sent', 'overdue']),
|
||
supabase.from('transactions').select('id', { count: 'exact', head: true }).eq('user_id', userId),
|
||
supabase.from('transactions').select('id', { count: 'exact', head: true }).eq('user_id', userId).is('journal_entry_id', null),
|
||
])
|
||
|
||
let netResult: number | null = null
|
||
if (period) {
|
||
try {
|
||
const { generateIncomeStatement } = await import('@/lib/reports/income-statement')
|
||
const report = await generateIncomeStatement(supabase, userId, period.id)
|
||
netResult = report.net_result
|
||
} catch {
|
||
// Non-critical
|
||
}
|
||
}
|
||
|
||
return JSON.stringify({
|
||
company: {
|
||
name: settings.company_name,
|
||
entity_type: settings.entity_type,
|
||
org_number: settings.org_number,
|
||
vat_registered: settings.vat_registered,
|
||
accounting_method: settings.accounting_method,
|
||
moms_period: settings.moms_period,
|
||
},
|
||
kpis: {
|
||
total_invoices: invoiceCount || 0,
|
||
unpaid_invoices: unpaidCount || 0,
|
||
total_transactions: txCount || 0,
|
||
unbooked_transactions: unbookedCount || 0,
|
||
...(netResult !== null ? { net_result: netResult } : {}),
|
||
...(period ? { current_period: `${period.start} – ${period.end}` } : {}),
|
||
},
|
||
})
|
||
},
|
||
{
|
||
name: 'get_company_overview',
|
||
description: 'Hämtar företagsinformation och nyckeltal (KPIs): antal fakturor, obetalda fakturor, transaktioner, obokförda transaktioner, årets resultat.',
|
||
schema: z.object({}),
|
||
}
|
||
)
|
||
|
||
const getAgingReport = tool(
|
||
async ({ type, limit }) => {
|
||
if (type === 'receivable') {
|
||
const { generateARLedger } = await import('@/lib/reports/ar-ledger')
|
||
const report = await generateARLedger(supabase, userId)
|
||
|
||
if (report.entries.length === 0) return 'Inga utestående kundfordringar.'
|
||
|
||
const entries = report.entries.slice(0, limit).map((e) => ({
|
||
name: e.customer_name,
|
||
current: e.current,
|
||
'1_30': e.days_1_30,
|
||
'31_60': e.days_31_60,
|
||
'61_90': e.days_61_90,
|
||
'90_plus': e.days_90_plus,
|
||
total: e.total_outstanding,
|
||
}))
|
||
|
||
return JSON.stringify({
|
||
type: 'receivable',
|
||
total_outstanding: report.total_outstanding,
|
||
total_current: report.total_current,
|
||
total_overdue: report.total_overdue,
|
||
unpaid_count: report.unpaid_count,
|
||
entries,
|
||
})
|
||
} else {
|
||
const { generateSupplierLedger } = await import('@/lib/reports/supplier-ledger')
|
||
const report = await generateSupplierLedger(supabase, userId)
|
||
|
||
if (report.entries.length === 0) return 'Inga utestående leverantörsskulder.'
|
||
|
||
const entries = report.entries.slice(0, limit).map((e) => ({
|
||
name: e.supplier_name,
|
||
current: e.current,
|
||
'1_30': e.days_1_30,
|
||
'31_60': e.days_31_60,
|
||
'61_90': e.days_61_90,
|
||
'90_plus': e.days_90_plus,
|
||
total: e.total_outstanding,
|
||
}))
|
||
|
||
return JSON.stringify({
|
||
type: 'payable',
|
||
total_outstanding: report.total_outstanding,
|
||
total_current: report.total_current,
|
||
total_overdue: report.total_overdue,
|
||
unpaid_count: report.unpaid_count,
|
||
entries,
|
||
})
|
||
}
|
||
},
|
||
{
|
||
name: 'get_aging_report',
|
||
description: 'Hämtar åldersanalys för kundfordringar (receivable) eller leverantörsskulder (payable). Visar utestående belopp uppdelat i ålderskategorier.',
|
||
schema: z.object({
|
||
type: z.enum(['receivable', 'payable']).describe("'receivable' för kundfordringar, 'payable' för leverantörsskulder"),
|
||
limit: z.number().max(20).default(10).describe('Max antal poster'),
|
||
}),
|
||
}
|
||
)
|
||
|
||
return [
|
||
getInvoices,
|
||
getSupplierInvoices,
|
||
getAccountBalances,
|
||
getTransactions,
|
||
getJournalEntries,
|
||
getIncomeStatement,
|
||
getBalanceSheet,
|
||
getVatSummary,
|
||
getCompanyOverview,
|
||
getAgingReport,
|
||
]
|
||
}
|