Compare commits

..

No commits in common. "a66f90e0c9c318142dd0f34d8bc348221957347e" and "3badc1e93642a02e2dfdfef5bdc1e8d47836d16b" have entirely different histories.

8 changed files with 52 additions and 51 deletions

View File

@ -830,7 +830,7 @@ impl ReportingStep for CurrentYearEarningsToEquity {
account: account.clone(), account: account.clone(),
quantity: -balance, quantity: -balance,
commodity: context.reporting_commodity.clone(), commodity: context.reporting_commodity.clone(),
quantity_ascost: Some(-balance), quantity_ascost: None,
}, },
Posting { Posting {
id: None, id: None,
@ -839,7 +839,7 @@ impl ReportingStep for CurrentYearEarningsToEquity {
account: crate::CURRENT_YEAR_EARNINGS.to_string(), account: crate::CURRENT_YEAR_EARNINGS.to_string(),
quantity: *balance, quantity: *balance,
commodity: context.reporting_commodity.clone(), commodity: context.reporting_commodity.clone(),
quantity_ascost: Some(*balance), quantity_ascost: None,
}, },
], ],
}) })
@ -1396,7 +1396,7 @@ impl ReportingStep for RetainedEarningsToEquity {
account: account.clone(), account: account.clone(),
quantity: -balance, quantity: -balance,
commodity: context.reporting_commodity.clone(), commodity: context.reporting_commodity.clone(),
quantity_ascost: Some(-balance), quantity_ascost: None,
}, },
Posting { Posting {
id: None, id: None,
@ -1405,7 +1405,7 @@ impl ReportingStep for RetainedEarningsToEquity {
account: crate::RETAINED_EARNINGS.to_string(), account: crate::RETAINED_EARNINGS.to_string(),
quantity: *balance, quantity: *balance,
commodity: context.reporting_commodity.clone(), commodity: context.reporting_commodity.clone(),
quantity_ascost: Some(*balance), quantity_ascost: None,
}, },
], ],
}) })

View File

