General ledger report using libdrcr
This commit is contained in:
parent
51a40e5ed9
commit
ad3276bbd5
@ -90,6 +90,7 @@ pub fn run() {
|
|||||||
.invoke_handler(tauri::generate_handler![
|
.invoke_handler(tauri::generate_handler![
|
||||||
get_open_filename,
|
get_open_filename,
|
||||||
set_open_filename,
|
set_open_filename,
|
||||||
|
libdrcr_bridge::get_all_transactions_except_earnings_to_equity,
|
||||||
libdrcr_bridge::get_balance_sheet,
|
libdrcr_bridge::get_balance_sheet,
|
||||||
libdrcr_bridge::get_income_statement,
|
libdrcr_bridge::get_income_statement,
|
||||||
libdrcr_bridge::get_trial_balance,
|
libdrcr_bridge::get_trial_balance,
|
||||||
|
@ -26,17 +26,18 @@ use libdrcr::reporting::generate_report;
|
|||||||
use libdrcr::reporting::steps::register_lookup_fns;
|
use libdrcr::reporting::steps::register_lookup_fns;
|
||||||
use libdrcr::reporting::types::{
|
use libdrcr::reporting::types::{
|
||||||
DateArgs, DateStartDateEndArgs, MultipleDateArgs, MultipleDateStartDateEndArgs,
|
DateArgs, DateStartDateEndArgs, MultipleDateArgs, MultipleDateStartDateEndArgs,
|
||||||
ReportingContext, ReportingProductId, ReportingProductKind, VoidArgs,
|
ReportingContext, ReportingProduct, ReportingProductId, ReportingProductKind, Transactions,
|
||||||
|
VoidArgs,
|
||||||
};
|
};
|
||||||
use tauri::State;
|
use tauri::State;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
|
|
||||||
async fn get_dynamic_report(
|
async fn get_report(
|
||||||
state: State<'_, Mutex<AppState>>,
|
state: State<'_, Mutex<AppState>>,
|
||||||
target: ReportingProductId,
|
target: &ReportingProductId,
|
||||||
) -> Result<String, ()> {
|
) -> Box<dyn ReportingProduct> {
|
||||||
let state = state.lock().await;
|
let state = state.lock().await;
|
||||||
let db_filename = state.db_filename.clone().unwrap();
|
let db_filename = state.db_filename.clone().unwrap();
|
||||||
|
|
||||||
@ -60,11 +61,29 @@ async fn get_dynamic_report(
|
|||||||
target.clone(),
|
target.clone(),
|
||||||
];
|
];
|
||||||
let products = generate_report(targets, Arc::new(context)).await.unwrap();
|
let products = generate_report(targets, Arc::new(context)).await.unwrap();
|
||||||
let result = products.get_or_err(&target).unwrap();
|
let result = products.get_owned_or_err(&target).unwrap();
|
||||||
|
|
||||||
let dynamic_report = result.downcast_ref::<DynamicReport>().unwrap().to_json();
|
result
|
||||||
|
}
|
||||||
|
|
||||||
Ok(dynamic_report)
|
#[tauri::command]
|
||||||
|
pub(crate) async fn get_all_transactions_except_earnings_to_equity(
|
||||||
|
state: State<'_, Mutex<AppState>>,
|
||||||
|
) -> Result<String, ()> {
|
||||||
|
Ok(get_report(
|
||||||
|
state,
|
||||||
|
&ReportingProductId {
|
||||||
|
name: "AllTransactionsExceptEarningsToEquity",
|
||||||
|
kind: ReportingProductKind::Transactions,
|
||||||
|
args: Box::new(DateArgs {
|
||||||
|
date: NaiveDate::from_ymd_opt(9999, 12, 31).unwrap(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.downcast_ref::<Transactions>()
|
||||||
|
.unwrap()
|
||||||
|
.to_json())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@ -79,9 +98,9 @@ pub(crate) async fn get_balance_sheet(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
get_dynamic_report(
|
Ok(get_report(
|
||||||
state,
|
state,
|
||||||
ReportingProductId {
|
&ReportingProductId {
|
||||||
name: "BalanceSheet",
|
name: "BalanceSheet",
|
||||||
kind: ReportingProductKind::Generic,
|
kind: ReportingProductKind::Generic,
|
||||||
args: Box::new(MultipleDateArgs {
|
args: Box::new(MultipleDateArgs {
|
||||||
@ -90,6 +109,9 @@ pub(crate) async fn get_balance_sheet(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
.downcast_ref::<DynamicReport>()
|
||||||
|
.unwrap()
|
||||||
|
.to_json())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@ -105,9 +127,9 @@ pub(crate) async fn get_income_statement(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
get_dynamic_report(
|
Ok(get_report(
|
||||||
state,
|
state,
|
||||||
ReportingProductId {
|
&ReportingProductId {
|
||||||
name: "IncomeStatement",
|
name: "IncomeStatement",
|
||||||
kind: ReportingProductKind::Generic,
|
kind: ReportingProductKind::Generic,
|
||||||
args: Box::new(MultipleDateStartDateEndArgs {
|
args: Box::new(MultipleDateStartDateEndArgs {
|
||||||
@ -116,6 +138,9 @@ pub(crate) async fn get_income_statement(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
.downcast_ref::<DynamicReport>()
|
||||||
|
.unwrap()
|
||||||
|
.to_json())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
@ -125,13 +150,16 @@ pub(crate) async fn get_trial_balance(
|
|||||||
) -> Result<String, ()> {
|
) -> Result<String, ()> {
|
||||||
let date = NaiveDate::parse_from_str(&date, "%Y-%m-%d").expect("Invalid date");
|
let date = NaiveDate::parse_from_str(&date, "%Y-%m-%d").expect("Invalid date");
|
||||||
|
|
||||||
get_dynamic_report(
|
Ok(get_report(
|
||||||
state,
|
state,
|
||||||
ReportingProductId {
|
&ReportingProductId {
|
||||||
name: "TrialBalance",
|
name: "TrialBalance",
|
||||||
kind: ReportingProductKind::Generic,
|
kind: ReportingProductKind::Generic,
|
||||||
args: Box::new(DateArgs { date }),
|
args: Box::new(DateArgs { date }),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
.downcast_ref::<DynamicReport>()
|
||||||
|
.unwrap()
|
||||||
|
.to_json())
|
||||||
}
|
}
|
||||||
|
@ -60,16 +60,13 @@
|
|||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import Clusterize from 'clusterize.js';
|
import Clusterize from 'clusterize.js';
|
||||||
|
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
|
|
||||||
import { PencilIcon, PlusIcon } from '@heroicons/vue/24/outline';
|
import { PencilIcon, PlusIcon } from '@heroicons/vue/24/outline';
|
||||||
|
import { invoke } from '@tauri-apps/api/core';
|
||||||
import { onUnmounted, ref, watch } from 'vue';
|
import { onUnmounted, ref, watch } from 'vue';
|
||||||
|
|
||||||
import { Transaction, db } from '../db.ts';
|
import { Transaction } from '../db.ts';
|
||||||
import { pp, ppWithCommodity } from '../display.ts';
|
import { pp, ppWithCommodity } from '../display.ts';
|
||||||
import { ReportingStage, ReportingWorkflow } from '../reporting.ts';
|
|
||||||
import { renderComponent } from '../webutil.ts';
|
import { renderComponent } from '../webutil.ts';
|
||||||
|
|
||||||
const commodityDetail = ref(false);
|
const commodityDetail = ref(false);
|
||||||
@ -78,14 +75,9 @@
|
|||||||
let clusterize: Clusterize | null = null;
|
let clusterize: Clusterize | null = null;
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
const session = await db.load();
|
transactions.value = JSON.parse(await invoke('get_all_transactions_except_earnings_to_equity'));
|
||||||
const reportingWorkflow = new ReportingWorkflow();
|
|
||||||
await reportingWorkflow.generate(session);
|
|
||||||
|
|
||||||
transactions.value = reportingWorkflow.getTransactionsAtStage(ReportingStage.FINAL_STAGE);
|
// Display transactions in reverse chronological order - they are returned in arbitrary order
|
||||||
|
|
||||||
// Display transactions in reverse chronological order
|
|
||||||
// We must sort here because they are returned by reportingWorkflow in order of ReportingStage
|
|
||||||
transactions.value.sort((a, b) => (b.dt.localeCompare(a.dt)) || ((b.id ?? 0) - (a.id ?? 0)));
|
transactions.value.sort((a, b) => (b.dt.localeCompare(a.dt)) || ((b.id ?? 0) - (a.id ?? 0)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
294
src/reporting.ts
294
src/reporting.ts
@ -1,294 +0,0 @@
|
|||||||
/*
|
|
||||||
DrCr: Web-based double-entry bookkeeping framework
|
|
||||||
Copyright (C) 2022–2025 Lee Yingtong Li (RunasSudo)
|
|
||||||
|
|
||||||
This program is free software: you can redistribute it and/or modify
|
|
||||||
it under the terms of the GNU Affero General Public License as published by
|
|
||||||
the Free Software Foundation, either version 3 of the License, or
|
|
||||||
(at your option) any later version.
|
|
||||||
|
|
||||||
This program is distributed in the hope that it will be useful,
|
|
||||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
GNU Affero General Public License for more details.
|
|
||||||
|
|
||||||
You should have received a copy of the GNU Affero General Public License
|
|
||||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import dayjs from 'dayjs';
|
|
||||||
|
|
||||||
import { asCost } from './amounts.ts';
|
|
||||||
import { DT_FORMAT, JoinedTransactionPosting, StatementLine, Transaction, db, getAccountsForKind, joinedToTransactions, totalBalances, totalBalancesAtDate } from './db.ts';
|
|
||||||
import { ExtendedDatabase } from './dbutil.ts';
|
|
||||||
|
|
||||||
import { BalanceSheetReport } from './reports/BalanceSheetReport.vue';
|
|
||||||
import { DrcrReport } from './reports/base.ts';
|
|
||||||
import TrialBalanceReport from './reports/TrialBalanceReport.ts';
|
|
||||||
import { IncomeStatementReport } from './reports/IncomeStatementReport.vue';
|
|
||||||
|
|
||||||
export enum ReportingStage {
|
|
||||||
// Load transactions from database
|
|
||||||
TransactionsFromDatabase = 100,
|
|
||||||
|
|
||||||
// Load unreconciled statement lines and other ordinary API transactions
|
|
||||||
OrdinaryAPITransactions = 200,
|
|
||||||
|
|
||||||
// Recognise accumulated surplus as equity
|
|
||||||
AccumulatedSurplusToEquity = 300,
|
|
||||||
|
|
||||||
// Interim income statement considering only DB and ordinary API transactions
|
|
||||||
InterimIncomeStatement = 400,
|
|
||||||
|
|
||||||
// Income tax estimation
|
|
||||||
//Tax = 500,
|
|
||||||
|
|
||||||
// Final income statement
|
|
||||||
//IncomeStatement = 600,
|
|
||||||
|
|
||||||
// Final balance sheet
|
|
||||||
BalanceSheet = 700,
|
|
||||||
|
|
||||||
FINAL_STAGE = BalanceSheet
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ReportingWorkflow {
|
|
||||||
transactionsForStage: Map<ReportingStage, Transaction[]> = new Map();
|
|
||||||
reportsForStage: Map<ReportingStage, DrcrReport[]> = new Map();
|
|
||||||
|
|
||||||
async generate(session: ExtendedDatabase, dt?: string, dtStart?: string) {
|
|
||||||
// ------------------------
|
|
||||||
// TransactionsFromDatabase
|
|
||||||
|
|
||||||
let balances: Map<string, number>;
|
|
||||||
|
|
||||||
{
|
|
||||||
// Load balances from database
|
|
||||||
if (dt) {
|
|
||||||
balances = await totalBalancesAtDate(session, dt);
|
|
||||||
} else {
|
|
||||||
balances = await totalBalances(session);
|
|
||||||
}
|
|
||||||
this.reportsForStage.set(ReportingStage.TransactionsFromDatabase, [new TrialBalanceReport(balances)]);
|
|
||||||
|
|
||||||
// Load transactions from database
|
|
||||||
let joinedTransactionPostings: JoinedTransactionPosting[];
|
|
||||||
if (dt) {
|
|
||||||
joinedTransactionPostings = await session.select(
|
|
||||||
`SELECT transaction_id, dt, transaction_description, id, description, account, quantity, commodity, quantity_ascost, running_balance
|
|
||||||
FROM transactions_with_running_balances
|
|
||||||
WHERE DATE(dt) <= DATE($1)
|
|
||||||
ORDER BY dt, transaction_id, id`,
|
|
||||||
[dt]
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
joinedTransactionPostings = await session.select(
|
|
||||||
`SELECT transaction_id, dt, transaction_description, id, description, account, quantity, commodity, quantity_ascost, running_balance
|
|
||||||
FROM transactions_with_running_balances
|
|
||||||
ORDER BY dt, transaction_id, id`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const transactions = joinedToTransactions(joinedTransactionPostings);
|
|
||||||
this.transactionsForStage.set(ReportingStage.TransactionsFromDatabase, transactions);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------
|
|
||||||
// OrdinaryAPITransactions
|
|
||||||
|
|
||||||
{
|
|
||||||
// Get unreconciled statement lines
|
|
||||||
let unreconciledStatementLines: StatementLine[];
|
|
||||||
if (dt) {
|
|
||||||
unreconciledStatementLines = await session.select(
|
|
||||||
// On testing, JOIN is much faster than WHERE NOT EXISTS
|
|
||||||
`SELECT statement_lines.* FROM statement_lines
|
|
||||||
LEFT JOIN statement_line_reconciliations ON statement_lines.id = statement_line_reconciliations.statement_line_id
|
|
||||||
WHERE statement_line_reconciliations.id IS NULL AND DATE(dt) <= DATE($1)`,
|
|
||||||
[dt]
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
unreconciledStatementLines = await session.select(
|
|
||||||
`SELECT statement_lines.* FROM statement_lines
|
|
||||||
LEFT JOIN statement_line_reconciliations ON statement_lines.id = statement_line_reconciliations.statement_line_id
|
|
||||||
WHERE statement_line_reconciliations.id IS NULL`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const transactions = [];
|
|
||||||
for (const line of unreconciledStatementLines) {
|
|
||||||
const unclassifiedAccount = line.quantity >= 0 ? 'Unclassified Statement Line Debits' : 'Unclassified Statement Line Credits';
|
|
||||||
transactions.push(new Transaction(
|
|
||||||
null,
|
|
||||||
line.dt,
|
|
||||||
line.description,
|
|
||||||
[
|
|
||||||
{
|
|
||||||
id: null,
|
|
||||||
description: null,
|
|
||||||
account: line.source_account,
|
|
||||||
quantity: line.quantity,
|
|
||||||
commodity: line.commodity,
|
|
||||||
quantity_ascost: asCost(line.quantity, line.commodity),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: null,
|
|
||||||
description: null,
|
|
||||||
account: unclassifiedAccount,
|
|
||||||
quantity: -line.quantity,
|
|
||||||
commodity: line.commodity,
|
|
||||||
quantity_ascost: asCost(-line.quantity, line.commodity),
|
|
||||||
}
|
|
||||||
]
|
|
||||||
));
|
|
||||||
}
|
|
||||||
this.transactionsForStage.set(ReportingStage.OrdinaryAPITransactions, transactions);
|
|
||||||
|
|
||||||
// Recompute balances
|
|
||||||
balances = applyTransactionsToBalances(balances, transactions);
|
|
||||||
this.reportsForStage.set(ReportingStage.OrdinaryAPITransactions, [new TrialBalanceReport(balances)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------------
|
|
||||||
// AccumulatedSurplusToEquity
|
|
||||||
|
|
||||||
{
|
|
||||||
// Compute balances at period start for TransactionsFromDatabase
|
|
||||||
let dayBeforePeriodStart;
|
|
||||||
if (dtStart) {
|
|
||||||
dayBeforePeriodStart = dayjs(dtStart).subtract(1, 'day');
|
|
||||||
} else {
|
|
||||||
dayBeforePeriodStart = dayjs(db.metadata.eofy_date).subtract(1, 'year');
|
|
||||||
}
|
|
||||||
const balancesAtPeriodStart = await totalBalancesAtDate(session, dayBeforePeriodStart.format('YYYY-MM-DD'));
|
|
||||||
|
|
||||||
// Add balances at period start for OrdinaryAPITransactions
|
|
||||||
for (const transaction of this.transactionsForStage.get(ReportingStage.OrdinaryAPITransactions)!) {
|
|
||||||
if (!dayjs(transaction.dt).isAfter(dayBeforePeriodStart)) {
|
|
||||||
for (const posting of transaction.postings) {
|
|
||||||
balancesAtPeriodStart.set(posting.account, (balancesAtPeriodStart.get(posting.account) ?? 0) + posting.quantity_ascost!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get income and expense accounts
|
|
||||||
const incomeAccounts = await getAccountsForKind(session, 'drcr.income');
|
|
||||||
const expenseAccounts = await getAccountsForKind(session, 'drcr.expense');
|
|
||||||
const pandlAccounts = [...incomeAccounts, ...expenseAccounts];
|
|
||||||
pandlAccounts.sort();
|
|
||||||
|
|
||||||
// Prepare transactions
|
|
||||||
const transactions = [];
|
|
||||||
for (const account of pandlAccounts) {
|
|
||||||
if (balancesAtPeriodStart.has(account)) {
|
|
||||||
const balanceAtPeriodStart = balancesAtPeriodStart.get(account)!;
|
|
||||||
if (balanceAtPeriodStart === 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
transactions.push(new Transaction(
|
|
||||||
null,
|
|
||||||
dayBeforePeriodStart.format(DT_FORMAT),
|
|
||||||
'Accumulated surplus/deficit',
|
|
||||||
[
|
|
||||||
{
|
|
||||||
id: null,
|
|
||||||
description: null,
|
|
||||||
account: account,
|
|
||||||
quantity: -balanceAtPeriodStart,
|
|
||||||
commodity: db.metadata.reporting_commodity,
|
|
||||||
quantity_ascost: asCost(-balanceAtPeriodStart, db.metadata.reporting_commodity),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: null,
|
|
||||||
description: null,
|
|
||||||
account: 'Accumulated surplus (deficit)',
|
|
||||||
quantity: balanceAtPeriodStart,
|
|
||||||
commodity: db.metadata.reporting_commodity,
|
|
||||||
quantity_ascost: asCost(balanceAtPeriodStart, db.metadata.reporting_commodity),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.transactionsForStage.set(ReportingStage.AccumulatedSurplusToEquity, transactions);
|
|
||||||
|
|
||||||
// Recompute balances
|
|
||||||
balances = applyTransactionsToBalances(balances, transactions);
|
|
||||||
this.reportsForStage.set(ReportingStage.AccumulatedSurplusToEquity, [new TrialBalanceReport(balances)]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------
|
|
||||||
// InterimIncomeStatement
|
|
||||||
|
|
||||||
let incomeStatementReport;
|
|
||||||
{
|
|
||||||
incomeStatementReport = new IncomeStatementReport();
|
|
||||||
await incomeStatementReport.generate(balances);
|
|
||||||
this.reportsForStage.set(ReportingStage.InterimIncomeStatement, [incomeStatementReport]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ------------
|
|
||||||
// BalanceSheet
|
|
||||||
|
|
||||||
{
|
|
||||||
const balanceSheetReport = new BalanceSheetReport();
|
|
||||||
await balanceSheetReport.generate(balances, incomeStatementReport);
|
|
||||||
this.reportsForStage.set(ReportingStage.BalanceSheet, [balanceSheetReport]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
getReportAtStage(stage: ReportingStage, reportType: any): DrcrReport {
|
|
||||||
// TODO: This function needs generics
|
|
||||||
const reportsForTheStage = this.reportsForStage.get(stage);
|
|
||||||
if (!reportsForTheStage) {
|
|
||||||
throw new Error('Attempt to get report for unavailable stage');
|
|
||||||
}
|
|
||||||
|
|
||||||
const report = reportsForTheStage.find((r) => r instanceof reportType);
|
|
||||||
if (report) {
|
|
||||||
return report;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recurse earlier stages
|
|
||||||
const stages = [...this.reportsForStage.keys()];
|
|
||||||
stages.reverse();
|
|
||||||
for (const earlierStage of stages) {
|
|
||||||
if (earlierStage >= stage) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const report = this.reportsForStage.get(earlierStage)!.find((r) => r instanceof reportType);
|
|
||||||
if (report) {
|
|
||||||
return report;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Report does not exist at requested stage or any earlier stage');
|
|
||||||
}
|
|
||||||
|
|
||||||
getTransactionsAtStage(stage: ReportingStage): Transaction[] {
|
|
||||||
const transactions: Transaction[] = [];
|
|
||||||
for (const [curStage, curTransactions] of this.transactionsForStage.entries()) {
|
|
||||||
if (curStage <= stage) {
|
|
||||||
transactions.push(...curTransactions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return transactions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyTransactionsToBalances(balances: Map<string, number>, transactions: Transaction[]): Map<string, number> {
|
|
||||||
// Initialise new balances
|
|
||||||
const newBalances: Map<string, number> = new Map([...balances.entries()]);
|
|
||||||
|
|
||||||
// Apply transactions
|
|
||||||
for (const transaction of transactions) {
|
|
||||||
for (const posting of transaction.postings) {
|
|
||||||
const openingBalance = newBalances.get(posting.account) ?? 0;
|
|
||||||
const runningBalance = openingBalance + posting.quantity_ascost!;
|
|
||||||
newBalances.set(posting.account, runningBalance);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort accounts
|
|
||||||
return new Map([...newBalances.entries()].sort((a, b) => a[0].localeCompare(b[0])));
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user