summaryrefslogtreecommitdiff
path: root/contrib
diff options
context:
space:
mode:
Diffstat (limited to 'contrib')
-rwxr-xr-xcontrib/non-profit-audit-reports/cash-receipts-and-disbursments-journals.plx4
-rwxr-xr-xcontrib/non-profit-audit-reports/csv2ods.py41
-rwxr-xr-xcontrib/non-profit-audit-reports/fund-report.plx221
-rwxr-xr-xcontrib/non-profit-audit-reports/general-ledger-report.plx129
-rwxr-xr-xcontrib/non-profit-audit-reports/summary-reports.plx161
-rw-r--r--contrib/non-profit-audit-reports/tests/non-profit-test-data_MANIFEST10
-rw-r--r--contrib/non-profit-audit-reports/tests/non-profit-test-data_chart-of-accounts.csv6
-rw-r--r--contrib/non-profit-audit-reports/tests/non-profit-test-data_chart-of-accounts.txt4
-rw-r--r--contrib/non-profit-audit-reports/tests/non-profit-test-data_general-ledger.odsbin11412 -> 5770 bytes
-rwxr-xr-xcontrib/non-profit-audit-reports/unpaid-accruals-report.plx110
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
index 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
Binary files differ
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:
+