@ -183,7 +183,7 @@ export function serialiseAmount(quantity: number, commodity: string): string {
function parseFloatStrict(quantity: string): number { function parseFloatStrict(quantity: string): number {
// Parses quantity as a float, throwing error on invalid input // Parses quantity as a float, throwing error on invalid input
if (!/^-?[0-9]+(\.[0-9]+)?$/.test(quantity)) { if (!/^[0-9]+(\.[0-9]+)?$/.test(quantity)) {
throw new DeserialiseAmountError('Invalid quantity: ' + quantity); throw new DeserialiseAmountError('Invalid quantity: ' + quantity);
} }
return parseFloat(quantity); return parseFloat(quantity);
@ -216,21 +216,12 @@ export function deserialiseAmount(amount: string): { quantity: number, commodity
throw new DeserialiseAmountError('Amount cannot be blank'); throw new DeserialiseAmountError('Amount cannot be blank');
} }
if (amount.charAt(0) === '-') {
// Handle negative amount
const amountAbs = deserialiseAmount(amount.substring(1));
return {
quantity: -amountAbs.quantity,
commodity: amountAbs.commodity
};
}
if (amount.charAt(0) < '0' || amount.charAt(0) > '9') { if (amount.charAt(0) < '0' || amount.charAt(0) > '9') {
// Check for single letter commodity // Check for single letter commodity
if (amount.length === 1) { if (amount.length === 1) {
throw new DeserialiseAmountError('Quantity cannot be blank (expected quantity after commodity symbol ' + amount + ')'); throw new DeserialiseAmountError('Quantity cannot be blank (expected quantity after commodity symbol ' + amount + ')');
} }
if ((amount.charAt(1) < '0' || amount.charAt(1) > '9') && amount.charAt(1) !== '-') { if (amount.charAt(1) < '0' || amount.charAt(1) > '9') {
throw new DeserialiseAmountError('Invalid quantity: ' + amount + ' (expected quantity after single-letter commodity symbol ' + amount.charAt(0) + ')'); throw new DeserialiseAmountError('Invalid quantity: ' + amount + ' (expected quantity after single-letter commodity symbol ' + amount.charAt(0) + ')');
} }
@ -326,17 +317,6 @@ export interface JoinedTransactionPosting {
running_balance?: number running_balance?: number
} }
export function postingQuantityAsCost(posting: Posting | JoinedTransactionPosting) {
// Convert the posting amount to cost price in the reporting commodity
if (posting.quantity_ascost) {
return posting.quantity_ascost;
} else {
// NB: This branch is rarely taken - most conversions are performed in SQL via the transactions_with_quantity_ascost view
return asCost(posting.quantity, posting.commodity);
}
}
export interface StatementLine { export interface StatementLine {
id: number | null, id: number | null,
source_account: string, source_account: string,

View File

@ -1,5 +1,5 @@
<!-- <!--
DrCr: Double-entry bookkeeping framework DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo) Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
@ -66,7 +66,7 @@
import { UnlistenFn, listen } from '@tauri-apps/api/event'; import { UnlistenFn, listen } from '@tauri-apps/api/event';
import { onUnmounted, ref, watch } from 'vue'; import { onUnmounted, ref, watch } from 'vue';
import { Transaction, postingQuantityAsCost } from '../db.ts'; import { Transaction } from '../db.ts';
import { pp, ppWithCommodity } from '../display.ts'; import { pp, ppWithCommodity } from '../display.ts';
import { renderComponent } from '../webutil.ts'; import { renderComponent } from '../webutil.ts';
@ -124,10 +124,10 @@
<td class="py-0.5 px-1 text-gray-900 text-end"><i>${ posting.quantity >= 0 ? 'Dr' : 'Cr' }</i></td> <td class="py-0.5 px-1 text-gray-900 text-end"><i>${ posting.quantity >= 0 ? 'Dr' : 'Cr' }</i></td>
<td class="py-0.5 px-1 text-gray-900 lg:w-[30%]"><a href="/transactions/${ encodeURIComponent(posting.account) }" class="text-gray-900 hover:text-blue-700 hover:underline">${ posting.account }</a></td> <td class="py-0.5 px-1 text-gray-900 lg:w-[30%]"><a href="/transactions/${ encodeURIComponent(posting.account) }" class="text-gray-900 hover:text-blue-700 hover:underline">${ posting.account }</a></td>
<td class="py-0.5 px-1 text-gray-900 lg:w-[12ex] text-end"> <td class="py-0.5 px-1 text-gray-900 lg:w-[12ex] text-end">
${ posting.quantity >= 0 ? pp(postingQuantityAsCost(posting)) : '' } ${ posting.quantity >= 0 ? pp(posting.quantity_ascost!) : '' }
</td> </td>
<td class="py-0.5 pl-1 text-gray-900 lg:w-[12ex] text-end"> <td class="py-0.5 pl-1 text-gray-900 lg:w-[12ex] text-end">
${ posting.quantity < 0 ? pp(-postingQuantityAsCost(posting)) : '' } ${ posting.quantity < 0 ? pp(-posting.quantity_ascost!) : '' }
</td> </td>
</tr>` </tr>`
); );

View File

@ -49,7 +49,7 @@
import { onMounted, onUnmounted, watch } from 'vue'; import { onMounted, onUnmounted, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { Transaction, postingQuantityAsCost } from '../db.ts'; import { Transaction } from '../db.ts';
import { pp } from '../display.ts'; import { pp } from '../display.ts';
import { renderComponent } from '../webutil.ts'; import { renderComponent } from '../webutil.ts';
@ -66,7 +66,7 @@
const transaction = transactions[i]; const transaction = transactions[i];
for (const posting of transaction.postings) { for (const posting of transaction.postings) {
if (posting.account === route.params.account) { if (posting.account === route.params.account) {
balance += postingQuantityAsCost(posting); balance += posting.quantity_ascost!;
posting.running_balance = balance; posting.running_balance = balance;
} }
} }
@ -100,8 +100,8 @@
<td class="py-0.5 pr-1 text-gray-900 lg:w-[12ex]">${ dayjs(transaction.dt).format('YYYY-MM-DD') }</td> <td class="py-0.5 pr-1 text-gray-900 lg:w-[12ex]">${ dayjs(transaction.dt).format('YYYY-MM-DD') }</td>
<td class="py-0.5 px-1 text-gray-900">${ transaction.description } ${ editLink }</td> <td class="py-0.5 px-1 text-gray-900">${ transaction.description } ${ editLink }</td>
<td class="py-0.5 px-1 text-gray-900"><a href="/transactions/${ encodeURIComponent(otherAccountPosting!.account) }" class="text-gray-900 hover:text-blue-700 hover:underline">${ otherAccountPosting!.account }</a></td> <td class="py-0.5 px-1 text-gray-900"><a href="/transactions/${ encodeURIComponent(otherAccountPosting!.account) }" class="text-gray-900 hover:text-blue-700 hover:underline">${ otherAccountPosting!.account }</a></td>
<td class="py-0.5 px-1 text-gray-900 lg:w-[12ex] text-end">${ thisAccountPosting!.quantity >= 0 ? pp(postingQuantityAsCost(thisAccountPosting!)) : '' }</td> <td class="py-0.5 px-1 text-gray-900 lg:w-[12ex] text-end">${ thisAccountPosting!.quantity >= 0 ? pp(thisAccountPosting!.quantity_ascost!) : '' }</td>
<td class="py-0.5 px-1 text-gray-900 lg:w-[12ex] text-end">${ thisAccountPosting!.quantity < 0 ? pp(-postingQuantityAsCost(thisAccountPosting!)) : '' }</td> <td class="py-0.5 px-1 text-gray-900 lg:w-[12ex] text-end">${ thisAccountPosting!.quantity < 0 ? pp(-thisAccountPosting!.quantity_ascost!) : '' }</td>
<td class="py-0.5 px-1 text-gray-900 lg:w-[12ex] text-end">${ pp(Math.abs(thisAccountPosting!.running_balance!)) }</td> <td class="py-0.5 px-1 text-gray-900 lg:w-[12ex] text-end">${ pp(Math.abs(thisAccountPosting!.running_balance!)) }</td>
<td class="py-0.5 text-gray-900">${ thisAccountPosting!.running_balance! >= 0 ? 'Dr' : 'Cr' }</td> <td class="py-0.5 text-gray-900">${ thisAccountPosting!.running_balance! >= 0 ? 'Dr' : 'Cr' }</td>
</tr>` </tr>`
@ -125,8 +125,8 @@
<td></td> <td></td>
<td class="py-0.5 px-1 text-gray-900 text-end"><i>${ posting.quantity >= 0 ? 'Dr' : 'Cr' }</i></td> <td class="py-0.5 px-1 text-gray-900 text-end"><i>${ posting.quantity >= 0 ? 'Dr' : 'Cr' }</i></td>
<td class="py-0.5 px-1 text-gray-900"><a href="/transactions/${ encodeURIComponent(posting.account) }" class="text-gray-900 hover:text-blue-700 hover:underline">${ posting.account }</a></td> <td class="py-0.5 px-1 text-gray-900"><a href="/transactions/${ encodeURIComponent(posting.account) }" class="text-gray-900 hover:text-blue-700 hover:underline">${ posting.account }</a></td>
<td class="py-0.5 px-1 text-gray-900 lg:w-[12ex] text-end">${ posting.quantity >= 0 ? pp(postingQuantityAsCost(posting)) : '' }</td> <td class="py-0.5 px-1 text-gray-900 lg:w-[12ex] text-end">${ posting.quantity >= 0 ? pp(posting.quantity_ascost!) : '' }</td>
<td class="py-0.5 px-1 text-gray-900 lg:w-[12ex] text-end">${ posting.quantity < 0 ? pp(-postingQuantityAsCost(posting)) : '' }</td> <td class="py-0.5 px-1 text-gray-900 lg:w-[12ex] text-end">${ posting.quantity < 0 ? pp(-posting.quantity_ascost!) : '' }</td>
<td class="py-0.5 px-1 text-gray-900 lg:w-[12ex] text-end">${ posting.account === route.params.account ? pp(Math.abs(posting.running_balance!)) : '' }</td> <td class="py-0.5 px-1 text-gray-900 lg:w-[12ex] text-end">${ posting.account === route.params.account ? pp(Math.abs(posting.running_balance!)) : '' }</td>
<td class="py-0.5 text-gray-900">${ posting.account === route.params.account ? (posting.running_balance! >= 0 ? 'Dr' : 'Cr') : '' }</td> <td class="py-0.5 text-gray-900">${ posting.account === route.params.account ? (posting.running_balance! >= 0 ? 'Dr' : 'Cr') : '' }</td>
</tr>` </tr>`

View File

@ -1,5 +1,5 @@
<!-- <!--
DrCr: Double-entry bookkeeping framework DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo) Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
@ -46,7 +46,13 @@
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<span class="text-gray-500">{{ db.metadata.reporting_commodity }}</span> <span class="text-gray-500">{{ db.metadata.reporting_commodity }}</span>
</div> </div>
<input type="number" class="bordered-field pl-7" step="0.01" v-model="adjustment.cost_adjustment" placeholder="0.00"> <input type="number" class="bordered-field pl-7 pr-16" step="0.01" v-model="adjustment.cost_adjustment_abs" placeholder="0.00">
<div class="absolute inset-y-0 right-0 flex items-center">
<select class="h-full border-0 bg-transparent py-0 pl-2 pr-8 text-gray-900 focus:ring-2 focus:ring-inset focus:ring-indigo-600" v-model="adjustment.sign">
<option value="dr">Dr</option>
<option value="cr">Cr</option>
</select>
</div>
</div> </div>
</div> </div>
@ -84,7 +90,8 @@
acquisition_dt: string, acquisition_dt: string,
dt: string, dt: string,
description: string, description: string,
cost_adjustment: string, sign: string,
cost_adjustment_abs: string,
} }
const { adjustment } = defineProps<{ adjustment: EditingCGTAdjustment }>(); const { adjustment } = defineProps<{ adjustment: EditingCGTAdjustment }>();
@ -107,9 +114,9 @@
} }
} }
let cost_adjustment; let cost_adjustment_abs;
try { try {
cost_adjustment = deserialiseAmount('' + adjustment.cost_adjustment).quantity; cost_adjustment_abs = deserialiseAmount('' + adjustment.cost_adjustment_abs);
} catch (err) { } catch (err) {
if (err instanceof DeserialiseAmountError) { if (err instanceof DeserialiseAmountError) {
error.value = err.message; error.value = err.message;
@ -119,6 +126,8 @@
} }
} }
const cost_adjustment = adjustment.sign === 'dr' ? cost_adjustment_abs.quantity : -cost_adjustment_abs.quantity;
const session = await db.load(); const session = await db.load();
if (adjustment.id === null) { if (adjustment.id === null) {

View File

@ -1,5 +1,5 @@
<!-- <!--
DrCr: Double-entry bookkeeping framework DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo) Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
@ -41,7 +41,8 @@
acquisition_dt: null!, acquisition_dt: null!,
dt: null!, dt: null!,
description: null!, description: null!,
cost_adjustment: null!, sign: null!,
cost_adjustment_abs: null!,
} as EditingCGTAdjustment); } as EditingCGTAdjustment);
async function load() { async function load() {
@ -59,7 +60,8 @@
rawAdjustment.asset = serialiseAmount(rawAdjustment.quantity, rawAdjustment.commodity); rawAdjustment.asset = serialiseAmount(rawAdjustment.quantity, rawAdjustment.commodity);
rawAdjustment.acquisition_dt = dayjs(rawAdjustment.acquisition_dt).format('YYYY-MM-DD'); rawAdjustment.acquisition_dt = dayjs(rawAdjustment.acquisition_dt).format('YYYY-MM-DD');
rawAdjustment.dt = dayjs(rawAdjustment.dt).format('YYYY-MM-DD'); rawAdjustment.dt = dayjs(rawAdjustment.dt).format('YYYY-MM-DD');
rawAdjustment.cost_adjustment = serialiseAmount(rawAdjustment.cost_adjustment, db.metadata.reporting_commodity); rawAdjustment.sign = rawAdjustment.cost_adjustment >= 0 ? 'dr' : 'cr';
rawAdjustment.cost_adjustment_abs = serialiseAmount(Math.abs(rawAdjustment.cost_adjustment), db.metadata.reporting_commodity);
adjustment.value = rawAdjustment as EditingCGTAdjustment; adjustment.value = rawAdjustment as EditingCGTAdjustment;
} }

