Add FX rate per account, convert foreign balances to base currency in budget
Deploy to LXC / deploy (push) Successful in 1m55s
Validate / validate (push) Successful in 34s

Accounts now have fxRateToBase (default 1.0). The budget total query
multiplies each transaction by the account's rate, so a USD account
with rate 34.5 contributes correctly to the THB-denominated budget.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-17 16:24:00 +07:00
parent bc0699a992
commit 34b1524d3a
4 changed files with 24 additions and 3 deletions
+2
View File
@@ -893,6 +893,8 @@ export const companyAccounts = pgTable(
creditLimit: numeric('credit_limit', { precision: 15, scale: 2 }),
statementCloseDay: integer('statement_close_day'),
paymentDueDay: integer('payment_due_day'),
// FX conversion to company base currency (e.g. 34.5 for USD→THB)
fxRateToBase: numeric('fx_rate_to_base', { precision: 18, scale: 8 }).notNull().default('1'),
// Banking integration link
externalAccountId: uuid('external_account_id').references(() => externalAccounts.id, {
onDelete: 'set null'
@@ -27,10 +27,10 @@ export const load: LayoutServerLoad = async ({ locals, params }) => {
error(403, 'Not a member of this company');
}
// Total budget = sum of all non-deleted account balances
// Total budget = sum of all non-deleted account balances, converted to base currency
const [balanceRow] = await db
.select({
total: sql<string>`coalesce(sum(${companyAccountTransactions.amount}), '0')::text`
total: sql<string>`coalesce(sum(${companyAccountTransactions.amount} * ${companyAccounts.fxRateToBase}), '0')::text`
})
.from(companyAccountTransactions)
.innerJoin(companyAccounts, eq(companyAccountTransactions.accountId, companyAccounts.id))
@@ -112,6 +112,7 @@ type AccountFields = {
creditLimit: string | null;
statementCloseDay: number | null;
paymentDueDay: number | null;
fxRateToBase: string;
externalAccountId: string | null;
};
@@ -171,6 +172,7 @@ function extractAccountFields(fd: FormData):
creditLimit: parseDecimalOrNull(fd.get('creditLimit')),
statementCloseDay,
paymentDueDay,
fxRateToBase: parseDecimalOrNull(fd.get('fxRateToBase')) ?? '1',
externalAccountId: trimOrNull(fd.get('externalAccountId'))
}
};
@@ -304,6 +306,7 @@ export const actions: Actions = {
creditLimit: f.creditLimit,
statementCloseDay: f.statementCloseDay,
paymentDueDay: f.paymentDueDay,
fxRateToBase: f.fxRateToBase,
externalAccountId: f.externalAccountId
})
.returning({ id: companyAccounts.id });
@@ -383,6 +386,7 @@ export const actions: Actions = {
creditLimit: f.creditLimit,
statementCloseDay: f.statementCloseDay,
paymentDueDay: f.paymentDueDay,
fxRateToBase: f.fxRateToBase,
externalAccountId: f.externalAccountId,
updatedAt: new Date()
})
@@ -143,6 +143,7 @@
creditLimit?: string | null;
statementCloseDay?: number | null;
paymentDueDay?: number | null;
fxRateToBase?: string | null;
externalAccountId?: string | null;
} = {}
)}
@@ -176,7 +177,20 @@
class={inputCls}
/>
</div>
<div></div>
<div>
<label for="{prefix}-fxRate" class={labelCls}>FX Rate to Base</label>
<input
id="{prefix}-fxRate"
name="fxRateToBase"
type="number"
step="0.0001"
min="0.0001"
value={prefill.fxRateToBase ?? '1'}
placeholder="1.0 for THB, 34.5 for USDTHB"
class={inputCls}
/>
<p class="mt-0.5 text-xs text-gray-400 dark:text-gray-500">1.0 if same as company currency</p>
</div>
{#if type === 'bank'}
<div>
@@ -891,6 +905,7 @@
creditLimit: acct.creditLimit,
statementCloseDay: acct.statementCloseDay,
paymentDueDay: acct.paymentDueDay,
fxRateToBase: acct.fxRateToBase,
externalAccountId: acct.externalAccountId
})}
<div class="mt-3 flex justify-end gap-2">