diff options
-rwxr-xr-x | contrib/non-profit-audit-reports/cash-receipts-and-disbursments-journals.plx | 147 | ||||
-rwxr-xr-x | contrib/non-profit-audit-reports/csv2ods.py | 14 | ||||
-rwxr-xr-x | contrib/non-profit-audit-reports/demo.sh | 4 | ||||
-rwxr-xr-x | contrib/non-profit-audit-reports/fund-report.plx | 154 | ||||
-rwxr-xr-x | contrib/non-profit-audit-reports/general-ledger-report.plx | 33 | ||||
-rwxr-xr-x | contrib/non-profit-audit-reports/summary-reports.plx | 355 | ||||
-rw-r--r-- | contrib/non-profit-audit-reports/tests/Financial/BankStuff/bank-statement.pdf | bin | 0 -> 3257 bytes | |||
-rw-r--r-- | contrib/non-profit-audit-reports/tests/Projects/Foo/earmark-record.txt | 1 | ||||
-rw-r--r-- | contrib/non-profit-audit-reports/tests/non-profit-test-data.ledger | 4 | ||||
-rw-r--r-- | doc/ledger3.texi | 2 | ||||
-rw-r--r-- | src/filters.cc | 3 | ||||
-rw-r--r-- | src/journal.cc | 13 |
12 files changed, 716 insertions, 14 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 new file mode 100755 index 00000000..346e4064 --- /dev/null +++ b/contrib/non-profit-audit-reports/cash-receipts-and-disbursments-journals.plx @@ -0,0 +1,147 @@ +#!/usr/bin/perl +# cash-receipts-and-disbursments-journals -*- Perl -*- +# +# Script to generate a General Ledger report that accountants like +# using Ledger. +# +# Copyright (C) 2011, 2012 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; +use File::Temp qw/tempfile/; + +my $LEDGER_CMD = "/usr/local/bin/ledger"; + +my $ACCT_WIDTH = 75; + +sub ParseNumber($) { + $_[0] =~ s/,//g; + return Math::BigFloat->new($_[0]); +} + +sub LedgerAcctToFilename($) { + my $x = $_[0]; + $x =~ s/ /-/g; + $x =~ s/:/-/g; + return $x; +} + +Math::BigFloat->precision(-2); +my $ZERO = Math::BigFloat->new("0.00"); + +if (@ARGV < 2) { + print STDERR "usage: $0 <BEGIN_DATE> <END_DATE> <OTHER_LEDGER_OPTS>\n"; + exit 1; +} + +my($beginDate, $endDate, @otherLedgerOpts) = @ARGV; + +my(@chartOfAccountsOpts) = ('-V', '-F', "%150A\n", '-w', '-s', + '-b', $beginDate, '-e', $endDate, @otherLedgerOpts, 'reg'); + +open(CHART_DATA, "-|", $LEDGER_CMD, @chartOfAccountsOpts) + or die "Unable to run $LEDGER_CMD @chartOfAccountsOpts: $!"; + +my @accounts; +while (my $line = <CHART_DATA>) { + chomp $line; + $line =~ s/^\s*//; $line =~ s/\s*$//; + push(@accounts, $line); + +} +close(CHART_DATA); die "error reading ledger output for chart of accounts: $!" 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"); + +foreach my $acct (@accounts) { + next unless ($acct =~ /^(?:Assets|Liabilities)/); + + my $acctFilename = LedgerAcctToFilename($acct); + + foreach my $typeData ({ name => 'disbursements', query => 'a<=0' }, + { name => 'receipts', query => 'a>0' }) { + my $fileNameBase = $acctFilename . '-' . $typeData->{name}; + + open(TEXT_OUT, ">", "$fileNameBase.txt") or die "unable to open $fileNameBase.txt: $!"; + open(CSV_OUT, ">", "$fileNameBase.csv") or die "unable to open $fileNameBase.csv: $!"; + + print TEXT_OUT "\n\nACCOUNT: $acct\nFROM: $beginDate TO $formattedEndDate\n\n"; + print CSV_OUT "\n\"ACCOUNT:\",\"$acct\"\n\"PERIOD START:\",\"$beginDate\"\n\"PERIOD END:\",\"$formattedEndDate\"\n"; + print CSV_OUT '"DATE","CHECK NUM","NAME","ACCOUNT","AMOUNT"'; + + my @entryLedgerOpts = ('-l', $typeData->{query}, + '-b', $beginDate, '-e', $endDate, @otherLedgerOpts, 'print', $acct); + + open(ENTRY_DATA, "-|", $LEDGER_CMD, @entryLedgerOpts) + or die "Unable to run $LEDGER_CMD @entryLedgerOpts: $!"; + + my($tempFH, $tempFile) = tempfile("cashreportsXXXXXXXX", TMPDIR => 1); + + while (my $line = <ENTRY_DATA>) { print $tempFH $line; } + close(ENTRY_DATA); die "Error reading ledger output for entries: $!" unless $? == 0; + $tempFH->close() or die "Error writing ledger output for entries to temp file, $tempFile: $!"; + + goto SKIP_REGISTER_COMMANDS if (-z $tempFile); + + my @txtRegLedgerOpts = ('-f', $tempFile, '-V', '-F', + "%(date) %-.70P %-.10C %-.80A %18t\n", '-w', '--sort', 'd', + '-b', $beginDate, '-e', $endDate, 'reg'); + + 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 .= "\n"; + print CSV_OUT "\n"; + my @csvRegLedgerOpts = ('-f', $tempFile, '-V', '-F', $formatString, '-w', '--sort', 'd', + '-b', $beginDate, '-e', $endDate, 'reg'); + + + open(TXT_DATA, "-|", $LEDGER_CMD, @txtRegLedgerOpts) + or die "unable to run ledger command for $fileNameBase.txt: $!"; + + while (my $line = <TXT_DATA>) { print TEXT_OUT $line; } + close(TEXT_OUT); die "Error read write text out to $fileNameBase.txt: $!" unless $? == 0; + + open(CSV_DATA, "-|", $LEDGER_CMD, @csvRegLedgerOpts) + or die "unable to run ledger command for $fileNameBase.csv: $!"; + + while (my $line = <CSV_DATA>) { print CSV_OUT $line; } + close(CSV_DATA); die "Error read from csv ledger command $!" unless $? == 0; + + SKIP_REGISTER_COMMANDS: + close(TXT_DATA); die "Error read from txt ledger command $!" unless $? == 0; + close(CSV_OUT); die "Error read write csv out to $fileNameBase.csv: $!" unless $? == 0; + unlink($tempFile); + } +} +############################################################################### +# +# Local variables: +# compile-command: "perl -c cash-receipts-and-disbursments-journals.plx" +# End: + diff --git a/contrib/non-profit-audit-reports/csv2ods.py b/contrib/non-profit-audit-reports/csv2ods.py index f6150158..2b3024d4 100755 --- a/contrib/non-profit-audit-reports/csv2ods.py +++ b/contrib/non-profit-audit-reports/csv2ods.py @@ -24,11 +24,14 @@ 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, verbose = False): +def csv2ods(csvname, odsname, encoding='', verbose = False): if verbose: print 'converting from %s to %s' % (csvname, odsname) doc = ooolib2.Calc() @@ -53,10 +56,12 @@ def csv2ods(csvname, odsname, verbose = False): if len(fields) > 0: for col in range(len(fields)): val = fields[col] + if encoding != '': + val = unicode(val, 'utf8') if len(val) > 0 and val[0] == '$': doc.set_cell_value(col + 1, row, 'currency', val[1:]) else: - if ( (col == 5) and (val != 'Receipt') and len(val) > 0) or ( (col == 6) and (val != 'Invoice') and len(val) > 0): + if ((col >= 5) and (not val in file_fields) and len(val) > 0): 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)) @@ -89,6 +94,8 @@ def main(): help='csv file to process') parser.add_option('-o', '--ods', action='store', help='ods output filename') + parser.add_option('-e', '--encoding', action='store', + help='unicode character encoding type') (options, args) = parser.parse_args() if len(args) != 0: parser.error("not expecting extra args") @@ -101,7 +108,8 @@ def main(): print '%s: verbose mode on' % program print 'csv:', options.csv print 'ods:', options.ods - csv2ods(options.csv, options.ods, options.verbose) + print 'ods:', options.encoding + csv2ods(options.csv, options.ods, options.verbose, options.encoding) if __name__ == '__main__': main() diff --git a/contrib/non-profit-audit-reports/demo.sh b/contrib/non-profit-audit-reports/demo.sh index 6a9dcadf..a4b837a6 100755 --- a/contrib/non-profit-audit-reports/demo.sh +++ b/contrib/non-profit-audit-reports/demo.sh @@ -32,11 +32,13 @@ else exit 1 fi +echo general-ledger.ods >> MANIFEST + # create a portable zip file with the spreadsheet # and the linked artifacts echo creating portable zipfile... -zip -r ../general-ledger.zip general-ledger.ods Financial Projects -x '*.txt' +cat MANIFEST | zip -@ ../general-ledger.zip echo " " echo "created general-ledger.zip" diff --git a/contrib/non-profit-audit-reports/fund-report.plx b/contrib/non-profit-audit-reports/fund-report.plx new file mode 100755 index 00000000..0c03d009 --- /dev/null +++ b/contrib/non-profit-audit-reports/fund-report.plx @@ -0,0 +1,154 @@ +#!/usr/bin/perl +# fund-report.plx -*- Perl -*- +# +# Script to generate a Trial Balance report for a ledger. +# +# Copyright (C) 2011, 2012, 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"); + +if (@ARGV < 2) { + print STDERR "usage: $0 <START_DATE> <END_DATE> <LEDGER_OPTIONS>\n"; + exit 1; +} +my($startDate, $endDate, @mainLedgerOptions) = @ARGV; + +my $err; +my $formattedEndDate = UnixDate(DateCalc(ParseDate($endDate), ParseDateDelta("- 1 day"), \$err), + "%B %e, %Y"); +die "Date calculation error on $endDate" if ($err); +my $formattedStartDate = UnixDate(ParseDate($startDate), "%B %e, %Y"); +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:/'); +my %funds; + +open(LEDGER_FUNDS, "-|", $LEDGER_CMD, @ledgerOptions) + or die "Unable to run $LEDGER_CMD for funds: $!"; + +while (my $fundLine = <LEDGER_FUNDS>) { + 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 =~ /\<Adjustment\>/ 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; +} +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 = <LEDGER_FUNDS>) { + 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 =~ /\<Adjustment\>/ 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}; +} + +@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'); + +foreach my $type ('Income', 'Expenses', @possibleTypes) { + foreach my $fund (keys %funds) { + open(LEDGER_INCOME, "-|", $LEDGER_CMD, @ledgerOptions, "^${type}:$fund") + or die "Unable to run $LEDGER_CMD for funds: $!"; + $funds{$fund}{$type} = $ZERO; + while (my $line = <LEDGER_INCOME>) { + die "Unable to parse output line from $type line command: $line" + unless $line =~ /^\s*([^\$]+)\s+\$\s*\s*([\-\d\.\,]+)/; + my($account, $amount) = ($1, $2); + $amount = ParseNumber($amount); + $funds{$fund}{$type} += $amount; + } + close LEDGER_INCOME; + } +} + +my($totStart, $totEnd) = ($ZERO, $ZERO); + +foreach my $fund (sort 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}); + } + print sprintf("\n%-35s\$%26.2f\n", "Balance as of $formattedEndDate:", + $funds{$fund}{ending}), "\n\n"; + # Santity check: + if ($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"; + } + $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); +############################################################################### +# +# Local variables: +# compile-command: "perl -c fund-report.plx" +# End: + diff --git a/contrib/non-profit-audit-reports/general-ledger-report.plx b/contrib/non-profit-audit-reports/general-ledger-report.plx index 5286d625..d1c92975 100755 --- a/contrib/non-profit-audit-reports/general-ledger-report.plx +++ b/contrib/non-profit-audit-reports/general-ledger-report.plx @@ -44,20 +44,20 @@ if (@ARGV < 3) { print STDERR "usage: $0 <BEGIN_DATE> <END_DATE> <OTHER_LEDGER_OPTS>\n"; exit 1; } +open(MANIFEST, ">", "MANIFEST") or die "Unable to open MANIFEST for writing: $!"; my($beginDate, $endDate, @otherLedgerOpts) = @ARGV; -my(@chartOfAccountsOpts) = ('-F', "%150A\n", '-w', '-s', +my(@chartOfAccountsOpts) = ('-V', '-F', "%150A\n", '-w', '-s', '-b', $beginDate, '-e', $endDate, @otherLedgerOpts, 'reg'); open(CHART_DATA, "-|", $LEDGER_CMD, @chartOfAccountsOpts) or die "Unable to run $LEDGER_CMD @chartOfAccountsOpts: $!"; -open(CHART_OUTPUT, ">", "chart-of-accounts.txt") or die "unable to write chart-of-accounts.txt: $!"; - my @accounts; while (my $line = <CHART_DATA>) { chomp $line; + next if $line =~ /^\s*\<\s*Adjustment\s*\>\s*$/; $line =~ s/^\s*//; $line =~ s/\s*$//; push(@accounts, $line); @@ -65,6 +65,7 @@ while (my $line = <CHART_DATA>) { 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"; my @sortedAccounts; foreach my $acct ( @@ -91,11 +92,14 @@ $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"; open(GL_CSV_OUT, ">", "general-ledger.csv") or die "unable to write general-ledger.csv: $!"; +print MANIFEST "general-ledger.csv\n"; +my %manifest; foreach my $acct (@sortedAccounts) { print GL_TEXT_OUT "\n\nACCOUNT: $acct\nFROM: $beginDate TO $formattedEndDate\n\n"; - my @acctLedgerOpts = ('-F', + my @acctLedgerOpts = ('-V', '-F', "%(date) %-.10C %-.80P %-.80N %18t %18T\n", '-w', '--sort', 'd', '-b', $beginDate, '-e', $endDate, @otherLedgerOpts, 'reg', $acct); open(GL_TEXT_DATA, "-|", $LEDGER_CMD, @acctLedgerOpts) @@ -107,13 +111,30 @@ 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","Receipt","Invoice"', "\n"; - @acctLedgerOpts = ('-F', '"%(date)","%C","%P","%t","%T","%(tag(\'Receipt\'))","%(tag(\'Invoice\'))"\n', '-w', '--sort', 'd', '-b', $beginDate, '-e', $endDate, @otherLedgerOpts, 'reg', $acct); + print GL_CSV_OUT '"DATE","CHECK NUM","NAME","TRANSACTION AMT","RUNNING TOTAL"'; + my $formatString = '"%(date)","%C","%P","%t","%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"; + + @acctLedgerOpts = ('-V', '-F', $formatString, '-w', '--sort', 'd', '-b', $beginDate, '-e', $endDate, @otherLedgerOpts, 'reg', $acct); open(GL_CSV_DATA, "-|", $LEDGER_CMD, @acctLedgerOpts) or die "Unable to run $LEDGER_CMD @acctLedgerOpts: $!"; foreach my $line (<GL_CSV_DATA>) { 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*$/; + 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; + } } close(GL_CSV_DATA); die "error reading ledger output for chart of accounts: $!" unless $? == 0; } diff --git a/contrib/non-profit-audit-reports/summary-reports.plx b/contrib/non-profit-audit-reports/summary-reports.plx new file mode 100755 index 00000000..7d2267d6 --- /dev/null +++ b/contrib/non-profit-audit-reports/summary-reports.plx @@ -0,0 +1,355 @@ +#!/usr/bin/perl +# fund-report.plx -*- Perl -*- +# +# Script to generate end-of-year summary reports. +# +# Copyright (C) 2011, 2012, 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 $VERBOSE = 0; +my $DEBUG = 0; + +my $LEDGER_BIN = "/usr/local/bin/ledger"; + +my $ACCT_WIDTH = 70; + +sub Commify ($) { + my $text = reverse $_[0]; + $text =~ s/(\d\d\d)(?=\d)(?!\d*\.)/$1,/g; + return scalar reverse $text; +} + +sub ParseNumber($) { + $_[0] =~ s/,//g; + return Math::BigFloat->new($_[0]); +} +Math::BigFloat->precision(-2); +my $ZERO = Math::BigFloat->new("0.00"); +my $ONE_PENNY = Math::BigFloat->new("0.01"); + +if (@ARGV < 2) { + print STDERR "usage: $0 <START_DATE> <END_DATE> <LEDGER_OPTIONS>\n"; + exit 1; +} +my($startDate, $endDate, @mainLedgerOptions) = @ARGV; + +my $err; +my $formattedEndDate = UnixDate(DateCalc(ParseDate($endDate), ParseDateDelta("- 1 day"), \$err), + "%B %e, %Y"); +die "Date calculation error on $endDate" if ($err); +my $formattedStartDate = UnixDate(ParseDate($startDate), "%B %e, %Y"); +die "Date calculation error on $startDate" if ($err); + +my %reportFields = + ('Cash' => { args => [ '-e', $endDate, 'bal', '/^Assets/' ] }, + 'Accounts Receivable' => {args => [ '-e', $endDate, 'bal', '/^Accrued:Accounts Receivable/' ]}, + 'Loans Receivable' => {args => [ '-e', $endDate, 'bal', '/^Accrued:Loans Receivable/' ]}, + 'Accounts Payable' => {args => [ '-e', $endDate, 'bal', '/^Accrued.*Accounts Payable/' ]}, + 'Accrued Expenses' => {args => [ '-e', $endDate, 'bal', '/^Accrued.*Expenses/' ]}, + 'Liabilities, Credit Cards' => {args => [ '-e', $endDate, 'bal', '/^Liabilities:Credit Card/' ]}, + 'Liabilities, Other' => {args => [ '-e', $endDate, 'bal', '/^Liabilities/', + 'and', 'not', '/^Liabilities:Credit Card/']}, + 'Unearned Income, Conference Registration' => {args => [ '-e', $endDate, 'bal', + '/^Unearned Income.*Conf.*Reg/' ]}, + 'Unearned Income, Other' => {args => [ '-e', $endDate, 'bal', '/^Unearned Income/', 'and', 'not', + '/^Unearned Income.*Conf.*Reg/' ]}, + 'Unrestricted Net Assets' => {args => [ '-e', $endDate, 'bal', '/^(Income|Expenses):Conservancy/' ]}, + 'Temporarily Restricted Net Assets' => {args => [ '-e', $endDate, 'bal', '/^(Income|Expenses)/', + 'and', 'not', '/^(Unearned Income|(Income|Expenses):Conservancy)/' ]}, + 'Total Net Assets' => {args => [ '-e', $endDate, 'bal', '/^(Income|Expenses)/' ]}, + +); +foreach my $item (keys %reportFields) { + my(@fullCommand) = ($LEDGER_BIN, @mainLedgerOptions, + '-V', '-X', '$', '-S', 'T', '-s', '-d', 'T', @{$reportFields{$item}{args}}); + open(FILE, "-|", @fullCommand) + or die "unable to run command ledger command: @fullCommand: $!"; + + my $foundBalance; + my $seenTotalLine = 0; + + print STDERR ($VERBOSE ? "Running: @fullCommand\n" : "."); + print STDERR " Output of @fullCommand\n" if $DEBUG; + + while (my $line = <FILE>) { + print STDERR $line if ($DEBUG); + + $seenTotalLine = 1 if $line =~ /^\s*\-+\s*/; # Skip lines until the total line + $foundBalance = $1 + if (not $seenTotalLine and $line =~ /^\s*[^0-9\-]+\s*([\-\d,\.]+)\s+/); + + if ($line =~ /^\s*\$\s*([\-\d,\.]+)\s*$/) { + $foundBalance = $1; + last; + } + } + close FILE; + die "problem running ledger command: @fullCommand: $!" unless ($? == 0); + if (not defined $foundBalance) { + $foundBalance = $ZERO; + } else { + $foundBalance =~ s/,//g; + $foundBalance = Math::BigFloat->new($foundBalance); + } + $foundBalance = $ZERO if not defined $foundBalance; + $reportFields{$item}{total} = abs($foundBalance); + 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: $!"; + +print BALANCE_SHEET " BALANCE SHEET\n", + " Ending ", $formattedEndDate, "\n", + "\n\nASSETS\n\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; + print BALANCE_SHEET sprintf($formatStr, "$item:", Commify($reportFields{$item}{total})); + $tot += $reportFields{$item}{total}; +} +print BALANCE_SHEET "\n", sprintf($formatStrTotal, "TOTAL ASSETS", Commify($tot)), "\n\nLIABILITIES\n\n"; + +my $totLiabilities = $ZERO; +foreach my $item ('Accounts Payable', 'Accrued Expenses', + 'Liabilities, Credit Cards', 'Liabilities, Other', + 'Unearned Income, Conference Registration', 'Unearned Income, Other') { + next if $reportFields{$item}{total} == $ZERO; + print BALANCE_SHEET sprintf($formatStr, "$item:", Commify($reportFields{$item}{total})); + $totLiabilities += $reportFields{$item}{total}; +} +print BALANCE_SHEET "\n", sprintf($formatStr, "TOTAL LIABILTIES", Commify($totLiabilities)), + "\n\nNET ASSETS\n\n"; + +my $totNetAssets = $ZERO; +foreach my $item ('Unrestricted Net Assets', 'Temporarily Restricted Net Assets') { + next if $reportFields{$item}{total} == $ZERO; + print BALANCE_SHEET sprintf($formatStr, "$item:", Commify($reportFields{$item}{total})); + $totNetAssets += $reportFields{$item}{total}; +} +print BALANCE_SHEET "\n", sprintf($formatStr, "TOTAL NET ASSETS", Commify($totNetAssets)), "\n\n", + sprintf($formatStrTotal, "TOTAL LIABILITIES AND NET ASSETS", + Commify($totNetAssets + $totLiabilities)); + +close BALANCE_SHEET; +print STDERR "\n"; +die "unable to write to balance-sheet.txt: $!" unless ($? == 0); + +die "Cash+accounts receivable total does not equal net assets and liabilities total" + if (abs( ($reportFields{'Cash'}{total} + $reportFields{'Accounts Receivable'}{total} + + $reportFields{'Loans Receivable'}{total})) - + abs($reportFields{'Accounts Payable'}{total} + + $reportFields{'Accrued Expenses'}{total} + + $reportFields{'Unearned Income, Conference Registration'}{total} + + $reportFields{'Unearned Income, Other'}{total} + + $reportFields{'Liabilities, Credit Cards'}{total} + + $reportFields{'Liabilities, Other'}{total} + + $reportFields{'Total Net Assets'}{total}) > $ONE_PENNY); + +die "Total net assets doesn't equal sum of restricted and unrestricted ones!" + if (abs($reportFields{'Total Net Assets'}{total}) - + abs($reportFields{'Unrestricted Net Assets'}{total} + + $reportFields{'Temporarily Restricted Net Assets'}{total}) > $ONE_PENNY); + + +my %incomeGroups = ('INTEREST INCOME' => { args => ['/^Income.*Interest/' ] }, + 'DONATIONS' => { args => [ '/^Income.*Donation/' ] }, + 'BOOK ROYALTIES & AFFILIATE PROGRAMS' => + { args => [ '/^Income.*(Royalt|Affilate)/' ] }, + 'CONFERENCES, REGISTRATION' => {args => [ '/^Income.*Conf.*Reg/' ] }, + 'CONFERENCES, RELATED BUSINESS INCOME' => { args => [ '/^Income.*(Booth|RBI)/'] }, + 'LICENSE ENFORCEMENT' => { args => [ '/^Income.*Enforce/' ]}, + 'TRADEMARKS' => {args => [ '/^Income.*Trademark/' ]}, + 'ADVERSITING' => {args => [ '/^Income.*Advertising/' ]}); + +my @otherArgs; +foreach my $type (keys %incomeGroups) { + @otherArgs = ("/^Income/") if @otherArgs == 0; + push(@otherArgs, 'and', 'not', @{$incomeGroups{$type}{args}}); +} +$incomeGroups{"OTHER"}{args} = \@otherArgs; +$incomeGroups{"TOTAL"}{args} = ['/^Income/']; + +open(INCOME, ">", "income.txt") or die "unable to open income.txt for writing: $!"; + +foreach my $type (keys %incomeGroups) { + my(@fullCommand) = ($LEDGER_BIN, @mainLedgerOptions, '-V', '-X', '$', + '-b', $startDate, '-e', $endDate, + '-F', '%-.80A %22.108t\n', '-s', + 'reg', @{$incomeGroups{$type}{args}}); + + open(FILE, "-|", @fullCommand) + or die "unable to run command ledger command: @fullCommand: $!"; + + print STDERR ($VERBOSE ? "Running: @fullCommand\n" : "."); + + $incomeGroups{$type}{total} = $ZERO; + $incomeGroups{$type}{output} = ""; + + foreach my $line (<FILE>) { + 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 =~ /\<Adjustment\>/ and (abs($amount) <= 0.02); + die "Weird account found, $account with amount of $amount in income command\n" + unless $account =~ s/^\s*Income://; + + $incomeGroups{$type}{total} += $amount; + $incomeGroups{$type}{output} .= " $line"; + } +} +print INCOME " INCOME\n", + " Between $formattedStartDate and $formattedEndDate\n\n"; + + +my $overallTotal = $ZERO; + +$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", + $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); + +die "calculated total of $overallTotal does equal $incomeGroups{TOTAL}{total}" + if (abs($overallTotal) - abs($incomeGroups{TOTAL}{total}) > $ONE_PENNY); + +print STDERR "\n"; + +my %expenseGroups = ('BANKING FEES' => { regex => '^Expenses.*(Banking Fees|Currency Conversion)' }, + 'COMPUTING, HOSTING AND EQUIPMENT' => { regex => '^Expenses.*(Hosting|Computer Equipment)' }, + 'CONFERENCES' => { regex => '^Expenses.*(Conferences|Sprint)' }, + 'DEVELOPER MENTORING' => {regex => '^Expenses.*Mentor' }, + 'LICENSE ENFORCEMENT' => { regex => '^Expenses.*Enforce' }, + 'ACCOUNTING' => { regex => '^Expenses.*(Accounting|Annual Audit)' }, + 'PAYROLL' => { regex => '^Expenses.*Payroll' }, + 'OFFICE' => { regex => '^Expenses.*(Office|Phones)' }, + 'RENT' => { regex => '^Expenses.*Rent' }, + 'SOFTWARE DEVELOPMENT' => { regex => '^Expenses.*Development' }, + 'OTHER PROGRAM ACTIVITY' => {regex => '^Expenses.*Gould' }, + 'ADVOCACY AND PROMOTION' => {regex => '^Expenses.*(Slipstream|Advocacy Merchandise|Promotional)' }, + 'ADVERSITING' => {regex => '^Expenses.*Advertising' }); + +foreach my $type (keys %expenseGroups, 'TRAVEL') { + $expenseGroups{$type}{total} = $ZERO; + $expenseGroups{$type}{output} = ""; +} + +open(EXPENSE, ">", "expense.txt") or die "unable to open expense.txt for writing: $!"; + +my(@fullCommand) = ($LEDGER_BIN, @mainLedgerOptions, '-V', '-X', '$', + '-b', $startDate, '-e', $endDate, + '-F', '%-.80A %22.108t\n', '-s', + 'reg', '/^Expenses/'); + +open(FILE, "-|", @fullCommand) + or die "unable to run command ledger command: @fullCommand: $!"; + +print STDERR ($VERBOSE ? "Running: @fullCommand\n" : "."); + +my $firstTotal = $ZERO; +foreach my $line (<FILE>) { + 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 =~ /\<Adjustment\>/ and (abs($amount) <= 0.02); + die "Weird account found, $account, with amount of $amount in expenses command\n" + unless $account =~ /^\s*Expenses:/; + + my $taken = 0; + # Note: Prioritize to put things under conference expenses if they were for a conference. + foreach my $type ('CONFERENCES', keys %expenseGroups) { + last if $taken; + next if $type eq 'TRAVEL' or $type eq 'OTHER'; + next unless $line =~ /$expenseGroups{$type}{regex}/; + $taken = 1; + $expenseGroups{$type}{total} += $amount; + $expenseGroups{$type}{output} .= " $line"; + } + if (not $taken) { + if ($account =~ /Travel/) { + $expenseGroups{'TRAVEL'}{total} += $amount; + $expenseGroups{'TRAVEL'}{output} .= " $line"; + } else { + $expenseGroups{'OTHER'}{total} += $amount; + $expenseGroups{'OTHER'}{output} .= " $line"; + } + } + $firstTotal += $amount; +} +print EXPENSE " EXPENSES\n", + " Between $formattedStartDate and $formattedEndDate\n\n"; +$overallTotal = $ZERO; +$formatStrTotal = "%-90s \$%14s\n"; + +my %verifyAllGroups; +foreach my $key (keys %expenseGroups) { + $verifyAllGroups{$key} = 1; +} +foreach my $type ('PAYROLL', 'SOFTWARE DEVELOPMENT', 'LICENSE ENFORCEMENT', 'CONFERENCES', + 'DEVELOPER MENTORING', 'TRAVEL', 'BANKING FEES', 'ADVOCACY AND PROMOTION', + 'COMPUTING, HOSTING AND EQUIPMENT', 'ACCOUNTING', + 'OFFICE', 'RENT', 'ADVERSITING', 'OTHER PROGRAM ACTIVITY', 'OTHER') { + delete $verifyAllGroups{$type}; + + 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", + $expenseGroups{$type}{output}, "\n", + sprintf($formatStrTotal, "TOTAL $type:", Commify($expenseGroups{$type}{total})); + $overallTotal += $expenseGroups{$type}{total}; +} + +print EXPENSE "\n\n\n", sprintf($formatStrTotal, "OVERALL TOTAL:", Commify($overallTotal)); + +close EXPENSE; die "unable to write to expense.txt: $!" unless ($? == 0); + +die "GROUPS NOT INCLUDED : ", join(keys(%verifyAllGroups), ", "), "\n" + unless (keys %verifyAllGroups == 0); + +die "calculated total of $overallTotal does equal $firstTotal" + if (abs($overallTotal) - abs($firstTotal) > $ONE_PENNY); + +print STDERR "\n"; + +############################################################################### +# +# Local variables: +# compile-command: "perl -c summary-reports.plx" +# End: diff --git a/contrib/non-profit-audit-reports/tests/Financial/BankStuff/bank-statement.pdf b/contrib/non-profit-audit-reports/tests/Financial/BankStuff/bank-statement.pdf Binary files differnew file mode 100644 index 00000000..27b40353 --- /dev/null +++ b/contrib/non-profit-audit-reports/tests/Financial/BankStuff/bank-statement.pdf diff --git a/contrib/non-profit-audit-reports/tests/Projects/Foo/earmark-record.txt b/contrib/non-profit-audit-reports/tests/Projects/Foo/earmark-record.txt new file mode 100644 index 00000000..c5ac98ac --- /dev/null +++ b/contrib/non-profit-audit-reports/tests/Projects/Foo/earmark-record.txt @@ -0,0 +1 @@ +I, Another J. Donor, would like $400 to be earmarked for Foo! diff --git a/contrib/non-profit-audit-reports/tests/non-profit-test-data.ledger b/contrib/non-profit-audit-reports/tests/non-profit-test-data.ledger index 69aeb571..fb6134ff 100644 --- a/contrib/non-profit-audit-reports/tests/non-profit-test-data.ledger +++ b/contrib/non-profit-audit-reports/tests/non-profit-test-data.ledger @@ -5,8 +5,9 @@ Assets:Checking $100.00 -2011/03/15 A Later Donation to Project Foo +2011/03/15 Another J. Donor Income:Foo:Donation $-400.00 + ;Approval: Projects/Foo/earmark-record.txt Assets:Checking $400.00 2011/04/20 (1) Baz Hosting Services, LLC @@ -24,3 +25,4 @@ ;Receipt: Projects/Blah/Expenses/hosting/AprilHostingReceipt.pdf ;Invoice: Projects/Blah/Expenses/hosting/april-invoice.pdf Assets:Checking $-250.00 + ;Statement: Financial/BankStuff/bank-statement.pdf diff --git a/doc/ledger3.texi b/doc/ledger3.texi index caec7667..b8aaa49b 100644 --- a/doc/ledger3.texi +++ b/doc/ledger3.texi @@ -2182,7 +2182,7 @@ This is a synonym for @code{comment} and must be closed by an @code{end} tag. @item year @c instance_t::year_directive in textual.cc Denotes the year used for all subsequent transactions that give a date -without a year. The year should appear immediately after the Y, for +without a year. The year should appear immediately after the directive, for example: @code{year 2004}. This is useful at the beginning of a file, to specify the year for that file. If all transactions specify a year, however, this command has no effect. diff --git a/src/filters.cc b/src/filters.cc index 5e2bf983..7570809a 100644 --- a/src/filters.cc +++ b/src/filters.cc @@ -983,7 +983,8 @@ void interval_posts::flush() sort_posts_by_date()); // Determine the beginning interval by using the earliest post - if (! interval.find_period(all_posts.front()->date())) + if (all_posts.front() && + ! interval.find_period(all_posts.front()->date())) throw_(std::logic_error, _("Failed to find period for interval report")); // Walk the interval forward reporting all posts within each one diff --git a/src/journal.cc b/src/journal.cc index e6c09125..f45b7527 100644 --- a/src/journal.cc +++ b/src/journal.cc @@ -127,8 +127,19 @@ account_t * journal_t::register_account(const string& name, post_t * post, // object. if (account_aliases.size() > 0) { accounts_map::const_iterator i = account_aliases.find(name); - if (i != account_aliases.end()) + if (i != account_aliases.end()) { result = (*i).second; + } else { + // only check the very first account for alias expansion, in case + // that can be expanded successfully + size_t colon = name.find(':'); + if(colon != string::npos) { + accounts_map::const_iterator i = account_aliases.find(name.substr(0, colon)); + if (i != account_aliases.end()) { + result = find_account((*i).second->fullname() + name.substr(colon)); + } + } + } } // Create the account object and associate it with the journal; this |