Add FX rate per account, convert foreign balances to base currency in budget
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:
@@ -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 USD→THB"
|
||||
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">
|
||||
|
||||
Reference in New Issue
Block a user