/* * Copyright (c) 2003-2009, John Wiegley. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * - Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * - Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * - Neither the name of New Artisans LLC nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include "derive.h" #include "xact.h" #include "post.h" #include "account.h" #include "journal.h" #include "session.h" #include "report.h" #include "output.h" namespace ledger { namespace { struct xact_template_t { optional date; optional code; optional note; mask_t payee_mask; struct post_template_t { bool from; optional account_mask; optional amount; optional cost_operator; optional cost; post_template_t() : from(false) {} }; std::list posts; xact_template_t() {} void dump(std::ostream& out) const { if (date) out << _("Date: ") << *date << std::endl; else out << _("Date: ") << std::endl; if (code) out << _("Code: ") << *code << std::endl; if (note) out << _("Note: ") << *note << std::endl; if (payee_mask.empty()) out << _("Payee mask: INVALID (template expression will cause an error)") << std::endl; else out << _("Payee mask: ") << payee_mask << std::endl; if (posts.empty()) { out << std::endl << _("") << std::endl; } else { bool has_only_from = true; bool has_only_to = true; foreach (const post_template_t& post, posts) { if (post.from) has_only_to = false; else has_only_from = false; } foreach (const post_template_t& post, posts) { straccstream accum; out << std::endl << ACCUM(accum << _("[Posting \"%1\"]") << (post.from ? _("from") : _("to"))) << std::endl; if (post.account_mask) out << _(" Account mask: ") << *post.account_mask << std::endl; else if (post.from) out << _(" Account mask: ") << std::endl; else out << _(" Account mask: ") << std::endl; if (post.amount) out << _(" Amount: ") << *post.amount << std::endl; if (post.cost) out << _(" Cost: ") << *post.cost_operator << " " << *post.cost << std::endl; } } } }; xact_template_t args_to_xact_template(value_t::sequence_t::const_iterator begin, value_t::sequence_t::const_iterator end) { regex date_mask(_("([0-9]+(?:[-/.][0-9]+)?(?:[-/.][0-9]+))?")); smatch what; xact_template_t tmpl; bool check_for_date = true; optional weekday; xact_template_t::post_template_t * post = NULL; for (; begin != end; begin++) { if (check_for_date && regex_match((*begin).to_string(), what, date_mask)) { tmpl.date = parse_date(what[0]); check_for_date = false; } else if (check_for_date && bool(weekday = string_to_day_of_week(what[0]))) { short dow = static_cast(*weekday); date_t date = CURRENT_DATE() - date_duration(1); while (date.day_of_week() != dow) date -= date_duration(1); tmpl.date = date; check_for_date = false; } else { string arg = (*begin).to_string(); if (arg == "at") { if (begin == end) throw std::runtime_error(_("Invalid xact command arguments")); tmpl.payee_mask = (*++begin).to_string(); } else if (arg == "to" || arg == "from") { if (! post || post->account_mask) { tmpl.posts.push_back(xact_template_t::post_template_t()); post = &tmpl.posts.back(); } if (begin == end) throw std::runtime_error(_("Invalid xact command arguments")); post->account_mask = mask_t((*++begin).to_string()); post->from = arg == "from"; } else if (arg == "on") { if (begin == end) throw std::runtime_error(_("Invalid xact command arguments")); tmpl.date = parse_date((*++begin).to_string()); check_for_date = false; } else if (arg == "code") { if (begin == end) throw std::runtime_error(_("Invalid xact command arguments")); tmpl.code = (*++begin).to_string(); } else if (arg == "note") { if (begin == end) throw std::runtime_error(_("Invalid xact command arguments")); tmpl.note = (*++begin).to_string(); } else if (arg == "rest") { ; // just ignore this argument } else if (arg == "@" || arg == "@@") { amount_t cost; post->cost_operator = arg; if (begin == end) throw std::runtime_error(_("Invalid xact command arguments")); arg = (*++begin).to_string(); if (! cost.parse(arg, amount_t::PARSE_SOFT_FAIL | amount_t::PARSE_NO_MIGRATE)) throw std::runtime_error(_("Invalid xact command arguments")); post->cost = cost; } else { // Without a preposition, it is either: // // A payee, if we have not seen one // An account or an amount, if we have // An account if an amount has just been seen // An amount if an account has just been seen if (tmpl.payee_mask.empty()) { tmpl.payee_mask = arg; } else { amount_t amt; optional account; if (! amt.parse(arg, amount_t::PARSE_SOFT_FAIL | amount_t::PARSE_NO_MIGRATE)) account = mask_t(arg); if (! post || (account && post->account_mask) || (! account && post->amount)) { tmpl.posts.push_back(xact_template_t::post_template_t()); post = &tmpl.posts.back(); } if (account) { post->from = false; post->account_mask = account; } else { post->amount = amt; } } } } } if (! tmpl.posts.empty()) { bool has_only_from = true; bool has_only_to = true; // A single account at the end of the line is the "from" account if (tmpl.posts.size() > 1 && tmpl.posts.back().account_mask && ! tmpl.posts.back().amount) tmpl.posts.back().from = true; foreach (xact_template_t::post_template_t& post, tmpl.posts) { if (post.from) has_only_to = false; else has_only_from = false; } if (has_only_from) { tmpl.posts.push_front(xact_template_t::post_template_t()); } else if (has_only_to) { tmpl.posts.push_back(xact_template_t::post_template_t()); tmpl.posts.back().from = true; } } return tmpl; } xact_t * derive_xact_from_template(xact_template_t& tmpl, report_t& report) { if (tmpl.payee_mask.empty()) throw std::runtime_error(_("xact' command requires at least a payee")); xact_t * matching = NULL; journal_t& journal(*report.session.journal.get()); std::auto_ptr added(new xact_t); for (xacts_list::reverse_iterator j = journal.xacts.rbegin(); j != journal.xacts.rend(); j++) { if (tmpl.payee_mask.match((*j)->payee)) { matching = *j; DEBUG("derive.xact", "Found payee match: transaction on line " << (*j)->pos->beg_line); break; } } if (! tmpl.date) { added->_date = CURRENT_DATE(); DEBUG("derive.xact", "Setting date to current date"); } else { added->_date = tmpl.date; DEBUG("derive.xact", "Setting date to template date: " << *tmpl.date); } added->set_state(item_t::UNCLEARED); if (matching) { added->payee = matching->payee; added->code = matching->code; added->note = matching->note; #if defined(DEBUG_ON) DEBUG("derive.xact", "Setting payee from match: " << added->payee); if (added->code) DEBUG("derive.xact", "Setting code from match: " << *added->code); if (added->note) DEBUG("derive.xact", "Setting note from match: " << *added->note); #endif } else { added->payee = tmpl.payee_mask.expr.str(); DEBUG("derive.xact", "Setting payee from template: " << added->payee); } if (tmpl.code) { added->code = tmpl.code; DEBUG("derive.xact", "Now setting code from template: " << *added->code); } if (tmpl.note) { added->note = tmpl.note; DEBUG("derive.xact", "Now setting note from template: " << *added->note); } if (tmpl.posts.empty()) { if (matching) { DEBUG("derive.xact", "Template had no postings, copying from match"); foreach (post_t * post, matching->posts) { added->add_post(new post_t(*post)); added->posts.back()->set_state(item_t::UNCLEARED); } } else { throw_(std::runtime_error, _("No accounts, and no past transaction matching '%1'") << tmpl.payee_mask); } } else { DEBUG("derive.xact", "Template had postings"); bool any_post_has_amount = false; foreach (xact_template_t::post_template_t& post, tmpl.posts) { if (post.amount) { DEBUG("derive.xact", " and at least one has an amount specified"); any_post_has_amount = true; break; } } foreach (xact_template_t::post_template_t& post, tmpl.posts) { std::auto_ptr new_post; commodity_t * found_commodity = NULL; if (matching) { if (post.account_mask) { DEBUG("derive.xact", "Looking for matching posting based on account mask"); foreach (post_t * x, matching->posts) { if (post.account_mask->match(x->account->fullname())) { new_post.reset(new post_t(*x)); DEBUG("derive.xact", "Founding posting from line " << x->pos->beg_line); break; } } } else { if (post.from) { for (posts_list::reverse_iterator j = matching->posts.rbegin(); j != matching->posts.rend(); j++) { if ((*j)->must_balance()) { new_post.reset(new post_t(**j)); DEBUG("derive.xact", "Copied last real posting from matching"); break; } } } else { for (posts_list::iterator j = matching->posts.begin(); j != matching->posts.end(); j++) { if ((*j)->must_balance()) { new_post.reset(new post_t(**j)); DEBUG("derive.xact", "Copied first real posting from matching"); break; } } } } } if (! new_post.get()) { new_post.reset(new post_t); DEBUG("derive.xact", "New posting was NULL, creating a blank one"); } if (! new_post->account) { DEBUG("derive.xact", "New posting still needs an account"); if (post.account_mask) { DEBUG("derive.xact", "The template has an account mask"); account_t * acct = NULL; if (! acct) { acct = journal.find_account_re(post.account_mask->expr.str()); #if defined(DEBUG_ON) if (acct) DEBUG("derive.xact", "Found account as a regular expression"); #endif } if (! acct) { acct = journal.find_account(post.account_mask->expr.str()); #if defined(DEBUG_ON) if (acct) DEBUG("derive.xact", "Found (or created) account by name"); #endif } // Find out the default commodity to use by looking at the last // commodity used in that account for (xacts_list::reverse_iterator j = journal.xacts.rbegin(); j != journal.xacts.rend(); j++) { foreach (post_t * x, (*j)->posts) { if (x->account == acct && ! x->amount.is_null()) { new_post.reset(new post_t(*x)); DEBUG("derive.xact", "Found account in journal postings, setting new posting"); break; } } } new_post->account = acct; DEBUG("derive.xact", "Set new posting's account to: " << acct->fullname()); } else { if (post.from) { new_post->account = journal.find_account(_("Liabilities:Unknown")); DEBUG("derive.xact", "Set new posting's account to: Liabilities:Unknown"); } else { new_post->account = journal.find_account(_("Expenses:Unknown")); DEBUG("derive.xact", "Set new posting's account to: Expenses:Unknown"); } } } if (new_post.get() && ! new_post->amount.is_null()) { found_commodity = &new_post->amount.commodity(); if (any_post_has_amount) { new_post->amount = amount_t(); DEBUG("derive.xact", "New posting has an amount, but we cleared it"); } else { any_post_has_amount = true; DEBUG("derive.xact", "New posting has an amount, and we're using it"); } } if (post.amount) { new_post->amount = *post.amount; DEBUG("derive.xact", "Copied over posting amount"); if (post.from) { new_post->amount.in_place_negate(); DEBUG("derive.xact", "Negated new posting amount"); } } if (post.cost) { if (post.cost->sign() < 0) throw parse_error(_("A posting's cost may not be negative")); post.cost->in_place_unround(); if (*post.cost_operator == "@") { // For the sole case where the cost might be uncommoditized, // guarantee that the commodity of the cost after multiplication // is the same as it was before. commodity_t& cost_commodity(post.cost->commodity()); *post.cost *= new_post->amount; post.cost->set_commodity(cost_commodity); } new_post->cost = *post.cost; DEBUG("derive.xact", "Copied over posting cost"); } if (found_commodity && ! new_post->amount.is_null() && ! new_post->amount.has_commodity()) { new_post->amount.set_commodity(*found_commodity); DEBUG("derive.xact", "Set posting amount commodity to: " << new_post->amount.commodity()); new_post->amount = new_post->amount.rounded(); DEBUG("derive.xact", "Rounded posting amount to: " << new_post->amount); } added->add_post(new_post.release()); added->posts.back()->set_state(item_t::UNCLEARED); DEBUG("derive.xact", "Added new posting to derived entry"); } } if (! journal.xact_finalize_hooks.run_hooks(*added.get(), false) || ! added->finalize() || ! journal.xact_finalize_hooks.run_hooks(*added.get(), true)) { throw_(std::runtime_error, _("Failed to finalize derived transaction (check commodities)")); } return added.release(); } } value_t template_command(call_scope_t& args) { report_t& report(find_scope(args)); std::ostream& out(report.output_stream); value_t::sequence_t::const_iterator begin = args.value().begin(); value_t::sequence_t::const_iterator end = args.value().end(); out << _("--- Input arguments ---") << std::endl; args.value().dump(out); out << std::endl << std::endl; xact_template_t tmpl = args_to_xact_template(begin, end); out << _("--- Transaction template ---") << std::endl; tmpl.dump(out); return true; } value_t xact_command(call_scope_t& args) { value_t::sequence_t::const_iterator begin = args.value().begin(); value_t::sequence_t::const_iterator end = args.value().end(); report_t& report(find_scope(args)); xact_template_t tmpl = args_to_xact_template(begin, end); std::auto_ptr new_xact(derive_xact_from_template(tmpl, report)); // Only consider actual postings for the "xact" command report.HANDLER(limit_).on(string("#xact"), "actual"); report.xact_report(post_handler_ptr (new format_posts(report, report.HANDLER(print_format_).str())), *new_xact.get()); return true; } } // namespace ledger