Added the concept of "balance setting transactions".

This commit is contained in:
John Wiegley 2008-07-27 18:37:55 -04:00
parent e5a8bbf997
commit c93175183e
5 changed files with 209 additions and 6 deletions

116
NEWS
View file

@ -2,7 +2,121 @@
* 2.6.1
- This version has no new features, it's all bug fixes.
- Added the concept of "balance setting transactions":
# Setting an account's balance
You can now manually set an account's balance to whatever you want, at
any time. Here's how it might look at the beginning of your Ledger
file:
2008/07/27 Starting fresh
Assets:Checking = $1,000.00
Equity:Opening Balances
If Assets:Checking is empty, this is no different from omitting the
"=". However, if Assets:Checking did have a prior balance, the amount
of the transaction will be auto-calculated so that the final balance
of Assets:Checking is now $1,000.00.
Let me give an example of this. Say you have this:
2008/07/27 Starting fresh
Assets:Checking $750.00
Equity:Opening Balances
2008/07/27 Starting fresh
Assets:Checking = $1,000.00
Equity:Adjustments
These two entries are exactly equivalent to these two:
2008/07/27 Starting fresh
Assets:Checking $750.00
Equity:Opening Balances
2008/07/27 Starting fresh
Assets:Checking $250.00
Equity:Adjustments
The use of the "=" sign here is that it sets the transaction's amount
to whatever is required to satisfy the assignment. This is the
behavior if the transaction's amount is left empty.
# Multiple commodities
As far as commodities go, the = sign only works if the account
balance's commodity matches the commodity of the amount after the
equals sign. However, if the account has multiple commodities, only
the matching commodity is affected. Here's what I mean:
2008/07/24 Opening Balance
Assets:Checking = $250.00 ; we force set it
Equity:Opening Balances
2008/07/24 Opening Balance
Assets:Checking = EC 250.00 ; we force set it again
Equity:Opening Balances
This is an error, because $250.00 cannot be auto-balanced to match EC
250.00. However:
2008/07/24 Opening Balance
Assets:Checking = $250.00 ; we force set it again
Assets:Checking EC 100.00 ; and add some EC's
Equity:Opening Balances
2008/07/24 Opening Balance
Assets:Checking = EC 250.00 ; we force set the EC's
Equity:Opening Balances
This is *not* an error, because the latter auto-balancing transaction
only affects the EC 100.00 part of the account's balance; the $250.00
part is left alone.
# Checking statement balances
When you reconcile a statement, there are typically one or more
transactions which result in a known balance. Here's how you specify
that in your Ledger data:
2008/07/24 Opening Balance
Assets:Checking = $100.00
Equity:Opening Balances
2008/07/30 We spend money, with a known balance afterward
Expenses:Food $20.00
Assets:Checking = $80.00
2008/07/30 Again we spend money, but this time with all the info
Expenses:Food $20.00
Assets:Checking $-20.00 = $60.00
2008/07/30 This entry yield an 'unbalanced' error
Expenses:Food $20.00
Assets:Checking $-20.00 = $30.00
The last entry in this set fails to balance with an unbalanced
remainder of $-10.00. Either the entry must be corrected, or you can
have Ledger deal with the remainder automatically:
2008/07/30 The fixed entry
Expenses:Food $20.00
Assets:Checking $-20.00 = $30.00
Equity:Adjustments
# Conclusion
This simple feature has all the utility of @check, plus auto-balancing
to match known target balances, plus the ability to guarantee that an
account which uses only one commodity does contain only that
commodity.
This feature slows down textual parsing slightly, does not affect
speed when loading from the binary cache.
- The rest of the changes in the version is all bug fixes (around 45 of
them).
* 2.6.0.90

View file

@ -182,6 +182,10 @@ unsigned int parse_ledger_data(config_t& config,
journal, acct);
if (! journal->price_db.empty())
journal->sources.push_back(journal->price_db);
// Clear out what was set during the textual parsing phase
clear_account_xdata acct_cleaner;
walk_accounts(*journal->master, acct_cleaner);
}
}

View file

