#!/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, 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; 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 \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 = ) { 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 $typeData ({ name => 'disbursements', query => 'a<=0' }, { name => 'receipts', query => 'a>0' }) { my $fileNameBase = $typeData->{name}; open(CSV_OUT, ">", "$fileNameBase.csv") or die "unable to open $fileNameBase.csv: $!"; foreach my $acct (sort { $a cmp $b } @accounts) { next unless ($acct =~ /^(?:Assets|Liabilities)/); 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 = ) { 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); print CSV_OUT "\"ACCOUNT:\",\"$acct\"\n\"PERIOD START:\",\"$beginDate\"\n\"PERIOD END:\",\"$formattedEndDate\"\n"; print CSV_OUT '"DATE","CHECK NUM","NAME","ACCOUNT","AMOUNT"'; my $formatString = '\n"%(date)","%C","%P","%A","%t"'; my $tagStrings = ""; foreach my $tagField (qw/Receipt Invoice Statement Contract PurchaseOrder Approval Check IncomeDistributionAnalysis CurrencyRate/) { print CSV_OUT ',"', $tagField, '"'; $tagStrings .= ',"link:%(tag(\'' . $tagField . '\'))"'; } $formatString .= $tagStrings . '\n%/"","","","%A","%t"' . $tagStrings . '\n'; # I thought '--sort', 'd', '--sort-xact', 'a', should # have worked below for a good sort. Then I tried # rather than '--sort', "d,n,a", which didn't work either. # I opened a bug: http://bugs.ledger-cli.org/show_bug.cgi?id=901 my @csvRegLedgerOpts = ('-f', $tempFile, '-V', '-F', $formatString, '-w', '--sort', 'd', '-b', $beginDate, '-e', $endDate, 'reg'); open(CSV_DATA, "-|", $LEDGER_CMD, @csvRegLedgerOpts) or die "unable to run ledger command for $fileNameBase.csv: $!"; my($curDepositDate, $curDepositTotal); while (my $line = ) { $line =~ s/"link:"/""/g; my $date = $line; chomp $date; $date =~ s/^\s*"([^"]*)"\s*,.*$/$1/; if (defined $date and $date !~ /^\s*$/ and defined $curDepositDate and ($date ne $curDepositDate or ($date eq $curDepositDate and $line !~ /DEPOSIT[\s\-]+BRANCH/))) { print CSV_OUT "\"$curDepositDate\",\"SUBTOTAL\",\"BRANCH DEPOSIT TOTAL:\",\"\",\"\$$curDepositTotal\"\n\n"; $curDepositTotal = $curDepositDate = undef; } if ($line =~ /DEPOSIT[\s\-]+BRANCH/) { if (not defined $curDepositDate) { $curDepositDate = $line; chomp $curDepositDate; $curDepositDate =~ s/^\s*"([^"]+)"\s*,.*$/$1/; } } # This is a bit of a hack because I can't ssume that the line with the # description on it has the account name in it. if (defined $curDepositDate and $line =~ /$acct/) { my $amt = $line; chomp $amt; $amt =~ s/^\s*"[^"]*","[^"]*","[^"]*","[^"]*","\$\s*([^"]*)".*$/$1/; $amt =~ s/,//g; $curDepositTotal = 0.0 unless defined $curDepositTotal; $curDepositTotal += $amt; } print CSV_OUT $line; } # Catch potential last Deposit subtotal print CSV_OUT "\n\"$curDepositDate\",\"SUBTOTAL\",\"BRANCH DEPOSIT TOTAL:\",\"\",\"\$$curDepositTotal\"\n\n" if (defined $curDepositDate); close(CSV_DATA); die "Error read from csv ledger command $!" unless $? == 0; print CSV_OUT "pagebreak\n"; SKIP_REGISTER_COMMANDS: unlink($tempFile); } close(CSV_OUT); die "Error read write csv out to $fileNameBase.csv: $!" unless $? == 0; } ############################################################################### # # Local variables: # compile-command: "perl -c cash-receipts-and-disbursments-journals.plx" # End: