gnucash/libgnucash/engine/Scrub.cpp
2024-05-05 14:35:38 +08:00

1549 lines
47 KiB
C++

/********************************************************************\
* Scrub.c -- convert single-entry accounts into clean double-entry *
* *
* This program is free software; you can redistribute it and/or *
* modify it under the terms of the GNU General Public License as *
* published by the Free Software Foundation; either version 2 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 General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License*
* along with this program; if not, contact: *
* *
* Free Software Foundation Voice: +1-617-542-5942 *
* 51 Franklin Street, Fifth Floor Fax: +1-617-542-2652 *
* Boston, MA 02110-1301, USA gnu@gnu.org *
* *
\********************************************************************/
/*
* FILE:
* Scrub.c
*
* FUNCTION:
* Provides a set of functions and utilities for scrubbing clean
* single-entry accounts so that they can be promoted into
* self-consistent, clean double-entry accounts.
*
* HISTORY:
* Created by Linas Vepstas December 1998
* Copyright (c) 1998-2000, 2003 Linas Vepstas <linas@linas.org>
* Copyright (c) 2002 Christian Stimming
* Copyright (c) 2006 David Hampton
*/
#include <config.h>
#include <glib.h>
#include <glib/gi18n.h>
#include <stdio.h>
#include <string.h>
#include <stdint.h>
#include <stdbool.h>
#include "Account.h"
#include "AccountP.hpp"
#include "Account.hpp"
#include "Scrub.h"
#include "Transaction.h"
#include "TransactionP.hpp"
#include "gnc-commodity.h"
#include "qofinstance-p.h"
#include "gnc-session.h"
#undef G_LOG_DOMAIN
#define G_LOG_DOMAIN "gnc.engine.scrub"
static QofLogModule log_module = G_LOG_DOMAIN;
static gboolean abort_now = FALSE;
static gint scrub_depth = 0;
static Account* xaccScrubUtilityGetOrMakeAccount (Account *root,
gnc_commodity* currency,
const char* accname,
GNCAccountType acctype,
gboolean placeholder,
gboolean checkname);
void
gnc_set_abort_scrub (gboolean abort)
{
abort_now = abort;
}
gboolean
gnc_get_abort_scrub (void)
{
return abort_now;
}
gboolean
gnc_get_ongoing_scrub (void)
{
return scrub_depth > 0;
}
/* ================================================================ */
static void add_transactions (const Account *account, GHashTable **ht)
{
for (auto s : xaccAccountGetSplits (account))
g_hash_table_add (*ht, xaccSplitGetParent (s));
}
static GList*
get_all_transactions (Account *account, bool descendants)
{
GHashTable *ht = g_hash_table_new (g_direct_hash, g_direct_equal);
add_transactions (account, &ht);
if (descendants)
gnc_account_foreach_descendant (account, (AccountCb)add_transactions, &ht);
GList *rv = g_hash_table_get_keys (ht);
g_hash_table_destroy (ht);
return rv;
}
/* ================================================================ */
static void
TransScrubOrphansFast (Transaction *trans, Account *root)
{
g_return_if_fail (trans && trans->common_currency && root);
for (GList *node = trans->splits; node; node = node->next)
{
Split *split = GNC_SPLIT(node->data);
if (abort_now) break;
if (split->acc) continue;
DEBUG ("Found an orphan\n");
gchar *accname = g_strconcat
(_("Orphan"), "-", gnc_commodity_get_mnemonic (trans->common_currency),
nullptr);
Account *orph = xaccScrubUtilityGetOrMakeAccount
(root, trans->common_currency, accname, ACCT_TYPE_BANK, false, true);
g_free (accname);
if (!orph) continue;
xaccSplitSetAccount(split, orph);
}
}
static void
AccountScrubOrphans (Account *acc, bool descendants, QofPercentageFunc percentagefunc)
{
if (!acc) return;
scrub_depth++;
GList *transactions = get_all_transactions (acc, descendants);
gint total_trans = g_list_length (transactions);
const char *message = _( "Looking for orphans in transaction: %u of %u");
guint current_trans = 0;
for (GList *node = transactions; node; current_trans++, node = node->next)
{
Transaction *trans = GNC_TRANSACTION(node->data);
if (current_trans % 10 == 0)
{
char *progress_msg = g_strdup_printf (message, current_trans, total_trans);
(percentagefunc)(progress_msg, (100 * current_trans) / total_trans);
g_free (progress_msg);
if (abort_now) break;
}
TransScrubOrphansFast (trans, gnc_account_get_root (acc));
}
(percentagefunc)(nullptr, -1.0);
scrub_depth--;
g_list_free (transactions);
}
void
xaccAccountScrubOrphans (Account *acc, QofPercentageFunc percentagefunc)
{
AccountScrubOrphans (acc, false, percentagefunc);
}
void
xaccAccountTreeScrubOrphans (Account *acc, QofPercentageFunc percentagefunc)
{
AccountScrubOrphans (acc, true, percentagefunc);
}
void
xaccTransScrubOrphans (Transaction *trans)
{
SplitList *node;
QofBook *book = nullptr;
Account *root = nullptr;
if (!trans) return;
for (node = trans->splits; node; node = node->next)
{
Split *split = GNC_SPLIT(node->data);
if (abort_now) break;
if (split->acc)
{
TransScrubOrphansFast (trans, gnc_account_get_root(split->acc));
return;
}
}
/* If we got to here, then *none* of the splits belonged to an
* account. Not a happy situation. We should dig an account
* out of the book the transaction belongs to.
* XXX we should probably *always* to this, instead of the above loop!
*/
PINFO ("Free Floating Transaction!");
book = xaccTransGetBook (trans);
root = gnc_book_get_root_account (book);
TransScrubOrphansFast (trans, root);
}
/* ================================================================ */
void
xaccAccountTreeScrubSplits (Account *account)
{
if (!account) return;
xaccAccountScrubSplits (account);
gnc_account_foreach_descendant(account,
(AccountCb)xaccAccountScrubSplits, nullptr);
}
void
xaccAccountScrubSplits (Account *account)
{
scrub_depth++;
for (auto s : xaccAccountGetSplits (account))
{
if (abort_now) break;
xaccSplitScrub (s);
}
scrub_depth--;
}
/* if dry_run is true, this function will analyze the split and
return true if the split will be modified during the actual scrub. */
static bool
split_scrub_or_dry_run (Split *split, bool dry_run)
{
Account *account;
Transaction *trans;
gnc_numeric value, amount;
gnc_commodity *currency, *acc_commodity;
int scu;
if (!split) return false;
ENTER ("(split=%p)", split);
trans = xaccSplitGetParent (split);
if (!trans)
{
LEAVE("no trans");
return false;
}
account = xaccSplitGetAccount (split);
/* If there's no account, this split is an orphan.
* We need to fix that first, before proceeding.
*/
if (!account)
{
if (dry_run)
return true;
else
xaccTransScrubOrphans (trans);
account = xaccSplitGetAccount (split);
}
/* Grrr... the register gnc_split_register_load() line 203 of
* src/register/ledger-core/split-register-load.c will create
* free-floating bogus transactions. Ignore these for now ...
*/
if (!account)
{
PINFO ("Free Floating Transaction!");
LEAVE ("no account");
return false;
}
/* Split amounts and values should be valid numbers */
value = xaccSplitGetValue (split);
if (gnc_numeric_check (value))
{
value = gnc_numeric_zero();
if (dry_run)
return true;
else
xaccSplitSetValue (split, value);
}
amount = xaccSplitGetAmount (split);
if (gnc_numeric_check (amount))
{
amount = gnc_numeric_zero();
if (dry_run)
return true;
else
xaccSplitSetAmount (split, amount);
}
currency = xaccTransGetCurrency (trans);
/* If the account doesn't have a commodity,
* we should attempt to fix that first.
*/
acc_commodity = xaccAccountGetCommodity(account);
if (!acc_commodity)
{
if (dry_run)
return true;
else
xaccAccountScrubCommodity (account);
}
if (!acc_commodity || !gnc_commodity_equiv(acc_commodity, currency))
{
LEAVE ("(split=%p) inequiv currency", split);
return false;
}
scu = MIN (xaccAccountGetCommoditySCU (account),
gnc_commodity_get_fraction (currency));
if (gnc_numeric_same (amount, value, scu, GNC_HOW_RND_ROUND_HALF_UP))
{
LEAVE("(split=%p) different values", split);
return false;
}
if (dry_run)
return true;
/*
* This will be hit every time you answer yes to the dialog "The
* current transaction has changed. Would you like to record it.
*/
PINFO ("Adjusted split with mismatched values, desc=\"%s\" memo=\"%s\""
" old amount %s %s, new amount %s",
trans->description, split->memo,
gnc_num_dbg_to_string (xaccSplitGetAmount(split)),
gnc_commodity_get_mnemonic (currency),
gnc_num_dbg_to_string (xaccSplitGetValue(split)));
xaccTransBeginEdit (trans);
xaccSplitSetAmount (split, value);
xaccTransCommitEdit (trans);
LEAVE ("(split=%p)", split);
return true;
}
/* ================================================================ */
static void
AccountScrubImbalance (Account *acc, bool descendants,
QofPercentageFunc percentagefunc)
{
const char *message = _( "Looking for imbalances in transaction date %s: %u of %u");
if (!acc) return;
QofBook *book = qof_session_get_book (gnc_get_current_session ());
Account *root = gnc_book_get_root_account (book);
GList *transactions = get_all_transactions (acc, descendants);
guint count = g_list_length (transactions), curr_trans = 0;
scrub_depth++;
for (GList *node = transactions; node; node = node->next, curr_trans++)
{
Transaction *trans = GNC_TRANSACTION(node->data);
if (abort_now) break;
PINFO("Start processing transaction %d of %d", curr_trans + 1, count);
if (curr_trans % 10 == 0)
{
char *date = qof_print_date (xaccTransGetDate (trans));
char *progress_msg = g_strdup_printf (message, date, curr_trans, count);
(percentagefunc)(progress_msg, (100 * curr_trans) / count);
g_free (progress_msg);
g_free (date);
}
TransScrubOrphansFast (trans, root);
xaccTransScrubCurrency(trans);
xaccTransScrubImbalance (trans, root, nullptr);
PINFO("Finished processing transaction %d of %d", curr_trans + 1, count);
}
(percentagefunc)(nullptr, -1.0);
scrub_depth--;
g_list_free (transactions);
}
void
xaccTransScrubSplits (Transaction *trans)
{
if (!trans) return;
gnc_commodity *currency = xaccTransGetCurrency (trans);
if (!currency)
PERR ("Transaction doesn't have a currency!");
bool must_scrub = false;
for (GList *n = xaccTransGetSplitList (trans); !must_scrub && n; n = g_list_next (n))
if (split_scrub_or_dry_run (GNC_SPLIT(n->data), true))
must_scrub = true;
if (!must_scrub)
return;
xaccTransBeginEdit(trans);
/* The split scrub expects the transaction to have a currency! */
for (GList *n = xaccTransGetSplitList (trans); n; n = g_list_next (n))
xaccSplitScrub (GNC_SPLIT(n->data));
xaccTransCommitEdit(trans);
}
/* ================================================================ */
void
xaccSplitScrub (Split *split)
{
split_scrub_or_dry_run (split, false);
}
/* ================================================================ */
void
xaccAccountTreeScrubImbalance (Account *acc, QofPercentageFunc percentagefunc)
{
AccountScrubImbalance (acc, true, percentagefunc);
}
void
xaccAccountScrubImbalance (Account *acc, QofPercentageFunc percentagefunc)
{
AccountScrubImbalance (acc, false, percentagefunc);
}
static Split *
get_balance_split (Transaction *trans, Account *root, Account *account,
gnc_commodity *commodity)
{
Split *balance_split;
gchar *accname;
if (!account ||
!gnc_commodity_equiv (commodity, xaccAccountGetCommodity(account)))
{
if (!root)
{
root = gnc_book_get_root_account (xaccTransGetBook (trans));
if (nullptr == root)
{
/* This can't occur, things should be in books */
PERR ("Bad data corruption, no root account in book");
return nullptr;
}
}
accname = g_strconcat (_("Imbalance"), "-",
gnc_commodity_get_mnemonic (commodity), nullptr);
account = xaccScrubUtilityGetOrMakeAccount (root, commodity,
accname, ACCT_TYPE_BANK,
FALSE, TRUE);
g_free (accname);
if (!account)
{
PERR ("Can't get balancing account");
return nullptr;
}
}
balance_split = xaccTransFindSplitByAccount(trans, account);
/* Put split into account before setting split value */
if (!balance_split)
{
balance_split = xaccMallocSplit (qof_instance_get_book(trans));
xaccTransBeginEdit (trans);
xaccSplitSetParent(balance_split, trans);
xaccSplitSetAccount(balance_split, account);
xaccTransCommitEdit (trans);
}
return balance_split;
}
static gnc_commodity*
find_root_currency(void)
{
QofSession *sess = gnc_get_current_session ();
Account *root = gnc_book_get_root_account (qof_session_get_book (sess));
gnc_commodity *root_currency = xaccAccountGetCommodity (root);
/* Some older books may not have a currency set on the root
* account. In that case find the first top-level INCOME account
* and use its currency. */
if (!root_currency)
{
GList *children = gnc_account_get_children (root);
for (GList *node = children; node && !root_currency;
node = g_list_next (node))
{
Account *child = GNC_ACCOUNT (node->data);
if (xaccAccountGetType (child) == ACCT_TYPE_INCOME)
root_currency = xaccAccountGetCommodity (child);
}
g_list_free (children);
}
return root_currency;
}
/* Get the trading split for a given commodity, creating it (and the
necessary parent accounts) if it doesn't exist. */
static Split *
get_trading_split (Transaction *trans, Account *base,
gnc_commodity *commodity)
{
Split *balance_split;
Account *trading_account;
Account *ns_account;
Account *account;
Account* root = gnc_book_get_root_account (xaccTransGetBook (trans));
trading_account = xaccScrubUtilityGetOrMakeAccount (root,
nullptr,
_("Trading"),
ACCT_TYPE_TRADING,
TRUE, FALSE);
if (!trading_account)
{
PERR ("Can't get trading account");
return nullptr;
}
ns_account = xaccScrubUtilityGetOrMakeAccount (trading_account,
nullptr,
gnc_commodity_get_namespace(commodity),
ACCT_TYPE_TRADING,
TRUE, TRUE);
if (!ns_account)
{
PERR ("Can't get namespace account");
return nullptr;
}
account = xaccScrubUtilityGetOrMakeAccount (ns_account, commodity,
gnc_commodity_get_mnemonic(commodity),
ACCT_TYPE_TRADING,
FALSE, FALSE);
if (!account)
{
PERR ("Can't get commodity account");
return nullptr;
}
balance_split = xaccTransFindSplitByAccount(trans, account);
/* Put split into account before setting split value */
if (!balance_split)
{
balance_split = xaccMallocSplit (qof_instance_get_book(trans));
xaccTransBeginEdit (trans);
xaccSplitSetParent(balance_split, trans);
xaccSplitSetAccount(balance_split, account);
xaccTransCommitEdit (trans);
}
return balance_split;
}
static void
add_balance_split (Transaction *trans, gnc_numeric imbalance,
Account *root, Account *account)
{
const gnc_commodity *commodity;
gnc_numeric old_value, new_value;
Split *balance_split;
gnc_commodity *currency = xaccTransGetCurrency (trans);
balance_split = get_balance_split(trans, root, account, currency);
if (!balance_split)
{
/* Error already logged */
LEAVE("");
return;
}
account = xaccSplitGetAccount(balance_split);
xaccTransBeginEdit (trans);
old_value = xaccSplitGetValue (balance_split);
/* Note: We have to round for the commodity's fraction, NOT any
* already existing denominator (bug #104343), because either one
* of the denominators might already be reduced. */
new_value = gnc_numeric_sub (old_value, imbalance,
gnc_commodity_get_fraction(currency),
GNC_HOW_RND_ROUND_HALF_UP);
xaccSplitSetValue (balance_split, new_value);
commodity = xaccAccountGetCommodity (account);
if (gnc_commodity_equiv (currency, commodity))
{
xaccSplitSetAmount (balance_split, new_value);
}
xaccSplitScrub (balance_split);
xaccTransCommitEdit (trans);
}
/* Balance a transaction without trading accounts. */
static void
gnc_transaction_balance_no_trading (Transaction *trans, Account *root,
Account *account)
{
gnc_numeric imbalance = xaccTransGetImbalanceValue (trans);
/* Make the value sum to zero */
if (! gnc_numeric_zero_p (imbalance))
{
PINFO ("Value unbalanced transaction");
add_balance_split (trans, imbalance, root, account);
}
}
static gnc_numeric
gnc_transaction_get_commodity_imbalance (Transaction *trans,
gnc_commodity *commodity)
{
/* Find the value imbalance in this commodity */
gnc_numeric val_imbalance = gnc_numeric_zero();
GList *splits = nullptr;
for (splits = trans->splits; splits; splits = splits->next)
{
Split *split = GNC_SPLIT(splits->data);
gnc_commodity *split_commodity =
xaccAccountGetCommodity(xaccSplitGetAccount(split));
if (xaccTransStillHasSplit (trans, split) &&
gnc_commodity_equal (commodity, split_commodity))
val_imbalance = gnc_numeric_add (val_imbalance,
xaccSplitGetValue (split),
GNC_DENOM_AUTO,
GNC_HOW_DENOM_EXACT);
}
return val_imbalance;
}
/* GFunc wrapper for xaccSplitDestroy */
static void
destroy_split (void* ptr)
{
Split *split = GNC_SPLIT (ptr);
if (split)
xaccSplitDestroy (split);
}
/* Balancing transactions with trading accounts works best when
* starting with no trading splits.
*/
static void
xaccTransClearTradingSplits (Transaction *trans)
{
GList *trading_splits = nullptr;
for (GList* node = trans->splits; node; node = node->next)
{
Split* split = GNC_SPLIT(node->data);
Account* acc = nullptr;
if (!split)
continue;
acc = xaccSplitGetAccount(split);
if (acc && xaccAccountGetType(acc) == ACCT_TYPE_TRADING)
trading_splits = g_list_prepend (trading_splits, node->data);
}
if (!trading_splits)
return;
xaccTransBeginEdit (trans);
/* destroy_splits doesn't actually free the splits but this gets
* the list itself freed.
*/
g_list_free_full (trading_splits, destroy_split);
xaccTransCommitEdit (trans);
}
static void
gnc_transaction_balance_trading (Transaction *trans, Account *root)
{
MonetaryList *imbal_list;
MonetaryList *imbalance_commod;
Split *balance_split = nullptr;
/* If the transaction is balanced, nothing more to do */
imbal_list = xaccTransGetImbalance (trans);
if (!imbal_list)
{
LEAVE("transaction is balanced");
return;
}
PINFO ("Currency unbalanced transaction");
for (imbalance_commod = imbal_list; imbalance_commod;
imbalance_commod = imbalance_commod->next)
{
auto imbal_mon = static_cast<gnc_monetary*>(imbalance_commod->data);
gnc_commodity *commodity;
gnc_numeric old_amount, new_amount;
const gnc_commodity *txn_curr = xaccTransGetCurrency (trans);
commodity = gnc_monetary_commodity (*imbal_mon);
balance_split = get_trading_split(trans, root, commodity);
if (!balance_split)
{
/* Error already logged */
gnc_monetary_list_free(imbal_list);
LEAVE("");
return;
}
xaccTransBeginEdit (trans);
old_amount = xaccSplitGetAmount (balance_split);
new_amount = gnc_numeric_sub (old_amount, gnc_monetary_value(*imbal_mon),
gnc_commodity_get_fraction(commodity),
GNC_HOW_RND_ROUND_HALF_UP);
xaccSplitSetAmount (balance_split, new_amount);
if (gnc_commodity_equal (txn_curr, commodity))
{
/* Imbalance commodity is the transaction currency, value in the
split must be the same as the amount */
xaccSplitSetValue (balance_split, new_amount);
}
else
{
gnc_numeric val_imbalance = gnc_transaction_get_commodity_imbalance (trans, commodity);
gnc_numeric old_value = xaccSplitGetValue (balance_split);
gnc_numeric new_value = gnc_numeric_sub (old_value, val_imbalance,
gnc_commodity_get_fraction(txn_curr),
GNC_HOW_RND_ROUND_HALF_UP);
xaccSplitSetValue (balance_split, new_value);
}
xaccSplitScrub (balance_split);
xaccTransCommitEdit (trans);
}
gnc_monetary_list_free(imbal_list);
}
/** Balance the transaction by adding more trading splits. This shouldn't
* ordinarily be necessary.
* @param trans the transaction to balance
* @param root the root account
*/
static void
gnc_transaction_balance_trading_more_splits (Transaction *trans, Account *root)
{
/* Copy the split list so we don't see the splits we're adding */
GList *splits_dup = g_list_copy(trans->splits), *splits = nullptr;
const gnc_commodity *txn_curr = xaccTransGetCurrency (trans);
for (splits = splits_dup; splits; splits = splits->next)
{
Split *split = GNC_SPLIT(splits->data);
if (! xaccTransStillHasSplit(trans, split)) continue;
if (!gnc_numeric_zero_p(xaccSplitGetValue(split)) &&
gnc_numeric_zero_p(xaccSplitGetAmount(split)))
{
gnc_commodity *commodity;
gnc_numeric old_value, new_value;
Split *balance_split;
commodity = xaccAccountGetCommodity(xaccSplitGetAccount(split));
if (!commodity)
{
PERR("Split has no commodity");
continue;
}
balance_split = get_trading_split(trans, root, commodity);
if (!balance_split)
{
/* Error already logged */
LEAVE("");
return;
}
xaccTransBeginEdit (trans);
old_value = xaccSplitGetValue (balance_split);
new_value = gnc_numeric_sub (old_value, xaccSplitGetValue(split),
gnc_commodity_get_fraction(txn_curr),
GNC_HOW_RND_ROUND_HALF_UP);
xaccSplitSetValue (balance_split, new_value);
/* Don't change the balance split's amount since the amount
is zero in the split we're working on */
xaccSplitScrub (balance_split);
xaccTransCommitEdit (trans);
}
}
g_list_free(splits_dup);
}
/** Correct transaction imbalances.
* @param trans The Transaction
* @param root The (hidden) root account, for the book default currency.
* @param account The account whose currency in which to balance.
*/
void
xaccTransScrubImbalance (Transaction *trans, Account *root,
Account *account)
{
gnc_numeric imbalance;
if (!trans) return;
ENTER ("()");
/* Must look for orphan splits even if there is no imbalance. */
xaccTransScrubSplits (trans);
/* Return immediately if things are balanced. */
if (xaccTransIsBalanced (trans))
{
LEAVE ("transaction is balanced");
return;
}
if (! xaccTransUseTradingAccounts (trans))
{
gnc_transaction_balance_no_trading (trans, root, account);
LEAVE ("transaction balanced, no managed trading accounts");
return;
}
xaccTransClearTradingSplits (trans);
imbalance = xaccTransGetImbalanceValue (trans);
if (! gnc_numeric_zero_p (imbalance))
{
PINFO ("Value unbalanced transaction");
add_balance_split (trans, imbalance, root, account);
}
gnc_transaction_balance_trading (trans, root);
if (gnc_numeric_zero_p(xaccTransGetImbalanceValue(trans)))
{
LEAVE ("()");
return;
}
/* If the transaction is still not balanced, it's probably because there
are splits with zero amount and non-zero value. These are usually
realized gain/loss splits. Add a reversing split for each of them to
balance the value. */
gnc_transaction_balance_trading_more_splits (trans, root);
if (!gnc_numeric_zero_p(xaccTransGetImbalanceValue(trans)))
PERR("Balancing currencies unbalanced value");
}
/* ================================================================ */
/* The xaccTransFindCommonCurrency () method returns
* a gnc_commodity indicating a currency denomination that all
* of the splits in this transaction have in common, using the
* old/obsolete currency/security fields of the split accounts.
*/
static gnc_commodity *
FindCommonExclSCurrency (SplitList *splits,
gnc_commodity * ra, gnc_commodity * rb,
Split *excl_split)
{
GList *node;
if (!splits) return nullptr;
for (node = splits; node; node = node->next)
{
Split *s = GNC_SPLIT(node->data);
gnc_commodity * sa, * sb;
if (s == excl_split) continue;
g_return_val_if_fail (s->acc, nullptr);
sa = DxaccAccountGetCurrency (s->acc);
sb = xaccAccountGetCommodity (s->acc);
if (ra && rb)
{
int aa = !gnc_commodity_equiv(ra, sa);
int ab = !gnc_commodity_equiv(ra, sb);
int ba = !gnc_commodity_equiv(rb, sa);
int bb = !gnc_commodity_equiv(rb, sb);
if ( (!aa) && bb) rb = nullptr;
else if ( (!ab) && ba) rb = nullptr;
else if ( (!ba) && ab) ra = nullptr;
else if ( (!bb) && aa) ra = nullptr;
else if ( aa && bb && ab && ba )
{
ra = nullptr;
rb = nullptr;
}
if (!ra)
{
ra = rb;
rb = nullptr;
}
}
else if (ra && !rb)
{
int aa = !gnc_commodity_equiv(ra, sa);
int ab = !gnc_commodity_equiv(ra, sb);
if ( aa && ab ) ra = nullptr;
}
else if (!ra && rb)
{
int aa = !gnc_commodity_equiv(rb, sa);
int ab = !gnc_commodity_equiv(rb, sb);
ra = ( aa && ab ) ? nullptr : rb;
}
if ((!ra) && (!rb)) return nullptr;
}
return (ra);
}
/* This is the wrapper for those calls (i.e. the older ones) which
* don't exclude one split from the splitlist when looking for a
* common currency.
*/
static gnc_commodity *
FindCommonCurrency (GList *splits, gnc_commodity * ra, gnc_commodity * rb)
{
return FindCommonExclSCurrency(splits, ra, rb, nullptr);
}
static gnc_commodity *
xaccTransFindOldCommonCurrency (Transaction *trans, QofBook *book)
{
gnc_commodity *ra, *rb, *retval;
Split *split;
if (!trans) return nullptr;
if (trans->splits == nullptr) return nullptr;
g_return_val_if_fail (book, nullptr);
split = GNC_SPLIT(trans->splits->data);
if (!split || nullptr == split->acc) return nullptr;
ra = DxaccAccountGetCurrency (split->acc);
rb = xaccAccountGetCommodity (split->acc);
retval = FindCommonCurrency (trans->splits, ra, rb);
if (retval && !gnc_commodity_is_currency(retval))
retval = nullptr;
return retval;
}
/* Test the currency of the splits and find the most common and return
* it, or nullptr if there is no currency more common than the
* others -- or none at all.
*/
typedef struct
{
gnc_commodity *commodity;
unsigned int count;
} CommodityCount;
static gint
commodity_equal (gconstpointer a, gconstpointer b)
{
CommodityCount *cc = (CommodityCount*)a;
gnc_commodity *com = (gnc_commodity*)b;
if ( cc == nullptr || cc->commodity == nullptr ||
!GNC_IS_COMMODITY( cc->commodity ) ) return -1;
if ( com == nullptr || !GNC_IS_COMMODITY( com ) ) return 1;
if ( gnc_commodity_equal(cc->commodity, com) )
return 0;
return 1;
}
static gint
commodity_compare( gconstpointer a, gconstpointer b)
{
CommodityCount *ca = (CommodityCount*)a, *cb = (CommodityCount*)b;
if (ca == nullptr || ca->commodity == nullptr ||
!GNC_IS_COMMODITY( ca->commodity ) )
{
if (cb == nullptr || cb->commodity == nullptr ||
!GNC_IS_COMMODITY( cb->commodity ) )
return 0;
return -1;
}
if (cb == nullptr || cb->commodity == nullptr ||
!GNC_IS_COMMODITY( cb->commodity ) )
return 1;
if (ca->count == cb->count)
return 0;
return ca->count > cb->count ? 1 : -1;
}
/* Find the commodities in the account of each of the splits of a
* transaction, and rank them by how many splits in which they
* occur. Commodities which are currencies count more than those which
* aren't, because for simple buy and sell transactions it makes
* slightly more sense for the transaction commodity to be the
* currency -- to the extent that it makes sense for a transaction to
* have a currency at all. jralls, 2010-11-02 */
static gnc_commodity *
xaccTransFindCommonCurrency (Transaction *trans, QofBook *book)
{
gnc_commodity *com_scratch;
GList *node = nullptr;
GSList *comlist = nullptr, *found = nullptr;
if (!trans) return nullptr;
if (trans->splits == nullptr) return nullptr;
g_return_val_if_fail (book, nullptr);
/* Find the most commonly used currency among the splits. If a given split
is in a non-currency commodity, then look for an ancestor account in a
currency, but prefer currencies used directly in splits. Ignore trading
account splits in this whole process, they don't add any value to this algorithm. */
for (node = trans->splits; node; node = node->next)
{
Split *s = GNC_SPLIT(node->data);
unsigned int curr_weight;
if (s == nullptr || s->acc == nullptr) continue;
if (xaccAccountGetType(s->acc) == ACCT_TYPE_TRADING) continue;
com_scratch = xaccAccountGetCommodity(s->acc);
if (com_scratch && gnc_commodity_is_currency(com_scratch))
{
curr_weight = 3;
}
else
{
com_scratch = gnc_account_get_currency_or_parent(s->acc);
if (com_scratch == nullptr) continue;
curr_weight = 1;
}
if ( comlist )
{
found = g_slist_find_custom(comlist, com_scratch, commodity_equal);
}
if (comlist == nullptr || found == nullptr)
{
CommodityCount *count = g_slice_new0(CommodityCount);
count->commodity = com_scratch;
count->count = curr_weight;
comlist = g_slist_append(comlist, count);
}
else
{
CommodityCount *count = (CommodityCount*)(found->data);
count->count += curr_weight;
}
}
found = g_slist_sort( comlist, commodity_compare);
if ( found && found->data && (((CommodityCount*)(found->data))->commodity != nullptr))
{
return ((CommodityCount*)(found->data))->commodity;
}
/* We didn't find a currency in the current account structure, so try
* an old one. */
return xaccTransFindOldCommonCurrency( trans, book );
}
/* ================================================================ */
void
xaccTransScrubCurrency (Transaction *trans)
{
SplitList *node;
gnc_commodity *currency;
if (!trans) return;
/* If there are any orphaned splits in a transaction, then the
* this routine will fail. Therefore, we want to make sure that
* there are no orphans (splits without parent account).
*/
xaccTransScrubOrphans (trans);
currency = xaccTransGetCurrency (trans);
if (currency && gnc_commodity_is_currency(currency)) return;
currency = xaccTransFindCommonCurrency (trans, qof_instance_get_book(trans));
if (currency)
{
xaccTransBeginEdit (trans);
xaccTransSetCurrency (trans, currency);
xaccTransCommitEdit (trans);
}
else
{
if (nullptr == trans->splits)
{
PWARN ("Transaction \"%s\" has no splits in it!", trans->description);
}
else
{
SplitList *node;
char guid_str[GUID_ENCODING_LENGTH + 1];
guid_to_string_buff(xaccTransGetGUID(trans), guid_str);
PWARN ("no common transaction currency found for trans=\"%s\" (%s);",
trans->description, guid_str);
for (node = trans->splits; node; node = node->next)
{
Split *split = GNC_SPLIT(node->data);
if (nullptr == split->acc)
{
PWARN (" split=\"%s\" is not in any account!", split->memo);
}
else
{
gnc_commodity *currency = xaccAccountGetCommodity(split->acc);
PWARN ("setting to split=\"%s\" account=\"%s\" commodity=\"%s\"",
split->memo, xaccAccountGetName(split->acc),
gnc_commodity_get_mnemonic(currency));
xaccTransBeginEdit (trans);
xaccTransSetCurrency (trans, currency);
xaccTransCommitEdit (trans);
return;
}
}
}
return;
}
for (node = trans->splits; node; node = node->next)
{
Split *sp = GNC_SPLIT(node->data);
if (!gnc_numeric_equal(xaccSplitGetAmount (sp),
xaccSplitGetValue (sp)))
{
gnc_commodity *acc_currency;
acc_currency = sp->acc ? xaccAccountGetCommodity(sp->acc) : nullptr;
if (acc_currency == currency)
{
/* This Split needs fixing: The transaction-currency equals
* the account-currency/commodity, but the amount/values are
* inequal i.e. they still correspond to the security
* (amount) and the currency (value). In the new model, the
* value is the amount in the account-commodity -- so it
* needs to be set to equal the amount (since the
* account-currency doesn't exist anymore).
*
* Note: Nevertheless we lose some information here. Namely,
* the information that the 'amount' in 'account-old-security'
* was worth 'value' in 'account-old-currency'. Maybe it would
* be better to store that information in the price database?
* But then, for old currency transactions there is still the
* 'other' transaction, which is going to keep that
* information. So I don't bother with that here. -- cstim,
* 2002/11/20. */
PWARN ("Adjusted split with mismatched values, desc=\"%s\" memo=\"%s\""
" old amount %s %s, new amount %s",
trans->description, sp->memo,
gnc_num_dbg_to_string (xaccSplitGetAmount(sp)),
gnc_commodity_get_mnemonic (currency),
gnc_num_dbg_to_string (xaccSplitGetValue(sp)));
xaccTransBeginEdit (trans);
xaccSplitSetAmount (sp, xaccSplitGetValue(sp));
xaccTransCommitEdit (trans);
}
/*else
{
PINFO ("Ok: Split '%s' Amount %s %s, value %s %s",
xaccSplitGetMemo (sp),
gnc_num_dbg_to_string (amount),
gnc_commodity_get_mnemonic (currency),
gnc_num_dbg_to_string (value),
gnc_commodity_get_mnemonic (acc_currency));
}*/
}
}
}
/* ================================================================ */
void
xaccAccountScrubCommodity (Account *account)
{
gnc_commodity *commodity;
if (!account) return;
if (xaccAccountGetType(account) == ACCT_TYPE_ROOT) return;
commodity = xaccAccountGetCommodity (account);
if (commodity) return;
/* Use the 'obsolete' routines to try to figure out what the
* account commodity should have been. */
commodity = xaccAccountGetCommodity (account);
if (commodity)
{
xaccAccountSetCommodity (account, commodity);
return;
}
commodity = DxaccAccountGetCurrency (account);
if (commodity)
{
xaccAccountSetCommodity (account, commodity);
return;
}
PERR ("Account \"%s\" does not have a commodity!",
xaccAccountGetName(account));
}
/* ================================================================ */
/* EFFECTIVE FRIEND FUNCTION declared in qofinstance-p.h */
extern void qof_instance_set_dirty (QofInstance*);
static void
xaccAccountDeleteOldData (Account *account)
{
if (!account) return;
xaccAccountBeginEdit (account);
qof_instance_set_kvp (QOF_INSTANCE (account), nullptr, 1, "old-currency");
qof_instance_set_kvp (QOF_INSTANCE (account), nullptr, 1, "old-security");
qof_instance_set_kvp (QOF_INSTANCE (account), nullptr, 1, "old-currency-scu");
qof_instance_set_kvp (QOF_INSTANCE (account), nullptr, 1, "old-security-scu");
qof_instance_set_dirty (QOF_INSTANCE (account));
xaccAccountCommitEdit (account);
}
static int
scrub_trans_currency_helper (Transaction *t, gpointer data)
{
xaccTransScrubCurrency (t);
return 0;
}
static void
scrub_account_commodity_helper (Account *account, gpointer data)
{
scrub_depth++;
xaccAccountScrubCommodity (account);
xaccAccountDeleteOldData (account);
scrub_depth--;
}
void
xaccAccountTreeScrubCommodities (Account *acc)
{
if (!acc) return;
scrub_depth++;
xaccAccountTreeForEachTransaction (acc, scrub_trans_currency_helper, nullptr);
scrub_account_commodity_helper (acc, nullptr);
gnc_account_foreach_descendant (acc, scrub_account_commodity_helper, nullptr);
scrub_depth--;
}
/* ================================================================ */
static gboolean
check_quote_source (gnc_commodity *com, gpointer data)
{
gboolean *commodity_has_quote_src = (gboolean *)data;
if (com && !gnc_commodity_is_iso(com))
*commodity_has_quote_src |= gnc_commodity_get_quote_flag(com);
return TRUE;
}
static void
move_quote_source (Account *account, gpointer data)
{
gnc_commodity *com;
gnc_quote_source *quote_source;
gboolean new_style = GPOINTER_TO_INT(data);
const char *source, *tz;
com = xaccAccountGetCommodity(account);
if (!com)
return;
if (!new_style)
{
source = dxaccAccountGetPriceSrc(account);
if (!source || !*source)
return;
tz = dxaccAccountGetQuoteTZ(account);
PINFO("to %8s from %s", gnc_commodity_get_mnemonic(com),
xaccAccountGetName(account));
gnc_commodity_set_quote_flag(com, TRUE);
quote_source = gnc_quote_source_lookup_by_internal(source);
if (!quote_source)
quote_source = gnc_quote_source_add_new(source, FALSE);
gnc_commodity_set_quote_source(com, quote_source);
gnc_commodity_set_quote_tz(com, tz);
}
dxaccAccountSetPriceSrc(account, nullptr);
dxaccAccountSetQuoteTZ(account, nullptr);
return;
}
void
xaccAccountTreeScrubQuoteSources (Account *root, gnc_commodity_table *table)
{
gboolean new_style = FALSE;
ENTER(" ");
if (!root || !table)
{
LEAVE("Oops");
return;
}
scrub_depth++;
gnc_commodity_table_foreach_commodity (table, check_quote_source, &new_style);
move_quote_source(root, GINT_TO_POINTER(new_style));
gnc_account_foreach_descendant (root, move_quote_source,
GINT_TO_POINTER(new_style));
LEAVE("Migration done");
scrub_depth--;
}
/* ================================================================ */
void
xaccAccountScrubKvp (Account *account)
{
GValue v = G_VALUE_INIT;
gchar *str2;
if (!account) return;
scrub_depth++;
qof_instance_get_kvp (QOF_INSTANCE (account), &v, 1, "notes");
if (G_VALUE_HOLDS_STRING (&v))
{
str2 = g_strstrip(g_value_dup_string(&v));
if (strlen(str2) == 0)
qof_instance_slot_delete (QOF_INSTANCE (account), "notes");
g_free(str2);
}
qof_instance_get_kvp (QOF_INSTANCE (account), &v, 1, "placeholder");
if ((G_VALUE_HOLDS_STRING (&v) &&
strcmp(g_value_get_string (&v), "false") == 0) ||
(G_VALUE_HOLDS_BOOLEAN (&v) && ! g_value_get_boolean (&v)))
qof_instance_slot_delete (QOF_INSTANCE (account), "placeholder");
g_value_unset (&v);
qof_instance_slot_delete_if_empty (QOF_INSTANCE (account), "hbci");
scrub_depth--;
}
/* ================================================================ */
void
xaccAccountScrubColorNotSet (QofBook *book)
{
GValue value_s = G_VALUE_INIT;
gboolean already_scrubbed;
// get the run-once value
qof_instance_get_kvp (QOF_INSTANCE (book), &value_s, 1, "remove-color-not-set-slots");
already_scrubbed = (G_VALUE_HOLDS_STRING (&value_s) &&
!g_strcmp0 (g_value_get_string (&value_s), "true"));
g_value_unset (&value_s);
if (already_scrubbed)
return;
else
{
GValue value_b = G_VALUE_INIT;
Account *root = gnc_book_get_root_account (book);
GList *accts = gnc_account_get_descendants_sorted (root);
GList *ptr;
for (ptr = accts; ptr; ptr = g_list_next (ptr))
{
auto acct = GNC_ACCOUNT(ptr->data);
auto color = xaccAccountGetColor (acct);
if (g_strcmp0 (color, "Not Set") == 0)
xaccAccountSetColor (acct, "");
}
g_list_free (accts);
g_value_init (&value_b, G_TYPE_BOOLEAN);
g_value_set_boolean (&value_b, TRUE);
// set the run-once value
qof_instance_set_kvp (QOF_INSTANCE (book), &value_b, 1, "remove-color-not-set-slots");
g_value_unset (&value_b);
}
}
/* ================================================================ */
static Account*
construct_account (Account *root, gnc_commodity *currency, const char *accname,
GNCAccountType acctype, gboolean placeholder)
{
gnc_commodity* root_currency = find_root_currency ();
Account *acc = xaccMallocAccount(gnc_account_get_book (root));
xaccAccountBeginEdit (acc);
if (accname && *accname)
xaccAccountSetName (acc, accname);
if (currency || root_currency)
xaccAccountSetCommodity (acc, currency ? currency : root_currency);
xaccAccountSetType (acc, acctype);
xaccAccountSetPlaceholder (acc, placeholder);
/* Hang the account off the root. */
gnc_account_append_child (root, acc);
xaccAccountCommitEdit (acc);
return acc;
}
static Account*
find_root_currency_account_in_list (GList *acc_list)
{
gnc_commodity* root_currency = find_root_currency();
for (GList *node = acc_list; node; node = g_list_next (node))
{
Account *acc = GNC_ACCOUNT (node->data);
gnc_commodity *acc_commodity = nullptr;
if (G_UNLIKELY (!acc)) continue;
acc_commodity = xaccAccountGetCommodity(acc);
if (gnc_commodity_equiv (acc_commodity, root_currency))
return acc;
}
return nullptr;
}
static Account*
find_account_matching_name_in_list (GList *acc_list, const char* accname)
{
for (GList* node = acc_list; node; node = g_list_next(node))
{
Account *acc = GNC_ACCOUNT (node->data);
if (G_UNLIKELY (!acc)) continue;
if (g_strcmp0 (accname, xaccAccountGetName (acc)) == 0)
return acc;
}
return nullptr;
}
Account *
xaccScrubUtilityGetOrMakeAccount (Account *root, gnc_commodity * currency,
const char *accname, GNCAccountType acctype,
gboolean placeholder, gboolean checkname)
{
GList* acc_list;
Account *acc = nullptr;
g_return_val_if_fail (root, nullptr);
acc_list =
gnc_account_lookup_by_type_and_commodity (root,
checkname ? accname : nullptr,
acctype, currency);
if (!acc_list)
return construct_account (root, currency, accname,
acctype, placeholder);
if (g_list_next(acc_list))
{
if (!currency)
acc = find_root_currency_account_in_list (acc_list);
if (!acc)
acc = find_account_matching_name_in_list (acc_list, accname);
}
if (!acc)
acc = GNC_ACCOUNT (acc_list->data);
g_list_free (acc_list);
return acc;
}
void
xaccTransScrubPostedDate (Transaction *trans)
{
time64 orig = xaccTransGetDate(trans);
if(orig == INT64_MAX)
{
GDate date = xaccTransGetDatePostedGDate(trans);
time64 time = gdate_to_time64(date);
if(time != INT64_MAX)
{
// xaccTransSetDatePostedSecs handles committing the change.
xaccTransSetDatePostedSecs(trans, time);
}
}
}
/* ==================== END OF FILE ==================== */