@ -201,13 +201,15 @@ transaction_t * parse_transaction(char * line, account_t * account,
goto finished;
if (p == ';')
goto parse_note;
if (p == '=' && entry)
goto parse_assign;
try {
unsigned long beg = (long)in.tellg();
xact->amount_expr =
parse_amount_expr(in, xact->amount, xact.get(),
PARSE_VALEXPR_NO_REDUCE);
PARSE_VALEXPR_NO_REDUCE | PARSE_VALEXPR_NO_ASSIGN);
unsigned long end = (long)in.tellg();
xact->amount_expr.expr = std::string(line, beg, end - beg);
@ -241,7 +243,8 @@ transaction_t * parse_transaction(char * line, account_t * account,
unsigned long beg = (long)in.tellg();
if (parse_amount_expr(in, *xact->cost, xact.get(),
PARSE_VALEXPR_NO_MIGRATE))
PARSE_VALEXPR_NO_MIGRATE |
PARSE_VALEXPR_NO_ASSIGN))
throw new parse_error
("A transaction's cost must evaluate to a constant value");
@ -288,6 +291,80 @@ transaction_t * parse_transaction(char * line, account_t * account,
DEBUG_PRINT("ledger.textual.parse", "line " << linenum << ": " <<
"Reduced amount is " << xact->amount);
parse_assign:
if (entry != NULL) {
// Add this amount to the related account now
account_xdata_t& xdata(account_xdata(*xact->account));
if (xact->amount) {
xdata.value += xact->amount;
DEBUG_PRINT("ledger.textual.parse", "line " << linenum << ": " <<
"XACT assign: account total = " << xdata.value);
}
// Parse the optional assigned (= AMOUNT)
if (in.good() && ! in.eof()) {
p = peek_next_nonws(in);
if (p == '=') {
in.get(p);
DEBUG_PRINT("ledger.textual.parse", "line " << linenum << ": " <<
"Found a balance assignment indicator");
if (in.good() && ! in.eof()) {
amount_t amt;
try {
unsigned long beg = (long)in.tellg();
if (parse_amount_expr(in, amt, xact.get(),
PARSE_VALEXPR_NO_MIGRATE))
throw new parse_error
("An assigned balance must evaluate to a constant value");
DEBUG_PRINT("ledger.textual.parse", "line " << linenum << ": " <<
"XACT assign: parsed amt = " << amt);
unsigned long end = (long)in.tellg();
amount_t diff;
if (xdata.value.type == value_t::AMOUNT)
diff = amt - *((amount_t *) xdata.value.data);
else if (xdata.value.type == value_t::BALANCE)
diff = amt - ((balance_t *) xdata.value.data)->amount(amt.commodity());
else if (xdata.value.type == value_t::BALANCE_PAIR)
diff = amt - ((balance_pair_t *) xdata.value.data)->quantity.amount(amt.commodity());
else
diff = amt;
DEBUG_PRINT("ledger.textual.parse", "line " << linenum << ": " <<
"XACT assign: diff = " << diff);
if (! diff.realzero()) {
if (xact->amount) {
transaction_t * temp
= new transaction_t(xact->account, diff, TRANSACTION_CALCULATED);
entry->add_transaction(temp);
DEBUG_PRINT("ledger.textual.parse", "line " << linenum << ": " <<
"Created balancing transaction");
} else {
xact->amount = diff;
DEBUG_PRINT("ledger.textual.parse", "line " << linenum << ": " <<
"Overwrite null transaction");
}
xdata.value = amt;
}
}
catch (error * err) {
err_desc = "While parsing assigned balance:";
throw err;
}
}
}
}
}
// Parse the optional note
parse_note:

View file

@ -833,7 +833,8 @@ value_expr_t * parse_value_term(std::istream& in, scope_t * scope,
bool definition = false;
if (c == '=') {
in.get(c);
if (peek_next_nonws(in) == '=') {
if ((flags & PARSE_VALEXPR_NO_ASSIGN) ||
peek_next_nonws(in) == '=') {
in.unget();
c = '\0';
} else {
@ -1160,10 +1161,16 @@ value_expr_t * parse_logic_expr(std::istream& in, scope_t * scope,
case '!':
case '=': {
bool negate = c == '!';
if ((c = peek_next_nonws(in)) == '=')
if (! negate && (flags & PARSE_VALEXPR_NO_ASSIGN)) {
in.unget();
break;
}
else if ((c = peek_next_nonws(in)) == '=') {
in.get(c);
else
}
else {
unexpected(c, '=');
}
value_expr prev(node.release());
node.reset(new value_expr_t(negate ? value_expr_t::O_NEQ :
value_expr_t::O_EQ));

View file

@ -280,6 +280,7 @@ bool compute_amount(value_expr_t * expr, amount_t& amt,
#define PARSE_VALEXPR_RELAXED 0x02
#define PARSE_VALEXPR_NO_MIGRATE 0x04
#define PARSE_VALEXPR_NO_REDUCE 0x08
#define PARSE_VALEXPR_NO_ASSIGN 0x10
value_expr_t * parse_value_expr(std::istream& in,
scope_t * scope = NULL,