View File

@ -1,5 +1,5 @@
<!-- <!--
DrCr: Double-entry bookkeeping framework DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo) Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
@ -57,7 +57,13 @@
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> <div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
<span class="text-gray-500">{{ db.metadata.reporting_commodity }}</span> <span class="text-gray-500">{{ db.metadata.reporting_commodity }}</span>
</div> </div>
<input type="number" class="bordered-field pl-7" step="0.01" v-model="cost_adjustment" placeholder="0.00"> <input type="number" class="bordered-field pl-7 pr-16" step="0.01" v-model="cost_adjustment_abs" placeholder="0.00">
<div class="absolute inset-y-0 right-0 flex items-center">
<select class="h-full border-0 bg-transparent py-0 pl-2 pr-8 text-gray-900 focus:ring-2 focus:ring-inset focus:ring-indigo-600" v-model="sign">
<option value="dr">Dr</option>
<option value="cr">Cr</option>
</select>
</div>
</div> </div>
</div> </div>
@ -95,7 +101,8 @@
const commodity = ref(''); const commodity = ref('');
const dt = ref(dayjs().format('YYYY-MM-DD')); const dt = ref(dayjs().format('YYYY-MM-DD'));
const description = ref(''); const description = ref('');
const cost_adjustment = ref(null! as number); const cost_adjustment_abs = ref(null! as number);
const sign = ref('dr');
const error = ref(null as string | null); const error = ref(null as string | null);
@ -104,9 +111,9 @@
error.value = null; error.value = null;
let totalAdjustment; let totalAdjustmentAbs;
try { try {
totalAdjustment = deserialiseAmount('' + cost_adjustment.value).quantity; totalAdjustmentAbs = deserialiseAmount('' + cost_adjustment_abs.value);
} catch (err) { } catch (err) {
if (err instanceof DeserialiseAmountError) { if (err instanceof DeserialiseAmountError) {
error.value = err.message; error.value = err.message;
@ -116,6 +123,8 @@
} }
} }
const totalAdjustment = sign.value === 'dr' ? totalAdjustmentAbs.quantity : -totalAdjustmentAbs.quantity;
// Get all postings to the CGT asset account // Get all postings to the CGT asset account
const session = await db.load(); const session = await db.load();
const cgtPostings = await session.select( const cgtPostings = await session.select(

View File

@ -1,5 +1,5 @@
<!-- <!--
DrCr: Double-entry bookkeeping framework DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo) Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify This program is free software: you can redistribute it and/or modify
@ -37,6 +37,7 @@
acquisition_dt: dayjs().format('YYYY-MM-DD'), acquisition_dt: dayjs().format('YYYY-MM-DD'),
dt: dayjs().format('YYYY-MM-DD'), dt: dayjs().format('YYYY-MM-DD'),
description: '', description: '',
cost_adjustment: '', sign: 'dr',
cost_adjustment_abs: '',
} as EditingCGTAdjustment); } as EditingCGTAdjustment);
</script> </script>