Merge pull request #110 from bkuhn/update-contrib-non-profit-annual-audit-reports_2012-11-26
Update to contrib/non-profit-audit-reports/ scripts.
This commit is contained in:
commit
edc272b7a0
9 changed files with 701 additions and 11 deletions
147
contrib/non-profit-audit-reports/cash-receipts-and-disbursments-journals.plx
Executable file
147
contrib/non-profit-audit-reports/cash-receipts-and-disbursments-journals.plx
Executable file
|
|
@ -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:
|
||||
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
154
contrib/non-profit-audit-reports/fund-report.plx
Executable file
154
contrib/non-profit-audit-reports/fund-report.plx
Executable file
|
|
@ -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:
|
||||
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
355
contrib/non-profit-audit-reports/summary-reports.plx
Executable file
355
contrib/non-profit-audit-reports/summary-reports.plx
Executable file
|
|
@ -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:
|
||||
Binary file not shown.
|
|
@ -0,0 +1 @@
|
|||
I, Another J. Donor, would like $400 to be earmarked for Foo!
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue