Compare commits

..

4 Commits

8 changed files with 51 additions and 52 deletions

View File

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

View File

@ -183,7 +183,7 @@ export function serialiseAmount(quantity: number, commodity: string): string {
function parseFloatStrict(quantity: string): number {
// 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);
}
return parseFloat(quantity);
@ -216,12 +216,21 @@ export function deserialiseAmount(amount: string): { quantity: number, commodity
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') {
// Check for single letter commodity
if (amount.length === 1) {
throw new DeserialiseAmountError('Quantity cannot be blank (expected quantity after commodity symbol ' + amount + ')');
}
if (amount.charAt(1) < '0' || amount.charAt(1) > '9') {
if ((amount.charAt(1) < '0' || amount.charAt(1) > '9') && amount.charAt(1) !== '-') {
throw new DeserialiseAmountError('Invalid quantity: ' + amount + ' (expected quantity after single-letter commodity symbol ' + amount.charAt(0) + ')');
}
@ -317,6 +326,17 @@ export interface JoinedTransactionPosting {
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 {
id: number | null,
source_account: string,

View File

@ -1,5 +1,5 @@
<!--
DrCr: Web-based double-entry bookkeeping framework
DrCr: Double-entry bookkeeping framework
Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo)
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 { onUnmounted, ref, watch } from 'vue';
import { Transaction } from '../db.ts';
import { Transaction, postingQuantityAsCost } from '../db.ts';
import { pp, ppWithCommodity } from '../display.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 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">
${ posting.quantity >= 0 ? pp(posting.quantity_ascost!) : '' }
${ posting.quantity >= 0 ? pp(postingQuantityAsCost(posting)) : '' }
</td>
<td class="py-0.5 pl-1 text-gray-900 lg:w-[12ex] text-end">
${ posting.quantity < 0 ? pp(-posting.quantity_ascost!) : '' }
${ posting.quantity < 0 ? pp(-postingQuantityAsCost(posting)) : '' }
</td>
</tr>`
);

View File

@ -49,7 +49,7 @@
import { onMounted, onUnmounted, watch } from 'vue';
import { useRoute } from 'vue-router';
import { Transaction } from '../db.ts';
import { Transaction, postingQuantityAsCost } from '../db.ts';
import { pp } from '../display.ts';
import { renderComponent } from '../webutil.ts';
@ -66,7 +66,7 @@
const transaction = transactions[i];
for (const posting of transaction.postings) {
if (posting.account === route.params.account) {
balance += posting.quantity_ascost!;
balance += postingQuantityAsCost(posting);
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 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 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(-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(-postingQuantityAsCost(thisAccountPosting!)) : '' }</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>
</tr>`
@ -125,8 +125,8 @@
<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"><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(posting.quantity_ascost!) : '' }</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(-postingQuantityAsCost(posting)) : '' }</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>
</tr>`

View File

@ -1,5 +1,5 @@
<!--
DrCr: Web-based double-entry bookkeeping framework
DrCr: Double-entry bookkeeping framework
Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify
@ -46,13 +46,7 @@
<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>
</div>
<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>
<input type="number" class="bordered-field pl-7" step="0.01" v-model="adjustment.cost_adjustment" placeholder="0.00">
</div>
</div>
@ -90,8 +84,7 @@
acquisition_dt: string,
dt: string,
description: string,
sign: string,
cost_adjustment_abs: string,
cost_adjustment: string,
}
const { adjustment } = defineProps<{ adjustment: EditingCGTAdjustment }>();
@ -114,9 +107,9 @@
}
}
let cost_adjustment_abs;
let cost_adjustment;
try {
cost_adjustment_abs = deserialiseAmount('' + adjustment.cost_adjustment_abs);
cost_adjustment = deserialiseAmount('' + adjustment.cost_adjustment).quantity;
} catch (err) {
if (err instanceof DeserialiseAmountError) {
error.value = err.message;
@ -126,8 +119,6 @@
}
}
const cost_adjustment = adjustment.sign === 'dr' ? cost_adjustment_abs.quantity : -cost_adjustment_abs.quantity;
const session = await db.load();
if (adjustment.id === null) {

View File

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

View File

@ -1,5 +1,5 @@
<!--
DrCr: Web-based double-entry bookkeeping framework
DrCr: Double-entry bookkeeping framework
Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify
@ -57,13 +57,7 @@
<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>
</div>
<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>
<input type="number" class="bordered-field pl-7" step="0.01" v-model="cost_adjustment" placeholder="0.00">
</div>
</div>
@ -101,8 +95,7 @@
const commodity = ref('');
const dt = ref(dayjs().format('YYYY-MM-DD'));
const description = ref('');
const cost_adjustment_abs = ref(null! as number);
const sign = ref('dr');
const cost_adjustment = ref(null! as number);
const error = ref(null as string | null);
@ -111,9 +104,9 @@
error.value = null;
let totalAdjustmentAbs;
let totalAdjustment;
try {
totalAdjustmentAbs = deserialiseAmount('' + cost_adjustment_abs.value);
totalAdjustment = deserialiseAmount('' + cost_adjustment.value).quantity;
} catch (err) {
if (err instanceof DeserialiseAmountError) {
error.value = err.message;
@ -123,8 +116,6 @@
}
}
const totalAdjustment = sign.value === 'dr' ? totalAdjustmentAbs.quantity : -totalAdjustmentAbs.quantity;
// Get all postings to the CGT asset account
const session = await db.load();
const cgtPostings = await session.select(

View File

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