diff options
Diffstat (limited to 'contrib')
10 files changed, 544 insertions, 142 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 = <CSV_DATA>) { print CSV_OUT $line; } + while (my $line = <CSV_DATA>) { $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 2b3024d4..8b880648 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 @@ -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): + 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): 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,25 +60,39 @@ def csv2ods(csvname, odsname, encoding='', verbose = False): 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 - 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(): @@ -109,7 +122,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.encoding, options.verbose) if __name__ == '__main__': main() 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 <START_DATE> <END_DATE> <LEDGER_OPTIONS>\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: $!"; - -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; +foreach my $type ('starting', 'ending') { + my(@ledgerOptions) = (@mainLedgerOptions, + '-V', '-X', '$', '-F', "%-.70A %22.108t\n", '-s'); -# 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; + 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 = <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) <= $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; - - 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 = <LEDGER_INCOME>) { 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 %tot; +($tot{Start}, $tot{End}) = ($ZERO, $ZERO); + +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"; -my($totStart, $totEnd) = ($ZERO, $ZERO); +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 keys %funds) { +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"; + } + $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 sprintf("\n%-35s\$%26.2f\n", "Balance as of $formattedEndDate:", - $funds{$fund}{ending}), "\n\n"; + 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: diff --git a/contrib/non-profit-audit-reports/general-ledger-report.plx b/contrib/non-profit-audit-reports/general-ledger-report.plx index d1c92975..1fd0e7ce 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 @@ -44,10 +44,24 @@ 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 $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,32 +78,83 @@ 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"; +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/ and $_[1] !~ /^Assets/) { + return -1; + } elsif ($_[1] =~ /^Assets/ and $_[0] !~ /^Assets/) { + return 1; + } elsif ($_[0] =~ /^Liabilities/ and $_[1] !~ /^(Assets|Liabilities)/) { + return -1; + } elsif ($_[1] =~ /^Liabilities/ and $_[0] !~ /^(Assets|Liabilities)/) { + return 1; + } elsif ($_[0] =~ /^(Accrued:[^:]+Receivable)/ and $_[1] !~ /^(Assets|Liabilities|Accrued:[^:]+Receivable)/) { + return -1; + } elsif ($_[1] =~ /^(Accrued:[^:]+Receivable)/ and $_[0] !~ /^(Assets|Liabilities|Accrued:[^:]+Receivable)/) { + return 1; + } elsif ($_[0] =~ /^(Accrued)/ and $_[1] !~ /^(Assets|Liabilities|Accrued)/) { + return -1; + } elsif ($_[1] =~ /^(Accrued)/ and $_[0] !~ /^(Assets|Liabilities|Accrued)/) { + return 1; + } elsif ($_[0] =~ /^(Unearned Income)/ and $_[1] !~ /^(Assets|Liabilities|Accrued|Unearned Income)/) { + return -1; + } elsif ($_[1] =~ /^(Unearned Income)/ and $_[0] !~ /^(Assets|Liabilities|Accrued|Unearned Income)/) { + return 1; + } elsif ($_[0] =~ /^Income/ and $_[1] !~ /^(Assets|Liabilities|Accrued|Unearned Income|Income)/) { + return -1; + } 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 ( - # 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) { - print CHART_OUTPUT "$acct\n"; +foreach my $acct ( sort preferredAccountSorting @accounts) { + 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"); +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 (<FILE>) { + 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 =~ /\<Adjustment\>/ 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"; @@ -110,32 +175,44 @@ 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 .= ',"link:%(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) or die "Unable to run $LEDGER_CMD @acctLedgerOpts: $!"; foreach my $line (<GL_CSV_DATA>) { + $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; } } + 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"; + } + 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; diff --git a/contrib/non-profit-audit-reports/summary-reports.plx b/contrib/non-profit-audit-reports/summary-reports.plx index 7d2267d6..e9e1a3b8 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 @@ -39,6 +39,40 @@ sub Commify ($) { return scalar reverse $text; } +sub preferredAccountSorting ($$) { + if ($_[0] =~ /^Assets/ and $_[1] !~ /^Assets/) { + return -1; + } elsif ($_[1] =~ /^Assets/ and $_[0] !~ /^Assets/) { + return 1; + } elsif ($_[0] =~ /^Liabilities/ and $_[1] !~ /^(Assets|Liabilities)/) { + return -1; + } elsif ($_[1] =~ /^Liabilities/ and $_[0] !~ /^(Assets|Liabilities)/) { + return 1; + } elsif ($_[0] =~ /^(Accrued:[^:]+Receivable)/ and $_[1] !~ /^(Assets|Liabilities|Accrued:[^:]+Receivable)/) { + return -1; + } elsif ($_[1] =~ /^(Accrued:[^:]+Receivable)/ and $_[0] !~ /^(Assets|Liabilities|Accrued:[^:]+Receivable)/) { + return 1; + } elsif ($_[0] =~ /^(Accrued)/ and $_[1] !~ /^(Assets|Liabilities|Accrued)/) { + return -1; + } elsif ($_[1] =~ /^(Accrued)/ and $_[0] !~ /^(Assets|Liabilities|Accrued)/) { + return 1; + } elsif ($_[0] =~ /^(Unearned Income)/ and $_[1] !~ /^(Assets|Liabilities|Accrued|Unearned Income)/) { + return -1; + } elsif ($_[1] =~ /^(Unearned Income)/ and $_[0] !~ /^(Assets|Liabilities|Accrued|Unearned Income)/) { + return 1; + } elsif ($_[0] =~ /^Income/ and $_[1] !~ /^(Assets|Liabilities|Accrued|Unearned Income|Income)/) { + return -1; + } 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]; + } +} + sub ParseNumber($) { $_[0] =~ s/,//g; return Math::BigFloat->new($_[0]); @@ -116,15 +150,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; @@ -156,7 +190,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} @@ -193,7 +227,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', '$', @@ -217,32 +251,32 @@ foreach my $type (keys %incomeGroups) { $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://; + 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); @@ -268,7 +302,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, @@ -291,6 +325,7 @@ foreach my $line (<FILE>) { 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) { @@ -299,23 +334,23 @@ foreach my $line (<FILE>) { 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) { @@ -330,7 +365,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}; @@ -338,7 +373,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); @@ -348,6 +383,80 @@ die "calculated total of $overallTotal does equal $firstTotal" print STDERR "\n"; +open(TRIAL, ">", "trial-balance.csv") or die "unable to open accrued.txt for writing: $!"; + +print TRIAL "\"TRIAL BALANCE REPORT\",\"ENDING: $formattedEndDate\"\n\n", + "\"ACCOUNT\",\"BALANCE AT $formattedStartDate\",\"CHANGE DURING FY\",\"BALANCE AT $formattedEndDate\"\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' ]); + +my %trialBalanceData; +my %fullAccountList; + +foreach my $id (keys %commands) { + my(@command) = @{$commands{$id}}; + + open(FILE, "-|", @command) + or die "unable to run command ledger command: @command: $!"; + + print STDERR ($VERBOSE ? "Running: @command\n" : "."); + + foreach my $line (<FILE>) { + 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 =~ /\<Adjustment\>/ 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 "pagebreak\n"; + $curOn = $account; + if ($curOn =~ /(Accrued:[^:]+):.*$/) { + $curOn = $1; + } else { + $curOn =~ s/^([^:]+):.*$/$1/; + } + } + 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); + ############################################################################### # # Local variables: 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 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 Binary files differindex 80771a6d..8eae706f 100644 --- 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 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..f481e02f --- /dev/null +++ b/contrib/non-profit-audit-reports/unpaid-accruals-report.plx @@ -0,0 +1,110 @@ +#!/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 <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), + "%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', '$', '-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: $!"; + + while (my $line = <LEDGER_FUNDS>) { + next if $line =~ /"\<Adjustment\>"/; + 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); + + 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"; +} +############################################################################### +# +# Local variables: +# compile-command: "perl -c unpaid-accruals-report.plx" +# End: + |