From 36f87f49d86e931bb99a226cd47721219ccd6301 Mon Sep 17 00:00:00 2001 From: Alexis Hildebrandt Date: Mon, 6 Sep 2010 15:01:09 +0200 Subject: [PATCH 01/20] Add --time-colon option The --time-colon option will display the value for a seconds based commodity as real hours and minutes. For example 8100 seconds by default will be displayed as 2.25 whereas with the --time-colon option they will be displayed as 2:15. --- src/amount.cc | 23 +++++++++++++++++++++-- src/commodity.cc | 1 + src/commodity.h | 2 ++ src/session.cc | 4 +++- src/session.h | 5 +++++ 5 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/amount.cc b/src/amount.cc index 4e658212..671215c2 100644 --- a/src/amount.cc +++ b/src/amount.cc @@ -195,7 +195,10 @@ namespace { for (const char * p = buf; *p; p++) { if (*p == '.') { - if (commodity_t::decimal_comma_by_default || + if (commodity_t::time_colon_by_default || + (comm && comm->has_flags(COMMODITY_STYLE_TIME_COLON))) + out << ':'; + else if (commodity_t::decimal_comma_by_default || (comm && comm->has_flags(COMMODITY_STYLE_DECIMAL_COMMA))) out << ','; else @@ -209,7 +212,10 @@ namespace { out << *p; if (integer_digits > 3 && --integer_digits % 3 == 0) { - if (commodity_t::decimal_comma_by_default || + if (commodity_t::time_colon_by_default || + (comm && comm->has_flags(COMMODITY_STYLE_TIME_COLON))) + out << ':'; + else if (commodity_t::decimal_comma_by_default || (comm && comm->has_flags(COMMODITY_STYLE_DECIMAL_COMMA))) out << '.'; else @@ -737,6 +743,16 @@ void amount_t::in_place_unreduce() } if (shifted) { + if ("h" == comm->symbol() && commodity_t::time_colon_by_default) { + amount_t floored = tmp.floored(); + amount_t precision = tmp - floored; + if (precision < 0.0) { + precision += 1.0; + floored -= 1.0; + } + tmp = floored + (precision * (comm->smaller()->number() / 100.0)); + } + *this = tmp; commodity_ = comm; } @@ -1090,6 +1106,9 @@ bool amount_t::parse(std::istream& in, const parse_flags_t& flags) bool decimal_comma_style = (commodity_t::decimal_comma_by_default || commodity().has_flags(COMMODITY_STYLE_DECIMAL_COMMA)); + bool time_colon_style + = (commodity_t::time_colon_by_default || + commodity().has_flags(COMMODITY_STYLE_TIME_COLON)); new_quantity->prec = 0; diff --git a/src/commodity.cc b/src/commodity.cc index 05d465ca..ffeac10d 100644 --- a/src/commodity.cc +++ b/src/commodity.cc @@ -40,6 +40,7 @@ namespace ledger { bool commodity_t::decimal_comma_by_default = false; +bool commodity_t::time_colon_by_default = false; void commodity_t::add_price(const datetime_t& date, const amount_t& price, const bool reflexive) diff --git a/src/commodity.h b/src/commodity.h index ab496850..1d69b689 100644 --- a/src/commodity.h +++ b/src/commodity.h @@ -107,6 +107,7 @@ protected: #define COMMODITY_SAW_ANNOTATED 0x200 #define COMMODITY_SAW_ANN_PRICE_FLOAT 0x400 #define COMMODITY_SAW_ANN_PRICE_FIXATED 0x800 +#define COMMODITY_STYLE_TIME_COLON 0x1000 string symbol; optional graph_index; @@ -176,6 +177,7 @@ protected: public: static bool decimal_comma_by_default; + static bool time_colon_by_default; virtual ~commodity_t() { TRACE_DTOR(commodity_t); diff --git a/src/session.cc b/src/session.cc index b6153203..7072fb09 100644 --- a/src/session.cc +++ b/src/session.cc @@ -348,9 +348,11 @@ option_t * session_t::lookup_option(const char * p) case 's': OPT(strict); break; + case 't': + OPT(time_colon); + break; case 'v': OPT(value_expr_); - break; } return NULL; } diff --git a/src/session.h b/src/session.h index a0aba91b..74aeab5f 100644 --- a/src/session.h +++ b/src/session.h @@ -100,6 +100,7 @@ public: HANDLER(day_break).report(out); HANDLER(download).report(out); HANDLER(decimal_comma).report(out); + HANDLER(time_colon).report(out); HANDLER(file_).report(out); HANDLER(input_date_format_).report(out); HANDLER(explicit).report(out); @@ -130,6 +131,10 @@ public: commodity_t::decimal_comma_by_default = true; }); + OPTION_(session_t, time_colon, DO() { + commodity_t::time_colon_by_default = true; + }); + OPTION__ (session_t, price_exp_, // -Z CTOR(session_t, price_exp_) { value = "24"; }); From 269d0fdd5efe2dc9de148f845f97ad95ddc1b8e9 Mon Sep 17 00:00:00 2001 From: "Bradley M. Kuhn" Date: Thu, 3 Jan 2013 10:39:58 -0500 Subject: [PATCH 02/20] Created Trial balance report for summary reports. --- .../summary-reports.plx | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/contrib/non-profit-audit-reports/summary-reports.plx b/contrib/non-profit-audit-reports/summary-reports.plx index 7d2267d6..ce6d56da 100755 --- a/contrib/non-profit-audit-reports/summary-reports.plx +++ b/contrib/non-profit-audit-reports/summary-reports.plx @@ -3,7 +3,7 @@ # # Script to generate end-of-year summary reports. # -# Copyright (C) 2011, 2012, Bradley M. Kuhn +# Copyright (C) 2011, 2012, 2013, Bradley M. Kuhn # # This program gives you software freedom; you can copy, modify, convey, # and/or redistribute it under the terms of the GNU General Public License @@ -348,6 +348,35 @@ die "calculated total of $overallTotal does equal $firstTotal" print STDERR "\n"; +open(TRIAL, ">", "trial-balance.txt") or die "unable to open accrued.txt for writing: $!"; + +@fullCommand = ($LEDGER_BIN, @mainLedgerOptions, '-V', '-X', '$', + '-e', $endDate, + '-F', '%-.80A %22.108t\n', '-s', + 'reg'); + +print TRIAL " TRIAL BALANCE \n", + " Ending $formattedEndDate\n\n"; + +open(FILE, "-|", @fullCommand) + or die "unable to run command ledger command: @fullCommand: $!"; + +print STDERR ($VERBOSE ? "Running: @fullCommand\n" : "."); + +my $accruedTotal = $ZERO; + +foreach my $line () { + die "Unable to parse output line from second funds command: $line" + unless $line =~ /^\s*([^\$]+)\s+\$\s*([\-\d\.\,]+)/; + my($account, $amount) = ($1, $2); + $amount = ParseNumber($amount); + $account =~ s/\s+$//; + next if $account =~ /\|^Equity:/ and (abs($amount) <= 0.02); + print TRIAL $line; + + $accruedTotal += $amount; +} + ############################################################################### # # Local variables: From bfdf20b31cbb60b5d92c82ecd3a5192059a0c7b1 Mon Sep 17 00:00:00 2001 From: "Bradley M. Kuhn" Date: Thu, 3 Jan 2013 11:44:55 -0500 Subject: [PATCH 03/20] Updated sorting function based on advice of auditing accountants. Our auditing accounts tell us they want accounts sorted by: Assets Liabilities Net Assets Income Expenses in a general ledger report. Generally, I think we should just apply the same sorting. Since Ledger doesn't use account codes by default, this is my hack to solve this problem for now. Maybe there should be an account code tag for sorting purposes at least? --- .../general-ledger-report.plx | 46 +++++++++++++------ .../summary-reports.plx | 30 ++++++++++++ 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/contrib/non-profit-audit-reports/general-ledger-report.plx b/contrib/non-profit-audit-reports/general-ledger-report.plx index d1c92975..66fc0031 100755 --- a/contrib/non-profit-audit-reports/general-ledger-report.plx +++ b/contrib/non-profit-audit-reports/general-ledger-report.plx @@ -4,8 +4,8 @@ # Script to generate a General Ledger report that accountants like # using Ledger. # -# Copyright (C) 2011, 2012 Bradley M. Kuhn -# Copyright (C) 2012 Tom Marble +# Copyright (C) 2011, 2012, 2013 Bradley M. Kuhn +# Copyright (C) 2012 Tom Marble # # This program gives you software freedom; you can copy, modify, convey, # and/or redistribute it under the terms of the GNU General Public License @@ -67,18 +67,38 @@ close(CHART_DATA); die "error reading ledger output for chart of accounts: $!" u open(CHART_OUTPUT, ">", "chart-of-accounts.txt") or die "unable to write chart-of-accounts.txt: $!"; print MANIFEST "chart-of-accounts.txt\n"; +sub preferredAccountSorting ($$) { + if ($_[0] =~ /^Assets/) { + return -1; + } elsif ($_[1] =~ /^Assets/) { + return 1; + } elsif ($_[0] =~ /^Liabilities/ and $_[1] !~ /^Assets/) { + return -1; + } elsif ($_[1] =~ /^Liabilities/ and $_[0] !~ /^Assets/) { + return 1; + } elsif ($_[0] =~ /^(Accrued)/ and $_[1] !~ /^(Assets|Liabilities)/) { + return -1; + } elsif ($_[1] =~ /^(Accrued)/ and $_[0] !~ /^(Assets|Liabilities)/) { + return 1; + } elsif ($_[0] =~ /^(Unearned Income)/ and $_[1] !~ /^(Assets|Liabilities|Accrued)/) { + return -1; + } elsif ($_[1] =~ /^(Unearned Income)/ and $_[0] !~ /^(Assets|Liabilities|Accrued)/) { + return 1; + } elsif ($_[0] =~ /^Income/ and $_[1] !~ /^(Assets|Liabilities|Accrued|Unearned Income)/) { + return -1; + } elsif ($_[1] =~ /^Income/ and $_[0] !~ /^(Assets|Liabilities|Accrued|Unearned Income)/) { + return 1; + } elsif ($_[0] =~ /^Expense/ and $_[1] !~ /^(Assets|Liabilities|Accrued|Unearned Income|Expense)/) { + return -1; + } elsif ($_[1] =~ /^Expense/ and $_[0] !~ /^(Assets|Liabilities|Accrued|Unearned Income|Expense)/) { + return 1; + } else { + return $_[0] cmp $_[1]; + } +} + my @sortedAccounts; -foreach my $acct ( - # Proper sorting for a chart of accounts - sort { - if ($a =~ /^Assets/ and $b !~ /^Assets/) { - return -1; - } elsif ($a =~ /^Liabilities/ and $b !~ /^Liabilitie/) { - return -1; - } else { - return $a cmp $b; - } - } @accounts) { +foreach my $acct ( sort preferredAccountSorting @accounts) { print CHART_OUTPUT "$acct\n"; push(@sortedAccounts, $acct); } diff --git a/contrib/non-profit-audit-reports/summary-reports.plx b/contrib/non-profit-audit-reports/summary-reports.plx index ce6d56da..30b27505 100755 --- a/contrib/non-profit-audit-reports/summary-reports.plx +++ b/contrib/non-profit-audit-reports/summary-reports.plx @@ -39,6 +39,36 @@ sub Commify ($) { return scalar reverse $text; } +sub preferredAccountSorting ($$) { + if ($_[0] =~ /^Assets/) { + return -1; + } elsif ($_[1] =~ /^Assets/) { + return 1; + } elsif ($_[0] =~ /^Liabilities/ and $_[1] !~ /^Assets/) { + return -1; + } elsif ($_[1] =~ /^Liabilities/ and $_[0] !~ /^Assets/) { + return 1; + } elsif ($_[0] =~ /^(Accrued)/ and $_[1] !~ /^(Assets|Liabilities)/) { + return -1; + } elsif ($_[1] =~ /^(Accrued)/ and $_[0] !~ /^(Assets|Liabilities)/) { + return 1; + } elsif ($_[0] =~ /^(Unearned Income)/ and $_[1] !~ /^(Assets|Liabilities|Accrued)/) { + return -1; + } elsif ($_[1] =~ /^(Unearned Income)/ and $_[0] !~ /^(Assets|Liabilities|Accrued)/) { + return 1; + } elsif ($_[0] =~ /^Income/ and $_[1] !~ /^(Assets|Liabilities|Accrued|Unearned Income)/) { + return -1; + } elsif ($_[1] =~ /^Income/ and $_[0] !~ /^(Assets|Liabilities|Accrued|Unearned Income)/) { + return 1; + } elsif ($_[0] =~ /^Expense/ and $_[1] !~ /^(Assets|Liabilities|Accrued|Unearned Income|Expense)/) { + return -1; + } elsif ($_[1] =~ /^Expense/ and $_[0] !~ /^(Assets|Liabilities|Accrued|Unearned Income|Expense)/) { + return 1; + } else { + return $_[0] cmp $_[1]; + } +} + sub ParseNumber($) { $_[0] =~ s/,//g; return Math::BigFloat->new($_[0]); From f01ddd4766741cd0dc09fd42cb7cc87f4dadbb20 Mon Sep 17 00:00:00 2001 From: "Bradley M. Kuhn" Date: Thu, 3 Jan 2013 12:19:28 -0500 Subject: [PATCH 04/20] Change chart of accounts output to be a CSV file instead of TXT file. This includes adding a formatted start date string too. --- .../general-ledger-report.plx | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/contrib/non-profit-audit-reports/general-ledger-report.plx b/contrib/non-profit-audit-reports/general-ledger-report.plx index 66fc0031..3c9ec74d 100755 --- a/contrib/non-profit-audit-reports/general-ledger-report.plx +++ b/contrib/non-profit-audit-reports/general-ledger-report.plx @@ -44,10 +44,24 @@ if (@ARGV < 3) { print STDERR "usage: $0 \n"; exit 1; } + + open(MANIFEST, ">", "MANIFEST") or die "Unable to open MANIFEST for writing: $!"; my($beginDate, $endDate, @otherLedgerOpts) = @ARGV; +my $formattedEndDate = new Date::Manip::Date; +die "badly formatted end date, $endDate" if $formattedEndDate->parse($endDate); +my $oneDayLess = new Date::Manip::Delta; +die "bad one day less" if $oneDayLess->parse("- 1 day"); +$formattedEndDate = $formattedEndDate->calc($oneDayLess); +$formattedEndDate = $formattedEndDate->printf("%Y/%m/%d"); + +my $formattedBeginDate = new Date::Manip::Date; +die "badly formatted end date, $endDate" if $formattedBeginDate->parse($endDate); +$formattedBeginDate = $formattedBeginDate->printf("%Y/%m/%d"); + + my(@chartOfAccountsOpts) = ('-V', '-F', "%150A\n", '-w', '-s', '-b', $beginDate, '-e', $endDate, @otherLedgerOpts, 'reg'); @@ -64,8 +78,12 @@ while (my $line = ) { } close(CHART_DATA); die "error reading ledger output for chart of accounts: $!" unless $? == 0; -open(CHART_OUTPUT, ">", "chart-of-accounts.txt") or die "unable to write chart-of-accounts.txt: $!"; -print MANIFEST "chart-of-accounts.txt\n"; +open(CHART_OUTPUT, ">", "chart-of-accounts.csv") or die "unable to write chart-of-accounts.csv: $!"; +print MANIFEST "chart-of-accounts.csv\n"; + +print CHART_OUTPUT "\"CHART OF ACCOUNTS\","; +print CHART_OUTPUT "\"BEGINNING:\",\"$formattedBeginDate\","; +print CHART_OUTPUT "\"ENDING:\",\"$formattedEndDate\"\n"; sub preferredAccountSorting ($$) { if ($_[0] =~ /^Assets/) { @@ -99,17 +117,11 @@ sub preferredAccountSorting ($$) { my @sortedAccounts; foreach my $acct ( sort preferredAccountSorting @accounts) { - print CHART_OUTPUT "$acct\n"; + print CHART_OUTPUT "\"$acct\"\n"; push(@sortedAccounts, $acct); } close(CHART_OUTPUT); die "error writing to chart-of-accounts.txt: $!" unless $? == 0; -my $formattedEndDate = new Date::Manip::Date; -die "badly formatted end date, $endDate" if $formattedEndDate->parse($endDate); -my $oneDayLess = new Date::Manip::Delta; -die "bad one day less" if $oneDayLess->parse("- 1 day"); -$formattedEndDate = $formattedEndDate->calc($oneDayLess); -$formattedEndDate = $formattedEndDate->printf("%Y/%m/%d"); open(GL_TEXT_OUT, ">", "general-ledger.txt") or die "unable to write general-ledger.txt: $!"; print MANIFEST "general-ledger.txt\n"; From d18e01a00f88ec7d073b88db464679a9aa9910a6 Mon Sep 17 00:00:00 2001 From: "Bradley M. Kuhn" Date: Thu, 3 Jan 2013 12:19:59 -0500 Subject: [PATCH 05/20] Changed balance sheet output from a TXT file to a CSV file. --- .../non-profit-audit-reports/summary-reports.plx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/contrib/non-profit-audit-reports/summary-reports.plx b/contrib/non-profit-audit-reports/summary-reports.plx index 30b27505..da2dba9d 100755 --- a/contrib/non-profit-audit-reports/summary-reports.plx +++ b/contrib/non-profit-audit-reports/summary-reports.plx @@ -146,15 +146,15 @@ foreach my $item (keys %reportFields) { print STDERR "$item: $reportFields{$item}{total}\n" if $VERBOSE; } -open(BALANCE_SHEET, ">", "balance-sheet.txt") - or die "unable to open balance-sheet.txt for writing: $!"; +open(BALANCE_SHEET, ">", "balance-sheet.csv") + or die "unable to open balance-sheet.csv for writing: $!"; -print BALANCE_SHEET " BALANCE SHEET\n", - " Ending ", $formattedEndDate, "\n", - "\n\nASSETS\n\n"; +print BALANCE_SHEET "\"BALANCE SHEET\"\n", + "\"Ending\",\"", $formattedEndDate, "\"\n", + "\n\n\"ASSETS\"\n\n"; -my $formatStr = " %-42s \$%13s\n"; -my $formatStrTotal = "%-45s \$%13s\n"; +my $formatStr = "\"\",\"%-42s\",\"\$%13s\"\n"; +my $formatStrTotal = "\"\",\"%-45s\",\"\$%13s\"\n"; my $tot = $ZERO; foreach my $item ('Cash', 'Accounts Receivable', 'Loans Receivable') { next if $reportFields{$item}{total} == $ZERO; @@ -186,7 +186,7 @@ print BALANCE_SHEET "\n", sprintf($formatStr, "TOTAL NET ASSETS", Commify($totNe close BALANCE_SHEET; print STDERR "\n"; -die "unable to write to balance-sheet.txt: $!" unless ($? == 0); +die "unable to write to balance-sheet.csv: $!" unless ($? == 0); die "Cash+accounts receivable total does not equal net assets and liabilities total" if (abs( ($reportFields{'Cash'}{total} + $reportFields{'Accounts Receivable'}{total} From b939bbe8c61a819e413b3e2a24d223f6232bd0d9 Mon Sep 17 00:00:00 2001 From: "Bradley M. Kuhn" Date: Thu, 3 Jan 2013 12:30:43 -0500 Subject: [PATCH 06/20] Convert trial-balance report to CSV from TXT file. Also, ignore Ledger's Equity: accounts properly. --- .../summary-reports.plx | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/contrib/non-profit-audit-reports/summary-reports.plx b/contrib/non-profit-audit-reports/summary-reports.plx index da2dba9d..dae8f2ce 100755 --- a/contrib/non-profit-audit-reports/summary-reports.plx +++ b/contrib/non-profit-audit-reports/summary-reports.plx @@ -378,15 +378,15 @@ die "calculated total of $overallTotal does equal $firstTotal" print STDERR "\n"; -open(TRIAL, ">", "trial-balance.txt") or die "unable to open accrued.txt for writing: $!"; +open(TRIAL, ">", "trial-balance.csv") or die "unable to open accrued.txt for writing: $!"; @fullCommand = ($LEDGER_BIN, @mainLedgerOptions, '-V', '-X', '$', '-e', $endDate, '-F', '%-.80A %22.108t\n', '-s', 'reg'); -print TRIAL " TRIAL BALANCE \n", - " Ending $formattedEndDate\n\n"; +print TRIAL "\"TRIAL BALANCE REPORT\",", + "\"ENDING:\",\"$formattedEndDate\"\n\n\"ACCOUNT NAME\", \"AMOUNT\"\n\n"; open(FILE, "-|", @fullCommand) or die "unable to run command ledger command: @fullCommand: $!"; @@ -395,17 +395,28 @@ print STDERR ($VERBOSE ? "Running: @fullCommand\n" : "."); my $accruedTotal = $ZERO; +my %trialBalances; + foreach my $line () { die "Unable to parse output line from second funds command: $line" unless $line =~ /^\s*([^\$]+)\s+\$\s*([\-\d\.\,]+)/; my($account, $amount) = ($1, $2); $amount = ParseNumber($amount); $account =~ s/\s+$//; - next if $account =~ /\|^Equity:/ and (abs($amount) <= 0.02); - print TRIAL $line; - + next if $account =~ /\/ and (abs($amount) <= 0.02); + next if $account =~ /^Equity:/; # Stupid auto-account made by ledger. + $trialBalances{$account} = $amount; $accruedTotal += $amount; } +close FILE; +die "unable to run trial balance ledger command: $!" unless ($? == 0); + +foreach my $account (sort preferredAccountSorting keys %trialBalances) { + print TRIAL "\"$account\",\"$trialBalances{$account}\"\n"; +} + +close TRIAL; +die "unable to write trial-balance.csv: $!" unless ($? == 0); ############################################################################### # From fbd6c309031393950ac387b9de5f07214c5fa3cf Mon Sep 17 00:00:00 2001 From: "Bradley M. Kuhn" Date: Thu, 3 Jan 2013 13:36:38 -0500 Subject: [PATCH 07/20] Add option to skip generating the page breaks. --- contrib/non-profit-audit-reports/csv2ods.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/contrib/non-profit-audit-reports/csv2ods.py b/contrib/non-profit-audit-reports/csv2ods.py index 2b3024d4..aded8e65 100755 --- a/contrib/non-profit-audit-reports/csv2ods.py +++ b/contrib/non-profit-audit-reports/csv2ods.py @@ -2,8 +2,8 @@ # csv2ods.py # Convert example csv file to ods # -# Copyright (c) 2012 Tom Marble -# Copyright (c) 2012 Bradley M. Kuhn +# Copyright (c) 2012 Tom Marble +# Copyright (c) 2012, 2013 Bradley M. Kuhn # # This program gives you software freedom; you can copy, modify, convey, # and/or redistribute it under the terms of the GNU General Public License @@ -31,7 +31,7 @@ def err(msg): print 'error: %s' % msg sys.exit(1) -def csv2ods(csvname, odsname, encoding='', verbose = False): +def csv2ods(csvname, odsname, encoding='', verbose = False, skip_page_break = False): if verbose: print 'converting from %s to %s' % (csvname, odsname) doc = ooolib2.Calc() @@ -77,7 +77,8 @@ def csv2ods(csvname, odsname, encoding='', verbose = False): # enter an empty string for blank lines doc.set_cell_value(1, row, 'string', '') # put a pagebreak here - doc.sheets[doc.sheet_index].set_sheet_config(('row', row), style_pagebreak) + if not skip_page_break: + doc.sheets[doc.sheet_index].set_sheet_config(('row', row), style_pagebreak) row += 1 # save the file doc.save(odsname) @@ -96,6 +97,9 @@ def main(): help='ods output filename') parser.add_option('-e', '--encoding', action='store', help='unicode character encoding type') + parser.add_option('-s', '--skip-page-break', action='store_true', + dest='skip_page_break', + help='do not add any page breaks') (options, args) = parser.parse_args() if len(args) != 0: parser.error("not expecting extra args") @@ -109,7 +113,7 @@ def main(): print 'csv:', options.csv print 'ods:', options.ods print 'ods:', options.encoding - csv2ods(options.csv, options.ods, options.verbose, options.encoding) + csv2ods(options.csv, options.ods, options.verbose, options.encoding, options.skip_page_break) if __name__ == '__main__': main() From 750321c0b1e35b0315138b35524f85d3648d8dda Mon Sep 17 00:00:00 2001 From: "Bradley M. Kuhn" Date: Thu, 3 Jan 2013 13:37:08 -0500 Subject: [PATCH 08/20] Change Income and Expenses reports to generate CSV files, rather than TXT files. --- .../summary-reports.plx | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/contrib/non-profit-audit-reports/summary-reports.plx b/contrib/non-profit-audit-reports/summary-reports.plx index dae8f2ce..efad30e7 100755 --- a/contrib/non-profit-audit-reports/summary-reports.plx +++ b/contrib/non-profit-audit-reports/summary-reports.plx @@ -223,7 +223,7 @@ foreach my $type (keys %incomeGroups) { $incomeGroups{"OTHER"}{args} = \@otherArgs; $incomeGroups{"TOTAL"}{args} = ['/^Income/']; -open(INCOME, ">", "income.txt") or die "unable to open income.txt for writing: $!"; +open(INCOME, ">", "income.csv") or die "unable to open income.csv for writing: $!"; foreach my $type (keys %incomeGroups) { my(@fullCommand) = ($LEDGER_BIN, @mainLedgerOptions, '-V', '-X', '$', @@ -247,32 +247,32 @@ foreach my $type (keys %incomeGroups) { $account =~ s/\s+$//; next if $account =~ /\/ and (abs($amount) <= 0.02); die "Weird account found, $account with amount of $amount in income command\n" - unless $account =~ s/^\s*Income://; + unless $account =~ /^\s*Income:/; $incomeGroups{$type}{total} += $amount; - $incomeGroups{$type}{output} .= " $line"; + $incomeGroups{$type}{output} .= "\"$account\",\"\$$amount\"\n"; } } -print INCOME " INCOME\n", - " Between $formattedStartDate and $formattedEndDate\n\n"; +print INCOME "\"INCOME\",", + "\"STARTING:\",\"$formattedStartDate\",\"ENDING:\",\"$formattedEndDate\"\n\n"; my $overallTotal = $ZERO; -$formatStrTotal = "%-90s \$%14s\n"; +$formatStrTotal = "\"%-90s\",\"\$%14s\"\n"; foreach my $type ('DONATIONS', 'LICENSE ENFORCEMENT', 'CONFERENCES, REGISTRATION', 'CONFERENCES, RELATED BUSINESS INCOME', 'BOOK ROYALTIES & AFFILIATE PROGRAMS', 'ADVERSITING', 'TRADEMARKS', 'INTEREST INCOME', 'OTHER') { next if ($incomeGroups{$type}{output} =~ /^\s*$/ and $incomeGroups{$type}{total} == $ZERO); - print INCOME "\n$type\n", + print INCOME "\n\"$type\"\n", $incomeGroups{$type}{output}, "\n", sprintf($formatStrTotal, "TOTAL $type:", Commify($incomeGroups{$type}{total})); $overallTotal += $incomeGroups{$type}{total}; } print INCOME "\n\n\n", sprintf($formatStrTotal, "OVERALL TOTAL:", Commify($overallTotal)); -close INCOME; die "unable to write to income.txt: $!" unless ($? == 0); +close INCOME; die "unable to write to income.csv: $!" unless ($? == 0); die "calculated total of $overallTotal does equal $incomeGroups{TOTAL}{total}" if (abs($overallTotal) - abs($incomeGroups{TOTAL}{total}) > $ONE_PENNY); @@ -298,7 +298,7 @@ foreach my $type (keys %expenseGroups, 'TRAVEL') { $expenseGroups{$type}{output} = ""; } -open(EXPENSE, ">", "expense.txt") or die "unable to open expense.txt for writing: $!"; +open(EXPENSE, ">", "expense.csv") or die "unable to open expense.csv for writing: $!"; my(@fullCommand) = ($LEDGER_BIN, @mainLedgerOptions, '-V', '-X', '$', '-b', $startDate, '-e', $endDate, @@ -321,6 +321,7 @@ foreach my $line () { die "Weird account found, $account, with amount of $amount in expenses command\n" unless $account =~ /^\s*Expenses:/; + my $outputLine = "\"$account\",\"\$$amount\"\n"; my $taken = 0; # Note: Prioritize to put things under conference expenses if they were for a conference. foreach my $type ('CONFERENCES', keys %expenseGroups) { @@ -329,23 +330,23 @@ foreach my $line () { next unless $line =~ /$expenseGroups{$type}{regex}/; $taken = 1; $expenseGroups{$type}{total} += $amount; - $expenseGroups{$type}{output} .= " $line"; + $expenseGroups{$type}{output} .= $outputLine; } if (not $taken) { if ($account =~ /Travel/) { $expenseGroups{'TRAVEL'}{total} += $amount; - $expenseGroups{'TRAVEL'}{output} .= " $line"; + $expenseGroups{'TRAVEL'}{output} .= $outputLine; } else { $expenseGroups{'OTHER'}{total} += $amount; - $expenseGroups{'OTHER'}{output} .= " $line"; + $expenseGroups{'OTHER'}{output} .= $outputLine; } } $firstTotal += $amount; } -print EXPENSE " EXPENSES\n", - " Between $formattedStartDate and $formattedEndDate\n\n"; +print EXPENSE "\"EXPENSES\",", + "\"STARTING:\",\"$formattedStartDate\",\"ENDING:\",\"$formattedEndDate\"\n\n"; $overallTotal = $ZERO; -$formatStrTotal = "%-90s \$%14s\n"; +$formatStrTotal = "\"%-90s\",\"\$%14s\"\n"; my %verifyAllGroups; foreach my $key (keys %expenseGroups) { @@ -360,7 +361,7 @@ foreach my $type ('PAYROLL', 'SOFTWARE DEVELOPMENT', 'LICENSE ENFORCEMENT', 'CON die "$type is not defined!" if not defined $expenseGroups{$type}; next if ($expenseGroups{$type}{output} =~ /^\s*$/ and $expenseGroups{$type}{total} == $ZERO); - print EXPENSE "\n$type\n", + print EXPENSE "\n\"$type\"\n", $expenseGroups{$type}{output}, "\n", sprintf($formatStrTotal, "TOTAL $type:", Commify($expenseGroups{$type}{total})); $overallTotal += $expenseGroups{$type}{total}; @@ -368,7 +369,7 @@ foreach my $type ('PAYROLL', 'SOFTWARE DEVELOPMENT', 'LICENSE ENFORCEMENT', 'CON print EXPENSE "\n\n\n", sprintf($formatStrTotal, "OVERALL TOTAL:", Commify($overallTotal)); -close EXPENSE; die "unable to write to expense.txt: $!" unless ($? == 0); +close EXPENSE; die "unable to write to expense.csv: $!" unless ($? == 0); die "GROUPS NOT INCLUDED : ", join(keys(%verifyAllGroups), ", "), "\n" unless (keys %verifyAllGroups == 0); From 2fad8fe238cee8509290562a823fe0c3b312ee94 Mon Sep 17 00:00:00 2001 From: "Bradley M. Kuhn" Date: Thu, 3 Jan 2013 13:37:18 -0500 Subject: [PATCH 09/20] Some minor formatting fixes for the trial balance report. --- contrib/non-profit-audit-reports/summary-reports.plx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contrib/non-profit-audit-reports/summary-reports.plx b/contrib/non-profit-audit-reports/summary-reports.plx index efad30e7..8e9339a5 100755 --- a/contrib/non-profit-audit-reports/summary-reports.plx +++ b/contrib/non-profit-audit-reports/summary-reports.plx @@ -387,7 +387,7 @@ open(TRIAL, ">", "trial-balance.csv") or die "unable to open accrued.txt for wri 'reg'); print TRIAL "\"TRIAL BALANCE REPORT\",", - "\"ENDING:\",\"$formattedEndDate\"\n\n\"ACCOUNT NAME\", \"AMOUNT\"\n\n"; + "\"ENDING:\",\"$formattedEndDate\"\n\n\"ACCOUNT NAME\",\"AMOUNT\"\n\n"; open(FILE, "-|", @fullCommand) or die "unable to run command ledger command: @fullCommand: $!"; @@ -413,7 +413,7 @@ close FILE; die "unable to run trial balance ledger command: $!" unless ($? == 0); foreach my $account (sort preferredAccountSorting keys %trialBalances) { - print TRIAL "\"$account\",\"$trialBalances{$account}\"\n"; + print TRIAL "\"$account\",\"\$$trialBalances{$account}\"\n"; } close TRIAL; From e317e1f23e236a2797b13113bc2afae14502fc5c Mon Sep 17 00:00:00 2001 From: "Bradley M. Kuhn" Date: Fri, 4 Jan 2013 10:18:41 -0500 Subject: [PATCH 10/20] Sort of accounts was buggy; it never made the final else due to bad regexes. This fix now has the sort working correctly. --- .../general-ledger-report.plx | 29 +++++++++++-------- .../summary-reports.plx | 28 ++++++++++-------- 2 files changed, 33 insertions(+), 24 deletions(-) diff --git a/contrib/non-profit-audit-reports/general-ledger-report.plx b/contrib/non-profit-audit-reports/general-ledger-report.plx index 3c9ec74d..a464053b 100755 --- a/contrib/non-profit-audit-reports/general-ledger-report.plx +++ b/contrib/non-profit-audit-reports/general-ledger-report.plx @@ -86,35 +86,40 @@ print CHART_OUTPUT "\"BEGINNING:\",\"$formattedBeginDate\","; print CHART_OUTPUT "\"ENDING:\",\"$formattedEndDate\"\n"; sub preferredAccountSorting ($$) { - if ($_[0] =~ /^Assets/) { + if ($_[0] =~ /^Assets/ and $_[1] !~ /^Assets/) { return -1; - } elsif ($_[1] =~ /^Assets/) { + } elsif ($_[1] =~ /^Assets/ and $_[0] !~ /^Assets/) { return 1; - } elsif ($_[0] =~ /^Liabilities/ and $_[1] !~ /^Assets/) { + } elsif ($_[0] =~ /^Liabilities/ and $_[1] !~ /^(Assets|Liabilities)/) { return -1; - } elsif ($_[1] =~ /^Liabilities/ and $_[0] !~ /^Assets/) { + } elsif ($_[1] =~ /^Liabilities/ and $_[0] !~ /^(Assets|Liabilities)/) { return 1; - } elsif ($_[0] =~ /^(Accrued)/ and $_[1] !~ /^(Assets|Liabilities)/) { + } elsif ($_[0] =~ /^(Accrued:[^:]+Receivable)/ and $_[1] !~ /^(Assets|Liabilities|Accrued:[^:]+Receivable)/) { return -1; - } elsif ($_[1] =~ /^(Accrued)/ and $_[0] !~ /^(Assets|Liabilities)/) { + } elsif ($_[1] =~ /^(Accrued:[^:]+Receivable)/ and $_[0] !~ /^(Assets|Liabilities|Accrued:[^:]+Receivable)/) { return 1; - } elsif ($_[0] =~ /^(Unearned Income)/ and $_[1] !~ /^(Assets|Liabilities|Accrued)/) { + } elsif ($_[0] =~ /^(Accrued)/ and $_[1] !~ /^(Assets|Liabilities|Accrued)/) { return -1; - } elsif ($_[1] =~ /^(Unearned Income)/ and $_[0] !~ /^(Assets|Liabilities|Accrued)/) { + } elsif ($_[1] =~ /^(Accrued)/ and $_[0] !~ /^(Assets|Liabilities|Accrued)/) { return 1; - } elsif ($_[0] =~ /^Income/ and $_[1] !~ /^(Assets|Liabilities|Accrued|Unearned Income)/) { + } elsif ($_[0] =~ /^(Unearned Income)/ and $_[1] !~ /^(Assets|Liabilities|Accrued|Unearned Income)/) { return -1; - } elsif ($_[1] =~ /^Income/ and $_[0] !~ /^(Assets|Liabilities|Accrued|Unearned Income)/) { + } elsif ($_[1] =~ /^(Unearned Income)/ and $_[0] !~ /^(Assets|Liabilities|Accrued|Unearned Income)/) { return 1; - } elsif ($_[0] =~ /^Expense/ and $_[1] !~ /^(Assets|Liabilities|Accrued|Unearned Income|Expense)/) { + } elsif ($_[0] =~ /^Income/ and $_[1] !~ /^(Assets|Liabilities|Accrued|Unearned Income|Income)/) { return -1; - } elsif ($_[1] =~ /^Expense/ and $_[0] !~ /^(Assets|Liabilities|Accrued|Unearned Income|Expense)/) { + } elsif ($_[1] =~ /^Income/ and $_[0] !~ /^(Assets|Liabilities|Accrued|Unearned Income|Income)/) { + return 1; + } elsif ($_[0] =~ /^Expense/ and $_[1] !~ /^(Assets|Liabilities|Accrued|Income|Unearned Income|Expense)/) { + return -1; + } elsif ($_[1] =~ /^Expense/ and $_[0] !~ /^(Assets|Liabilities|Accrued|Income|Unearned Income|Expense)/) { return 1; } else { return $_[0] cmp $_[1]; } } + my @sortedAccounts; foreach my $acct ( sort preferredAccountSorting @accounts) { print CHART_OUTPUT "\"$acct\"\n"; diff --git a/contrib/non-profit-audit-reports/summary-reports.plx b/contrib/non-profit-audit-reports/summary-reports.plx index 8e9339a5..17999c36 100755 --- a/contrib/non-profit-audit-reports/summary-reports.plx +++ b/contrib/non-profit-audit-reports/summary-reports.plx @@ -40,29 +40,33 @@ sub Commify ($) { } sub preferredAccountSorting ($$) { - if ($_[0] =~ /^Assets/) { + if ($_[0] =~ /^Assets/ and $_[1] !~ /^Assets/) { return -1; - } elsif ($_[1] =~ /^Assets/) { + } elsif ($_[1] =~ /^Assets/ and $_[0] !~ /^Assets/) { return 1; - } elsif ($_[0] =~ /^Liabilities/ and $_[1] !~ /^Assets/) { + } elsif ($_[0] =~ /^Liabilities/ and $_[1] !~ /^(Assets|Liabilities)/) { return -1; - } elsif ($_[1] =~ /^Liabilities/ and $_[0] !~ /^Assets/) { + } elsif ($_[1] =~ /^Liabilities/ and $_[0] !~ /^(Assets|Liabilities)/) { return 1; - } elsif ($_[0] =~ /^(Accrued)/ and $_[1] !~ /^(Assets|Liabilities)/) { + } elsif ($_[0] =~ /^(Accrued:[^:]+Receivable)/ and $_[1] !~ /^(Assets|Liabilities|Accrued:[^:]+Receivable)/) { return -1; - } elsif ($_[1] =~ /^(Accrued)/ and $_[0] !~ /^(Assets|Liabilities)/) { + } elsif ($_[1] =~ /^(Accrued:[^:]+Receivable)/ and $_[0] !~ /^(Assets|Liabilities|Accrued:[^:]+Receivable)/) { return 1; - } elsif ($_[0] =~ /^(Unearned Income)/ and $_[1] !~ /^(Assets|Liabilities|Accrued)/) { + } elsif ($_[0] =~ /^(Accrued)/ and $_[1] !~ /^(Assets|Liabilities|Accrued)/) { return -1; - } elsif ($_[1] =~ /^(Unearned Income)/ and $_[0] !~ /^(Assets|Liabilities|Accrued)/) { + } elsif ($_[1] =~ /^(Accrued)/ and $_[0] !~ /^(Assets|Liabilities|Accrued)/) { return 1; - } elsif ($_[0] =~ /^Income/ and $_[1] !~ /^(Assets|Liabilities|Accrued|Unearned Income)/) { + } elsif ($_[0] =~ /^(Unearned Income)/ and $_[1] !~ /^(Assets|Liabilities|Accrued|Unearned Income)/) { return -1; - } elsif ($_[1] =~ /^Income/ and $_[0] !~ /^(Assets|Liabilities|Accrued|Unearned Income)/) { + } elsif ($_[1] =~ /^(Unearned Income)/ and $_[0] !~ /^(Assets|Liabilities|Accrued|Unearned Income)/) { return 1; - } elsif ($_[0] =~ /^Expense/ and $_[1] !~ /^(Assets|Liabilities|Accrued|Unearned Income|Expense)/) { + } elsif ($_[0] =~ /^Income/ and $_[1] !~ /^(Assets|Liabilities|Accrued|Unearned Income|Income)/) { return -1; - } elsif ($_[1] =~ /^Expense/ and $_[0] !~ /^(Assets|Liabilities|Accrued|Unearned Income|Expense)/) { + } elsif ($_[1] =~ /^Income/ and $_[0] !~ /^(Assets|Liabilities|Accrued|Unearned Income|Income)/) { + return 1; + } elsif ($_[0] =~ /^Expense/ and $_[1] !~ /^(Assets|Liabilities|Accrued|Income|Unearned Income|Expense)/) { + return -1; + } elsif ($_[1] =~ /^Expense/ and $_[0] !~ /^(Assets|Liabilities|Accrued|Income|Unearned Income|Expense)/) { return 1; } else { return $_[0] cmp $_[1]; From 986829b1d656611eb243b920703acc198fdf3f37 Mon Sep 17 00:00:00 2001 From: "Bradley M. Kuhn" Date: Fri, 4 Jan 2013 10:19:16 -0500 Subject: [PATCH 11/20] Corrected Trial Balance report based on discussion with accountants. I believe this trial balance report will look "more natural" to accountants. --- .../summary-reports.plx | 85 +++++++++++++------ 1 file changed, 58 insertions(+), 27 deletions(-) diff --git a/contrib/non-profit-audit-reports/summary-reports.plx b/contrib/non-profit-audit-reports/summary-reports.plx index 17999c36..bbe83c88 100755 --- a/contrib/non-profit-audit-reports/summary-reports.plx +++ b/contrib/non-profit-audit-reports/summary-reports.plx @@ -385,41 +385,72 @@ print STDERR "\n"; open(TRIAL, ">", "trial-balance.csv") or die "unable to open accrued.txt for writing: $!"; -@fullCommand = ($LEDGER_BIN, @mainLedgerOptions, '-V', '-X', '$', - '-e', $endDate, - '-F', '%-.80A %22.108t\n', '-s', - 'reg'); +print TRIAL "\"TRIAL BALANCE REPORT\",\"ENDING: $formattedEndDate\"\n\n", + "\"ACCOUNT\",\"BALANCE AT $formattedStartDate\",\"CHANGE DURING FY\",\"BALANCE AT $formattedEndDate\"\n\n"; -print TRIAL "\"TRIAL BALANCE REPORT\",", - "\"ENDING:\",\"$formattedEndDate\"\n\n\"ACCOUNT NAME\",\"AMOUNT\"\n\n"; +my %commands = ( + 'totalEndFY' => [ $LEDGER_BIN, @mainLedgerOptions, '-V', '-X', '$', + '-e', $endDate, '-F', '%-.80A %22.108t\n', '-s', + 'reg' ], + 'amountInYear' => [ $LEDGER_BIN, @mainLedgerOptions, '-V', '-X', '$', + '-b', $startDate, '-e', $endDate, '-F', '%-.80A %22.108t\n', + '-s', 'reg' ], + 'totalBeginFY' => [ $LEDGER_BIN, @mainLedgerOptions, '-V', '-X', '$', + '-e', $startDate, '-F', '%-.80A %22.108t\n', + '-s', 'reg' ]); -open(FILE, "-|", @fullCommand) - or die "unable to run command ledger command: @fullCommand: $!"; +my %trialBalanceData; +my %fullAccountList; -print STDERR ($VERBOSE ? "Running: @fullCommand\n" : "."); +foreach my $id (keys %commands) { + my(@command) = @{$commands{$id}}; -my $accruedTotal = $ZERO; + open(FILE, "-|", @command) + or die "unable to run command ledger command: @command: $!"; -my %trialBalances; + print STDERR ($VERBOSE ? "Running: @command\n" : "."); -foreach my $line () { - die "Unable to parse output line from second funds command: $line" - unless $line =~ /^\s*([^\$]+)\s+\$\s*([\-\d\.\,]+)/; - my($account, $amount) = ($1, $2); - $amount = ParseNumber($amount); - $account =~ s/\s+$//; - next if $account =~ /\/ and (abs($amount) <= 0.02); - next if $account =~ /^Equity:/; # Stupid auto-account made by ledger. - $trialBalances{$account} = $amount; - $accruedTotal += $amount; -} -close FILE; -die "unable to run trial balance ledger command: $!" unless ($? == 0); - -foreach my $account (sort preferredAccountSorting keys %trialBalances) { - print TRIAL "\"$account\",\"\$$trialBalances{$account}\"\n"; + foreach my $line () { + die "Unable to parse output line from trial balance $id command: $line" + unless $line =~ /^\s*([^\$]+)\s+\$\s*([\-\d\.\,]+)/; + my($account, $amount) = ($1, $2); + $amount = ParseNumber($amount); + $account =~ s/\s+$//; + next if $account =~ /\/ and (abs($amount) <= 0.02); + next if $account =~ /^Equity:/; # Stupid auto-account made by ledger. + $trialBalanceData{$id}{$account} = $amount; + $fullAccountList{$account} = $id; + } + close FILE; + die "unable to run trial balance ledger command, @command: $!" unless ($? == 0); } +my $curOn = 'Assets'; + +foreach my $account (sort preferredAccountSorting keys %fullAccountList) { + # Blank lines right + if ($account !~ /^$curOn/) { + print TRIAL "\n"; + $curOn = $account; + $curOn =~ s/^([^:]+):.*$/$1/; + print "CurOn now: $curOn\n"; + } + if ($account =~ /^Assets|Liabilities|Accrued|Unearned Income/) { + foreach my $id (qw/totalBeginFY totalEndFY amountInYear/) { + $trialBalanceData{$id}{$account} = $ZERO + unless defined $trialBalanceData{$id}{$account}; + } + print TRIAL "\"$account\",\"\$$trialBalanceData{totalBeginFY}{$account}\",", + "\"\$$trialBalanceData{amountInYear}{$account}\",\"\$$trialBalanceData{totalEndFY}{$account}\"\n" + unless $trialBalanceData{totalBeginFY}{$account} == $ZERO and + $trialBalanceData{amountInYear}{$account} == $ZERO and + $trialBalanceData{totalEndFY}{$account} == $ZERO; + } else { + print TRIAL "\"$account\",\"\",\"\$$trialBalanceData{amountInYear}{$account}\",\"\"\n" + if defined $trialBalanceData{amountInYear}{$account} and + $trialBalanceData{amountInYear}{$account} != $ZERO; + } +} close TRIAL; die "unable to write trial-balance.csv: $!" unless ($? == 0); From 87f0c4434d131ba78ba191780dbff2b8c47f3123 Mon Sep 17 00:00:00 2001 From: "Bradley M. Kuhn" Date: Fri, 4 Jan 2013 12:23:37 -0500 Subject: [PATCH 12/20] Updated expected test output data for general-ledger report. --- ...non-profit-test-data_chart-of-accounts.csv | 6 ++++++ ...non-profit-test-data_chart-of-accounts.txt | 4 ---- .../non-profit-test-data_general-ledger.ods | Bin 11412 -> 5770 bytes 3 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 contrib/non-profit-audit-reports/tests/non-profit-test-data_chart-of-accounts.csv delete mode 100644 contrib/non-profit-audit-reports/tests/non-profit-test-data_chart-of-accounts.txt diff --git a/contrib/non-profit-audit-reports/tests/non-profit-test-data_chart-of-accounts.csv b/contrib/non-profit-audit-reports/tests/non-profit-test-data_chart-of-accounts.csv new file mode 100644 index 00000000..445bc412 --- /dev/null +++ b/contrib/non-profit-audit-reports/tests/non-profit-test-data_chart-of-accounts.csv @@ -0,0 +1,6 @@ +"CHART OF ACCOUNTS","BEGINNING:","2012/03/01","ENDING:","2012/02/29" +"Assets:Checking" +"Income:Donation" +"Income:Foo:Donation" +"Expenses:Blah:Hosting" +"Expenses:Foo:Hosting" diff --git a/contrib/non-profit-audit-reports/tests/non-profit-test-data_chart-of-accounts.txt b/contrib/non-profit-audit-reports/tests/non-profit-test-data_chart-of-accounts.txt deleted file mode 100644 index 57e636b9..00000000 --- a/contrib/non-profit-audit-reports/tests/non-profit-test-data_chart-of-accounts.txt +++ /dev/null @@ -1,4 +0,0 @@ -Assets:Checking -Expenses:Foo:Hosting -Income:Donation -Income:Foo:Donation diff --git a/contrib/non-profit-audit-reports/tests/non-profit-test-data_general-ledger.ods b/contrib/non-profit-audit-reports/tests/non-profit-test-data_general-ledger.ods index 80771a6d7d9c74fb1490485ac6eb607939ac0e80..8eae706f5d97664ea0371b6d9a8d72ea4de90668 100644 GIT binary patch literal 5770 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 literal 11412 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 C0ZSzS From 2b237aa3ba15fd0964690eac379f9226990e6f05 Mon Sep 17 00:00:00 2001 From: "Bradley M. Kuhn" Date: Fri, 4 Jan 2013 12:24:30 -0500 Subject: [PATCH 13/20] MANIFEST file is now also generated by general-ledger report. We should give the sample MANIFEST for users that want to make sure they got the script working properly, and to show the sample output. --- .gitignore | 1 + .../tests/non-profit-test-data_MANIFEST | 10 ++++++++++ 2 files changed, 11 insertions(+) create mode 100644 contrib/non-profit-audit-reports/tests/non-profit-test-data_MANIFEST diff --git a/.gitignore b/.gitignore index 02f6433a..c60fe7d4 100644 --- a/.gitignore +++ b/.gitignore @@ -103,4 +103,5 @@ contrib/non-profit-audit-reports/tests/chart-of-accounts.txt contrib/non-profit-audit-reports/tests/general-ledger.csv contrib/non-profit-audit-reports/tests/general-ledger.ods contrib/non-profit-audit-reports/tests/general-ledger.txt +contrib/non-profit-audit-reports/tests/MANIFEST contrib/non-profit-audit-reports/general-ledger.zip diff --git a/contrib/non-profit-audit-reports/tests/non-profit-test-data_MANIFEST b/contrib/non-profit-audit-reports/tests/non-profit-test-data_MANIFEST new file mode 100644 index 00000000..b8bfc107 --- /dev/null +++ b/contrib/non-profit-audit-reports/tests/non-profit-test-data_MANIFEST @@ -0,0 +1,10 @@ +chart-of-accounts.csv +general-ledger.txt +general-ledger.csv +Financial/BankStuff/bank-statement.pdf +Financial/Invoices/Invoice20110510.pdf +Projects/Foo/Invoices/Invoice20100101.pdf +Projects/Foo/earmark-record.txt +Projects/Blah/Expenses/hosting/AprilHostingReceipt.pdf +Projects/Blah/Expenses/hosting/april-invoice.pdf +Projects/Foo/Expenses/hosting/AprilHostingReceipt.pdf From 4290a4ec52793baeaaa3ae6dbb75cbef993d3ef4 Mon Sep 17 00:00:00 2001 From: "Bradley M. Kuhn" Date: Sat, 5 Jan 2013 13:13:05 -0500 Subject: [PATCH 14/20] Add balances for permanent (i.e., asset) accounts. Based on a request from our accountants, I've changed the RUNNING TOTAL field (which is generally useless to accountants anyway) to be a BALANCE amount for starting and ending accounts. --- .../general-ledger-report.plx | 44 +++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/contrib/non-profit-audit-reports/general-ledger-report.plx b/contrib/non-profit-audit-reports/general-ledger-report.plx index a464053b..3b230837 100755 --- a/contrib/non-profit-audit-reports/general-ledger-report.plx +++ b/contrib/non-profit-audit-reports/general-ledger-report.plx @@ -127,6 +127,34 @@ foreach my $acct ( sort preferredAccountSorting @accounts) { } close(CHART_OUTPUT); die "error writing to chart-of-accounts.txt: $!" unless $? == 0; +my %commands = ( + 'totalEnd' => [ $LEDGER_CMD, @otherLedgerOpts, '-V', '-X', '$', + '-e', $endDate, '-F', '%-.80A %22.108t\n', '-s', + 'reg' ], + 'totalBegin' => [ $LEDGER_CMD, @otherLedgerOpts, '-V', '-X', '$', + '-e', $beginDate, '-F', '%-.80A %22.108t\n', + '-s', 'reg' ]); + +my %balanceData; + +foreach my $id (keys %commands) { + my(@command) = @{$commands{$id}}; + + open(FILE, "-|", @command) or die "unable to run command ledger command: @command: $!"; + + foreach my $line () { + die "Unable to parse output line from balance data $id command: $line" + unless $line =~ /^\s*([^\$]+)\s+\$\s*([\-\d\.\,]+)/; + my($account, $amount) = ($1, $2); + $amount = ParseNumber($amount); + $account =~ s/\s+$//; + next if $account =~ /\/ and (abs($amount) <= 0.02); + next if $account =~ /^Equity:/; # Stupid auto-account made by ledger. + $balanceData{$id}{$account} = $amount; + } + close FILE; + die "unable to run balance data ledger command, @command: $!" unless ($? == 0); +} open(GL_TEXT_OUT, ">", "general-ledger.txt") or die "unable to write general-ledger.txt: $!"; print MANIFEST "general-ledger.txt\n"; @@ -147,15 +175,20 @@ foreach my $acct (@sortedAccounts) { } close(GL_TEXT_DATA); die "error reading ledger output for chart of accounts: $!" unless $? == 0; - print GL_CSV_OUT "\n\"ACCOUNT:\",\"$acct\"\n\"PERIOD START:\",\"$beginDate\"\n\"PERIOD END:\",\"$formattedEndDate\"\n"; - print GL_CSV_OUT '"DATE","CHECK NUM","NAME","TRANSACTION AMT","RUNNING TOTAL"'; - my $formatString = '"%(date)","%C","%P","%t","%T"'; + print GL_CSV_OUT "\n\"ACCOUNT:\",\"$acct\"\n\"PERIOD START:\",\"$formattedBeginDate\"\n\"PERIOD END:\",\"$formattedEndDate\"\n"; + print GL_CSV_OUT '"DATE","CHECK NUM","NAME","TRANSACTION AMT","BALANCE"'; + + my $formatString = '"%(date)","%C","%P","%t",""'; foreach my $tagField (qw/Receipt Invoice Statement Contract PurchaseOrder Approval Check IncomeDistributionAnalysis CurrencyRate/) { print GL_CSV_OUT ',"', $tagField, '"'; $formatString .= ',"%(tag(\'' . $tagField . '\'))"'; } $formatString .= "\n"; print GL_CSV_OUT "\n"; + if ($acct =~ /^(Assets|Liabilities|Accrued|Unearned Income)/) { + $balanceData{totalBegin}{$acct} = $ZERO unless defined $balanceData{totalBegin}{$acct}; + print GL_CSV_OUT "\"$formattedBeginDate\"", ',"","BALANCE","","$', "$balanceData{totalBegin}{$acct}\"\n"; + } @acctLedgerOpts = ('-V', '-F', $formatString, '-w', '--sort', 'd', '-b', $beginDate, '-e', $endDate, @otherLedgerOpts, 'reg', $acct); open(GL_CSV_DATA, "-|", $LEDGER_CMD, @acctLedgerOpts) @@ -173,6 +206,11 @@ foreach my $acct (@sortedAccounts) { $manifest{$file} = $line; } } + if ($acct =~ /^(Assets|Liabilities|Accrued|Unearned Income)/) { + $balanceData{totalEnd}{$acct} = $ZERO unless defined $balanceData{totalEnd}{$acct}; + print GL_CSV_OUT "\"$formattedEndDate\"", ',"","BALANCE","","$', "$balanceData{totalEnd}{$acct}\"\n"; + } + close(GL_CSV_DATA); die "error reading ledger output for chart of accounts: $!" unless $? == 0; } close(GL_TEXT_OUT); die "error writing to general-ledger.txt: $!" unless $? == 0; From b04fbb1b7306ce202a015dd7587fe9c396e27103 Mon Sep 17 00:00:00 2001 From: "Bradley M. Kuhn" Date: Sat, 5 Jan 2013 17:39:12 -0500 Subject: [PATCH 15/20] First crack at an unpaid accruals report. --- .../unpaid-accruals-report.plx | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100755 contrib/non-profit-audit-reports/unpaid-accruals-report.plx diff --git a/contrib/non-profit-audit-reports/unpaid-accruals-report.plx b/contrib/non-profit-audit-reports/unpaid-accruals-report.plx new file mode 100755 index 00000000..3d4c9cb0 --- /dev/null +++ b/contrib/non-profit-audit-reports/unpaid-accruals-report.plx @@ -0,0 +1,84 @@ +#!/usr/bin/perl +# unpaid-acccurals-report.plx -*- Perl -*- + +# This report is designed to create what our accounts call a "Schedule of +# accounts payable". and "Schedule of accounts receivable". + + + +# Copyright (C) 2013 Bradley M. Kuhn +# +# This program gives you software freedom; you can copy, modify, convey, +# and/or redistribute it under the terms of the GNU 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 +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program in a file called 'GPLv3'. If not, write to the: +# Free Software Foundation, Inc., 51 Franklin St, Fifth Floor +# Boston, MA 02110-1301, USA. + +use strict; +use warnings; + +use Math::BigFloat; +use Date::Manip; + +my $LEDGER_CMD = "/usr/local/bin/ledger"; + +my $ACCT_WIDTH = 70; + +sub ParseNumber($) { + $_[0] =~ s/,//g; + return Math::BigFloat->new($_[0]); +} +Math::BigFloat->precision(-2); +my $ZERO = Math::BigFloat->new("0.00"); +my $TWO_CENTS = Math::BigFloat->new("0.02"); + +if (@ARGV < 2) { + print STDERR "usage: $0 \n"; + exit 1; +} +my($startDate, $endDate, @mainLedgerOptions) = @ARGV; + +my $err; +my $formattedEndDate = UnixDate(DateCalc(ParseDate($endDate), ParseDateDelta("- 1 day"), \$err), + "%Y/%m/%d"); +die "Date calculation error on $endDate" if ($err); +my $formattedStartDate = UnixDate(ParseDate($startDate), "%Y/%m/%d"); +die "Date calculation error on $startDate" if ($err); + +my(@ledgerOptions) = (@mainLedgerOptions, + '-V', '-X', '$', '-F', + '\"%(tag("Invoice"))\",\"%A\",\"%(date)\",\"%(payee)\",\"%22.108t\"\n', + '--limit', 'tag("Invoice") !~ /^\s*$/', 'reg'); + +my @possibleTypes = ('Accrued:Loans Receivable', 'Accrued:Accounts Payable', + 'Accrued:Accounts Receivable', 'Accrued:Expenses'); + + +foreach my $type (@possibleTypes) { + open(LEDGER_FUNDS, "-|", $LEDGER_CMD, @ledgerOptions, "/^$type/") + or die "Unable to run $LEDGER_CMD @ledgerOptions: $!"; + + while (my $line = ) { + next if $line =~ /"\"/; + die "Unable to parse output line $line from @ledgerOptions" + + unless $line =~ /^\s*"([^"]+)","([^"]+)","([^"]+)","([^"]+)","\s*\$\s*([\-\d\.\,]+)"\s*$/; + my($invoice, $account, $date, $payee, $amount) = ($1, $2, $3, $4, $5); + $amount = ParseNumber($amount); + } +} +############################################################################### +# +# Local variables: +# compile-command: "perl -c unpaid-accruals-report.plx" +# End: + From 39db5bbce77c3173b006755fc71ce95ba0e4041a Mon Sep 17 00:00:00 2001 From: "Bradley M. Kuhn" Date: Sun, 6 Jan 2013 08:21:35 -0500 Subject: [PATCH 16/20] Ordering of options had always been incorrect on this call; Fixed. --- contrib/non-profit-audit-reports/csv2ods.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contrib/non-profit-audit-reports/csv2ods.py b/contrib/non-profit-audit-reports/csv2ods.py index aded8e65..e6d33906 100755 --- a/contrib/non-profit-audit-reports/csv2ods.py +++ b/contrib/non-profit-audit-reports/csv2ods.py @@ -113,7 +113,7 @@ def main(): print 'csv:', options.csv print 'ods:', options.ods print 'ods:', options.encoding - csv2ods(options.csv, options.ods, options.verbose, options.encoding, options.skip_page_break) + csv2ods(options.csv, options.ods, options.encoding, options.verbose, options.skip_page_break) if __name__ == '__main__': main() From 2142a36c1c81bae4ea5bdb6fde65141e8dca5227 Mon Sep 17 00:00:00 2001 From: "Bradley M. Kuhn" Date: Sun, 6 Jan 2013 19:28:21 -0500 Subject: [PATCH 17/20] Completed report on unpaid accruals. --- .../unpaid-accruals-report.plx | 32 +++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/contrib/non-profit-audit-reports/unpaid-accruals-report.plx b/contrib/non-profit-audit-reports/unpaid-accruals-report.plx index 3d4c9cb0..f481e02f 100755 --- a/contrib/non-profit-audit-reports/unpaid-accruals-report.plx +++ b/contrib/non-profit-audit-reports/unpaid-accruals-report.plx @@ -55,14 +55,14 @@ my $formattedStartDate = UnixDate(ParseDate($startDate), "%Y/%m/%d"); die "Date calculation error on $startDate" if ($err); my(@ledgerOptions) = (@mainLedgerOptions, - '-V', '-X', '$', '-F', + '-V', '-X', '$', '-e', $endDate, '-F', '\"%(tag("Invoice"))\",\"%A\",\"%(date)\",\"%(payee)\",\"%22.108t\"\n', '--limit', 'tag("Invoice") !~ /^\s*$/', 'reg'); my @possibleTypes = ('Accrued:Loans Receivable', 'Accrued:Accounts Payable', 'Accrued:Accounts Receivable', 'Accrued:Expenses'); - +my %data; foreach my $type (@possibleTypes) { open(LEDGER_FUNDS, "-|", $LEDGER_CMD, @ledgerOptions, "/^$type/") or die "Unable to run $LEDGER_CMD @ledgerOptions: $!"; @@ -73,8 +73,34 @@ foreach my $type (@possibleTypes) { unless $line =~ /^\s*"([^"]+)","([^"]+)","([^"]+)","([^"]+)","\s*\$\s*([\-\d\.\,]+)"\s*$/; my($invoice, $account, $date, $payee, $amount) = ($1, $2, $3, $4, $5); - $amount = ParseNumber($amount); + $amount = ParseNumber($amount); + + push(@{$data{$type}{$invoice}{entries}}, { account => $account, date => $date, payee => $payee, amount => $amount}); + $data{$type}{$invoice}{total} = $ZERO unless defined $data{$type}{$invoice}{total}; + $data{$type}{$invoice}{total} += $amount; } + close LEDGER_FUNDS; + die "Failure on ledger command for $type: $!" unless ($? == 0); + +} +foreach my $type (keys %data) { + foreach my $invoice (keys %{$data{$type}}) { + delete $data{$type}{$invoice} if abs($data{$type}{$invoice}{total}) <= $TWO_CENTS; + } +} +foreach my $type (keys %data) { + delete $data{$type} if scalar(keys %{$data{$type}}) == 0; +} +foreach my $type (keys %data) { + print "\"SCHEDULE OF $type\"\n\"ENDING:\",\"$formattedEndDate\"\n\n", + '"DATE","PAYEE","ACCOUNT","AMOUNT","INVOICE"', "\n"; + foreach my $invoice (keys %{$data{$type}}) { + my $vals; + foreach my $vals (@{$data{$type}{$invoice}{entries}}) { + print "\"$vals->{date}\",\"$vals->{payee}\",\"$vals->{account}\",\"\$$vals->{amount}\",\"link:$invoice\"\n"; + } + } + print "pagebreak\n"; } ############################################################################### # From 6d98bc58ae555d5d4487e7ee90c6ce8b2a49cbe6 Mon Sep 17 00:00:00 2001 From: "Bradley M. Kuhn" Date: Sun, 6 Jan 2013 19:28:46 -0500 Subject: [PATCH 18/20] Correct sorting of output in trial balance report. --- contrib/non-profit-audit-reports/summary-reports.plx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/contrib/non-profit-audit-reports/summary-reports.plx b/contrib/non-profit-audit-reports/summary-reports.plx index bbe83c88..e9e1a3b8 100755 --- a/contrib/non-profit-audit-reports/summary-reports.plx +++ b/contrib/non-profit-audit-reports/summary-reports.plx @@ -430,10 +430,13 @@ my $curOn = 'Assets'; foreach my $account (sort preferredAccountSorting keys %fullAccountList) { # Blank lines right if ($account !~ /^$curOn/) { - print TRIAL "\n"; + print TRIAL "pagebreak\n"; $curOn = $account; - $curOn =~ s/^([^:]+):.*$/$1/; - print "CurOn now: $curOn\n"; + if ($curOn =~ /(Accrued:[^:]+):.*$/) { + $curOn = $1; + } else { + $curOn =~ s/^([^:]+):.*$/$1/; + } } if ($account =~ /^Assets|Liabilities|Accrued|Unearned Income/) { foreach my $id (qw/totalBeginFY totalEndFY amountInYear/) { From 8cddda4c3eb67234c12285fe52bd2bc328f6678b Mon Sep 17 00:00:00 2001 From: "Bradley M. Kuhn" Date: Sun, 6 Jan 2013 19:43:54 -0500 Subject: [PATCH 19/20] More flexible CSV -> ODS hyperlinks and pagebreaks; csv2ods.py produces MANIFEST. Previous version of csv2ods.py simply assumed that fields beyond five would have links to files. This obviously lacked flexibility and was a silly hard-code. Now, those CSV fields that have link:SOMETHING will cause a hyperlink to be created to SOMETHING. Meanwhile, the pagebreak support was similarly hard-coded. Now, any CSV field that has the word "pagebreak" in it will generate a pagebreak. The general ledger and cash receipts/disbursement journals have been modified to make use of these new features in csv2ods.py. Finally, the --skip-page-break option is now moot in csv2ods.py, so that is herein removed. --- ...ash-receipts-and-disbursments-journals.plx | 4 +- contrib/non-profit-audit-reports/csv2ods.py | 43 +++++++++++-------- .../general-ledger-report.plx | 6 ++- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/contrib/non-profit-audit-reports/cash-receipts-and-disbursments-journals.plx b/contrib/non-profit-audit-reports/cash-receipts-and-disbursments-journals.plx index 346e4064..2ad18a44 100755 --- a/contrib/non-profit-audit-reports/cash-receipts-and-disbursments-journals.plx +++ b/contrib/non-profit-audit-reports/cash-receipts-and-disbursments-journals.plx @@ -113,7 +113,7 @@ foreach my $acct (@accounts) { my $formatString = '\n"%(date)","%C","%P","%A","%t"\n%/"","","","%A","%t"'; foreach my $tagField (qw/Receipt Invoice Statement Contract PurchaseOrder Approval Check IncomeDistributionAnalysis CurrencyRate/) { print CSV_OUT ',"', $tagField, '"'; - $formatString .= ',"%(tag(\'' . $tagField . '\'))"'; + $formatString .= ',"link:%(tag(\'' . $tagField . '\'))"'; } $formatString .= "\n"; print CSV_OUT "\n"; @@ -130,7 +130,7 @@ foreach my $acct (@accounts) { open(CSV_DATA, "-|", $LEDGER_CMD, @csvRegLedgerOpts) or die "unable to run ledger command for $fileNameBase.csv: $!"; - while (my $line = ) { print CSV_OUT $line; } + while (my $line = ) { $line =~ s/"link:"/""/g; print CSV_OUT $line; } close(CSV_DATA); die "Error read from csv ledger command $!" unless $? == 0; SKIP_REGISTER_COMMANDS: diff --git a/contrib/non-profit-audit-reports/csv2ods.py b/contrib/non-profit-audit-reports/csv2ods.py index e6d33906..8b880648 100755 --- a/contrib/non-profit-audit-reports/csv2ods.py +++ b/contrib/non-profit-audit-reports/csv2ods.py @@ -24,14 +24,13 @@ import sys, os, os.path, optparse import csv import ooolib2 -file_fields = [ 'Receipt', 'Invoice', 'Statement', 'Contract', 'PurchaseOrder', - 'Approval', 'Check', 'IncomeDistributionAnalysis', 'CurrencyRate' ] - def err(msg): print 'error: %s' % msg sys.exit(1) -def csv2ods(csvname, odsname, encoding='', verbose = False, skip_page_break = False): +def csv2ods(csvname, odsname, encoding='', verbose = False): + filesSavedinManifest = {} + if verbose: print 'converting from %s to %s' % (csvname, odsname) doc = ooolib2.Calc() @@ -45,7 +44,7 @@ def csv2ods(csvname, odsname, encoding='', verbose = False, skip_page_break = Fa style_currency = doc.styles.get_next_style('cell') style_data = tuple([style]) doc.styles.style_config[style_data] = style_currency - + row = 1 csvdir = os.path.dirname(csvname) if len(csvdir) == 0: @@ -61,26 +60,39 @@ def csv2ods(csvname, odsname, encoding='', verbose = False, skip_page_break = Fa if len(val) > 0 and val[0] == '$': doc.set_cell_value(col + 1, row, 'currency', val[1:]) else: - if ((col >= 5) and (not val in file_fields) and len(val) > 0): + if (len(val) > 0 and val[0:5] == "link:"): + val = val[5:] linkrel = '../' + val # ../ means remove the name of the *.ods linkname = os.path.basename(val) # name is just the last component doc.set_cell_value(col + 1, row, 'link', (linkrel, linkname)) linkpath = csvdir + '/' + val + + if not val in filesSavedinManifest: + filesSavedinManifest[val] = col + + if not os.path.exists(linkpath): + print "WARNING: link %s DOES NOT EXIST at %s" % (val, linkpath) if verbose: if os.path.exists(linkpath): print 'relative link %s EXISTS at %s' % (val, linkpath) - else: - print 'relative link %s DOES NOT EXIST at %s' % (val, linkpath) else: - doc.set_cell_value(col + 1, row, 'string', val) + if val == "pagebreak": + doc.sheets[doc.sheet_index].set_sheet_config(('row', row), style_pagebreak) + else: + doc.set_cell_value(col + 1, row, 'string', val) else: # enter an empty string for blank lines doc.set_cell_value(1, row, 'string', '') - # put a pagebreak here - if not skip_page_break: - doc.sheets[doc.sheet_index].set_sheet_config(('row', row), style_pagebreak) row += 1 - # save the file + # save manifest file + if filesSavedinManifest.keys() != []: + manifestFH = open("MANIFEST", "a") + manifestFH.write("# Files from %s\n" % odsname) + for file in filesSavedinManifest.keys(): + manifestFH.write("%s\n" % file) + + manifestFH.close() + # Save spreadsheet file. doc.save(odsname) def main(): @@ -97,9 +109,6 @@ def main(): help='ods output filename') parser.add_option('-e', '--encoding', action='store', help='unicode character encoding type') - parser.add_option('-s', '--skip-page-break', action='store_true', - dest='skip_page_break', - help='do not add any page breaks') (options, args) = parser.parse_args() if len(args) != 0: parser.error("not expecting extra args") @@ -113,7 +122,7 @@ def main(): print 'csv:', options.csv print 'ods:', options.ods print 'ods:', options.encoding - csv2ods(options.csv, options.ods, options.encoding, options.verbose, options.skip_page_break) + csv2ods(options.csv, options.ods, options.encoding, options.verbose) if __name__ == '__main__': main() diff --git a/contrib/non-profit-audit-reports/general-ledger-report.plx b/contrib/non-profit-audit-reports/general-ledger-report.plx index 3b230837..1fd0e7ce 100755 --- a/contrib/non-profit-audit-reports/general-ledger-report.plx +++ b/contrib/non-profit-audit-reports/general-ledger-report.plx @@ -181,7 +181,7 @@ foreach my $acct (@sortedAccounts) { my $formatString = '"%(date)","%C","%P","%t",""'; foreach my $tagField (qw/Receipt Invoice Statement Contract PurchaseOrder Approval Check IncomeDistributionAnalysis CurrencyRate/) { print GL_CSV_OUT ',"', $tagField, '"'; - $formatString .= ',"%(tag(\'' . $tagField . '\'))"'; + $formatString .= ',"link:%(tag(\'' . $tagField . '\'))"'; } $formatString .= "\n"; print GL_CSV_OUT "\n"; @@ -195,12 +195,14 @@ foreach my $acct (@sortedAccounts) { or die "Unable to run $LEDGER_CMD @acctLedgerOpts: $!"; foreach my $line () { + $line =~ s/"link:"/""/g; print GL_CSV_OUT $line; next if $line =~ /ACCOUNT:.*PERIOD/; # Skip column header lines $line =~ s/^"[^"]*","[^"]*","[^"]*","[^"]*","[^"]*",//; while ($line =~ s/^"([^"]*)"(,|$)//) { my $file = $1; next if $file =~ /^\s*$/; + $file =~ s/^link:(.*)$/$1/; warn "$file does not exist and/or is not readable" unless -r $file; print MANIFEST "$file\n" if not defined $manifest{$file}; $manifest{$file} = $line; @@ -210,7 +212,7 @@ foreach my $acct (@sortedAccounts) { $balanceData{totalEnd}{$acct} = $ZERO unless defined $balanceData{totalEnd}{$acct}; print GL_CSV_OUT "\"$formattedEndDate\"", ',"","BALANCE","","$', "$balanceData{totalEnd}{$acct}\"\n"; } - + print GL_CSV_OUT "pagebreak\n"; close(GL_CSV_DATA); die "error reading ledger output for chart of accounts: $!" unless $? == 0; } close(GL_TEXT_OUT); die "error writing to general-ledger.txt: $!" unless $? == 0; From 9d78dc639593e5ae6f4ccbf7867131763df33dcd Mon Sep 17 00:00:00 2001 From: "Bradley M. Kuhn" Date: Sun, 6 Jan 2013 19:48:22 -0500 Subject: [PATCH 20/20] The restricted fund report has been almost entirely rewritten. The previous version was somewhat confusing anyway. Now it builds a relatively clear spreadsheet of all categories. It also now outputs CSV. --- .../non-profit-audit-reports/fund-report.plx | 221 ++++++++++++------ 1 file changed, 151 insertions(+), 70 deletions(-) diff --git a/contrib/non-profit-audit-reports/fund-report.plx b/contrib/non-profit-audit-reports/fund-report.plx index 0c03d009..ce59da96 100755 --- a/contrib/non-profit-audit-reports/fund-report.plx +++ b/contrib/non-profit-audit-reports/fund-report.plx @@ -1,9 +1,19 @@ #!/usr/bin/perl # fund-report.plx -*- Perl -*- # -# Script to generate a Trial Balance report for a ledger. +# Script to generate a Restricted Fund Report. Usefulness of this +# script may be confined to those who track separate funds in their +# accounts by having accounts that match this format: +# /^(Income|Expenses|Unearned Income|(Accrued:[^:]+:):PROJECTNAME/ + +# Conservancy does this because we carefully track fund balances for our +# fiscal sponsored projects. Those who aren't fiscal sponsors won't find +# this report all that useful, I suspect. Note that the name +# "Conservancy" is special-cased in a few places, mainly because our +# "General" fund is called "Conservancy". + # -# Copyright (C) 2011, 2012, Bradley M. Kuhn +# Copyright (C) 2011, 2012, 2013 Bradley M. Kuhn # # This program gives you software freedom; you can copy, modify, convey, # and/or redistribute it under the terms of the GNU General Public License @@ -36,6 +46,7 @@ sub ParseNumber($) { } Math::BigFloat->precision(-2); my $ZERO = Math::BigFloat->new("0.00"); +my $TWO_CENTS = Math::BigFloat->new("0.02"); if (@ARGV < 2) { print STDERR "usage: $0 \n"; @@ -45,71 +56,65 @@ my($startDate, $endDate, @mainLedgerOptions) = @ARGV; my $err; my $formattedEndDate = UnixDate(DateCalc(ParseDate($endDate), ParseDateDelta("- 1 day"), \$err), - "%B %e, %Y"); + "%Y/%m/%d"); die "Date calculation error on $endDate" if ($err); -my $formattedStartDate = UnixDate(ParseDate($startDate), "%B %e, %Y"); +my $formattedStartDate = UnixDate(ParseDate($startDate), "%Y/%m/%d"); die "Date calculation error on $startDate" if ($err); -# First, get fund list from ending balance -my(@ledgerOptions) = (@mainLedgerOptions, - '-V', '-X', '$', '-F', "%-.70A %22.108t\n", '-s', - '-e', $endDate, 'reg', '/^Funds:Restricted:/'); +# First, get balances for starting and ending for each fund + my %funds; -open(LEDGER_FUNDS, "-|", $LEDGER_CMD, @ledgerOptions) - or die "Unable to run $LEDGER_CMD for funds: $!"; +foreach my $type ('starting', 'ending') { + my(@ledgerOptions) = (@mainLedgerOptions, + '-V', '-X', '$', '-F', "%-.70A %22.108t\n", '-s'); -while (my $fundLine = ) { - die "Unable to parse output line from first funds command: \"$fundLine\"" - unless $fundLine =~ /^\s*([^\$]+)\s+\$\s*\s*([\-\d\.\,]+)/; - my($account, $amount) = ($1, $2); - $amount = ParseNumber($amount); - $account =~ s/\s+$//; - next if $account =~ /\/ and (abs($amount) <= 0.02); - die "Weird account found, $account with amount of $amount in first funds command\n" - unless $account =~ s/^\s*Funds:Restricted://; - $funds{$account}{ending} = $amount; + if ($type eq 'starting') { + push(@ledgerOptions, '-e', $startDate); + } else { + push(@ledgerOptions,'-e', $endDate); + } + push(@ledgerOptions, 'reg', '/^(Income|Expenses):([^:]+):/'); + + open(LEDGER_FUNDS, "-|", $LEDGER_CMD, @ledgerOptions) + or die "Unable to run $LEDGER_CMD @ledgerOptions: $!"; + + while (my $fundLine = ) { + die "Unable to parse output line from first funds command: \"$fundLine\"" + unless $fundLine =~ /^\s*([^\$]+)\s+\$\s*\s*([\-\d\.\,]+)/; + my($account, $amount) = ($1, $2); + $amount = ParseNumber($amount); + $account =~ s/\s+$//; + next if $account =~ /\/ and (abs($amount) <= $TWO_CENTS); + die "Weird account found, $account with amount of $amount in command: @ledgerOptions\n" + unless $account =~ s/^\s*(?:Income|Expenses):([^:]+)://; + $account = $1; + $account = 'General' if $account eq 'Conservancy'; # FIXME: this is a special case for Consrevancy + $funds{$account}{$type} += $amount; + } + close LEDGER_FUNDS; + die "Failure on ledger command @ledgerOptions: $!" unless ($? == 0); } -close LEDGER_FUNDS; - -# First, get fund list from starting balance -@ledgerOptions = (@mainLedgerOptions, - '-V', '-X', '$', '-F', "%-.70A %22.108t\n", '-w', '-s', - '-e', $startDate, 'reg', '^Funds:Restricted:'); - -open(LEDGER_FUNDS, "-|", $LEDGER_CMD, @ledgerOptions) - or die "Unable to run $LEDGER_CMD for funds: $!"; - -while (my $fundLine = ) { - die "Unable to parse output line from second funds command: $fundLine" - unless $fundLine =~ /^\s*([^\$]+)\s+\$\s*([\-\d\.\,]+)/; - my($account, $amount) = ($1, $2); - $amount = ParseNumber($amount); - $account =~ s/\s+$//; - next if $account =~ /\/ and (abs($amount) <= 0.02); - die "Weird account found, $account with amount of $amount in first second command\n" - unless $account =~ s/^\s*Funds:Restricted://; - $funds{$account}{starting} = $amount; -} -close LEDGER_FUNDS; - - foreach my $fund (keys %funds) { - $funds{$fund}{starting} = $ZERO if not defined $funds{$fund}{starting}; + foreach my $type (keys %{$funds{$fund}}) { + $funds{$fund}{$type} = $ZERO - $funds{$fund}{$type}; + } } - -@ledgerOptions = (@mainLedgerOptions, +my(@ledgerOptions) = (@mainLedgerOptions, '-V', '-X', '$', '-F', "%-.70A %22.108t\n", '-w', '-s', '-b', $startDate, '-e', $endDate, 'reg'); -my @possibleTypes = ('Unearned Income', 'Retained Earnings', 'Retained Costs', - 'Accrued:Accounts Payable', 'Accrued:Accounts Receivable'); +my @possibleTypes = ('Income', 'Expenses', 'Unearned Income', 'Retained Earnings', 'Retained Costs', + 'Accrued:Loans Receivable', 'Accrued:Accounts Payable', + 'Accrued:Accounts Receivable', 'Accrued:Expenses'); -foreach my $type ('Income', 'Expenses', @possibleTypes) { +foreach my $type (@possibleTypes) { foreach my $fund (keys %funds) { - open(LEDGER_INCOME, "-|", $LEDGER_CMD, @ledgerOptions, "^${type}:$fund") + my $query; + $query = ($fund eq 'General') ? "/^${type}:Conservancy/": "/^${type}:$fund/"; + open(LEDGER_INCOME, "-|", $LEDGER_CMD, @ledgerOptions, $query) or die "Unable to run $LEDGER_CMD for funds: $!"; - $funds{$fund}{$type} = $ZERO; + $funds{$fund}{$type} = $ZERO; while (my $line = ) { die "Unable to parse output line from $type line command: $line" unless $line =~ /^\s*([^\$]+)\s+\$\s*\s*([\-\d\.\,]+)/; @@ -118,34 +123,110 @@ foreach my $type ('Income', 'Expenses', @possibleTypes) { $funds{$fund}{$type} += $amount; } close LEDGER_INCOME; + die "Failure on ledger command for ${type}:$fund: $!" unless ($? == 0); } } -my($totStart, $totEnd) = ($ZERO, $ZERO); +my %tot; +($tot{Start}, $tot{End}) = ($ZERO, $ZERO); -foreach my $fund (sort keys %funds) { +my %beforeEndings = ('Income' => 1, 'Expenses' => 1); +my %afterEndings; + +# For other @possibleTypes, build up @fieldsList to just thoes that are present. + +foreach my $fund (keys %funds) { + foreach my $type (@possibleTypes) { + if ($funds{$fund}{$type} != $ZERO) { + if ($type =~ /^(Unearned Income|Accrued)/) { + $afterEndings{$type} = 1; + } else { + $beforeEndings{$type} = 1; + } + } + } +} +my(@beforeEndingFields, @afterEndingFields); + +foreach my $ii (@possibleTypes) { + push(@beforeEndingFields, $ii) if defined $beforeEndings{$ii}; + push(@afterEndingFields, $ii) if defined $afterEndings{$ii}; +} +# Make sure fieldLists present items are zero for those that should be zero. +foreach my $fund (keys %funds) { + foreach my $type ('starting', @beforeEndingFields, 'ending', @afterEndingFields) { + $funds{$fund}{$type} = $ZERO unless defined $funds{$fund}{$type}; + } +} + +print '"RESTRICTED AND GENERAL FUND REPORT",', "\"BEGINNING:\",\"$formattedStartDate\",\"ENDING:\",\"$formattedEndDate\"\n\n"; +print '"FUND","STARTING BALANCE",'; +my @finalPrints; +foreach my $type (@beforeEndingFields) { + $tot{$type} = $ZERO; + my $formattedType = $type; + print "\"$formattedType\","; +} +print '"ENDING BALANCE",""'; +foreach my $type (@afterEndingFields) { + $tot{$type} = $ZERO; + my $formattedType = $type; + $formattedType = "Prepaid Expenses" if $formattedType eq 'Accrued:Expenses'; + $formattedType =~ s/^Accrued://; + print ",\"$formattedType\""; +} +print "\n\n"; + +sub printTotal ($$) { + my($label, $tot) = @_; + print "\"$label\",\"\$$tot->{Start}\","; + foreach my $type (@beforeEndingFields) { + print "\"\$$tot->{$type}\","; + } + print "\"\$$tot->{End}\",\"\""; + foreach my $type (@afterEndingFields) { + print ",\"\$$tot->{$type}\""; + } + print "\n"; +} + +foreach my $fund (sort { + if ($a eq "General") { return 1 } + elsif ($b eq "General") { return -1 } + else { return $a cmp $b } } + keys %funds) { my $sanityTotal = $funds{$fund}{starting}; - print "Fund: $fund\n", sprintf("%-35s\$%26.2f\n\n", "Balance as of $formattedStartDate:", - $funds{$fund}{starting}); - foreach my $type ('Income', 'Expenses', @possibleTypes) { - my $formattedType = $type; $formattedType =~ s/^Accrued://; - next if $type ne 'Income' and $type ne 'Expenses' and $funds{$fund}{$type} == $ZERO; - print sprintf("%19s during period: \$%26.2f\n", $formattedType, $funds{$fund}{$type}); + + if ($fund eq 'General') { + print "\n"; + printTotal("Restricted Subtotal", \%tot); + print "\n"; } - print sprintf("\n%-35s\$%26.2f\n", "Balance as of $formattedEndDate:", - $funds{$fund}{ending}), "\n\n"; + $tot{Start} += $funds{$fund}{starting}; + $tot{End} += $funds{$fund}{ending}; + + print "\"$fund\",\"\$$funds{$fund}{starting}\","; + foreach my $type (@beforeEndingFields) { + print "\"\$$funds{$fund}{$type}\","; + $tot{$type} += $funds{$fund}{$type}; + } + print "\"\$$funds{$fund}{ending}\",\"\""; + foreach my $type (@afterEndingFields) { + print ",\"\$$funds{$fund}{$type}\""; + $tot{$type} += $funds{$fund}{$type}; + } + print "\n"; # Santity check: - if ($funds{$fund}{ending} != + if (abs($funds{$fund}{ending} - ($funds{$fund}{starting} - - $funds{$fund}{Income} - $funds{$fund}{'Unearned Income'} - $funds{$fund}{Expenses})) { - print "$fund FAILED SANITY CHECK\n\n\n"; - die "$fund FAILED SANITY CHECK"; + - $funds{$fund}{Income} - $funds{$fund}{Expenses})) + > $TWO_CENTS) { + print "$fund FAILED SANITY CHECK: Ending: $funds{$fund}{ending} \n\n\n"; + warn "$fund FAILED SANITY CHECK"; } - $totStart += $funds{$fund}{starting}; - $totEnd += $funds{$fund}{ending}; } -print "\n\n\nTotal Restricted Funds as of $formattedStartDate: ", sprintf("\$%15.2f\n", $totStart); -print "\nTotal Restricted Funds as of $formattedStartDate: ", sprintf("\$%15.2f\n", $totEnd); +print "\n"; +printTotal("OVERALL TOTAL", \%tot); ############################################################################### # # Local variables: