ledger/contrib/raw/GenerateLatexExpeneseReport.pl

429 lines
11 KiB
Perl
Executable file

#!/usr/bin/perl
use warnings;
use strict;
use Getopt::Long; # Options processing
use Smart::Comments -ENV, "###"; # Ignore my dividers, and use
# Smart_Comments=1 to activate
use Cwd;
use File::Basename;
use 5.10.0;
use POSIX qw(strftime);
use Date::Calc qw(Add_Delta_Days);
use Template;
my $TT = Template->new( { POST_CHOMP => 1 } );
######################################################################
# TODO
#
# DONE Meal summaries are broken for multi-week reports
######################################################################
# Options
# Is this an internal report?
my $ExpenseReportCode = undef;
my $Internal = undef;
my $SuppressMeals = 0;
my $ViewAfter = 0;
my $ImageDir = ".";
my $Anonymize = 0;
my $Help = undef;
GetOptions( 'c' => \$Internal,
'm' => \$SuppressMeals,
'v' => \$ViewAfter,
'a' => \$Anonymize,
'I' => \$ImageDir,
'e:s' => \$ExpenseReportCode,
'h|help' => \$Help
);
# Help
defined $Help && do {
print <<EOF;
Usage: GenerateLatexExpenseReport.pl [OPTION] -e ERCode
Options:
-c Internal report
-m Suppress meals
-v View PDF on completion
-a Anonymous, omit header/footer
-I Image directory
-e ER Code (AISER0001)
EOF
exit -1;
};
die "Pass -e <ExpenseReportCode>" unless defined $ExpenseReportCode;
######################################################################
# Report items
my @ItemizedExpenses;
my $ItemizedTotal = 0.00;
my @ItemizedReceipts;
my @MealsReport;
######################################################################
# Gather required data about this expense report from the directory name
# ie: ./AISER0015 20090419-20090425 AGIL2078 Shands HACMP/
#
# ExpenseReportCode = AISER0015
# DateRange = 20090419-20090425
# ProjectCode = AGIL2078
# Description = Shands HACMP
######################################################################
# Remaining options
# Where is the ledger binary?
my $LedgerBin = "./ledger -f ./.ledger -V";
# -E Show empty accounts
# -S d Sort by date
# -V Convert mileage to $
my $LedgerOpts = "--no-color -S d";
my $LedgerAcct = "^Dest:Projects";
my $LedgerCriteria = "%" . "ER=$ExpenseReportCode";
# Internal report?
if ( $Internal ) {
# No mileage on an internal report
# $LedgerCriteria .= "&!/Mileage/"; # This shouldn't matter, just don't put metadata for ER on mileage
$LedgerAcct = "^Dest:Internal";
}
my $CmdLine = "$LedgerBin reg $LedgerOpts -E \"$LedgerCriteria\" and ^Stub "
. "--format \"%(tag('ER'))~%(tag('PROJECT'))~%(tag('NOTE'))\n\"";
### $CmdLine
my @TempLine = `$CmdLine`;
# Match all remaining items
$TempLine[0] =~ m,^(?<Er>.*?)~
(?<Project>.*?)~
(?<Note>.*?)\s*$,x;
my $ProjectCode= $+{'Project'};
my $Description= $+{'Note'};
### $ExpenseReportCode
### $ProjectCode
### $Description
### $LedgerAcct
### $Internal
### $Anonymize
### $LedgerAcct
### $LedgerOpts
### $LedgerCriteria
######################################################################
# Pull main ledger report of line items for the expense report
# Using ~ as a delimiter
#
# Example:
# '2009/04/25~AR:Projects:AGIL2078:PersMealsLunch~:AISER0015: PILOT 00004259 MIDWAY, FL~ 8.68~Receipts/AGIL2078/20090425_Pilot_8_68_21204.jpg\n'
#
#./ledger --no-color reg %ER=AISER0040 and ^Projects -y "%Y/%m/%d" -V --format "%(date)~%(account)~%(payee)~%(amount)~%(tag('NOTE'))\n"
$CmdLine = "$LedgerBin reg $LedgerOpts \"$LedgerCriteria\" and \"$LedgerAcct\" "
. "-y \"%Y/%m/%d\" "
. "--format \"%(date)~%(tag('CATEGORY'))~%(payee)~%(display_amount)~%(tag('NOTE'))~%(tag('RECEIPT'))\\n\"";
### $CmdLine
my @MainReport = `$CmdLine`;
### MainReport: @MainReport
# Remove any project codes and linefeeds
#map { chomp(); s/(:AISER[0-9][0-9][0-9][0-9])+://g; } @MainReport; # No need, thats now metadata
foreach my $line (@MainReport) { ### Processing Main Report... done
# Remove bad chars (#&)
$line =~ tr/#&//d;
# Match all remaining items
$line =~ m,^(?<Date>[0-9]{4}/[0-9]{2}/[0-9]{2})~
(?<Category>.*?)~
(?<Vendor>.*?)~
(?<Amount>.*?)~
(?<Note>.*?)~
(?<Receipts>.*?)\s*$,x;
my %Record = %+;
$Record{'Amount'}=~tr/$,//d;
foreach (keys %Record) {
$Record{$_} =~ s/^\s+//g;
}
# Grab images from <<file:///dir/filename.jpg>>
my $ImageList = $Record{'Receipts'};
$ImageList //= '';
my @Images = split( /,/, $ImageList );
# Cleanup
# Take last word of account name as category
$Record{'Category'} = ( split( /:/, $Record{'Category'} ) )[-1];
# If no images, italicise the line item.
$Record{'Italics'} = 1;
# Test images
foreach my $Image (@Images) {
# Turn off italics because there is an image
$Record{'Italics'} = 0;
if (! -r $ImageDir . "/" . $Image) {
print STDERR "Missing $ImageDir/$Image\n";
}
}
# Add to itemized expenses to be printed
push( @ItemizedExpenses, \%Record );
$ItemizedTotal += $Record{'Amount'};
# Add to itemized reciepts for printing
push( @ItemizedReceipts, { 'Vendor' => $Record{'Vendor'},
'Amount' => $Record{'Amount'},
'Date' => $Record{'Date'},
'Images' => \@Images } )
if $ImageList;
}
### @ItemizedExpenses
######################################################################
# Meals report
# Summarize total spent on meals by day
$CmdLine = "$LedgerBin reg $LedgerOpts "
. "\"$LedgerCriteria\" and \"$LedgerAcct\" and \%CATEGORY=Meals "
. "-D -n "
. "--format \"%(account)~%(payee)~%(display_amount)~%(total)\n\" "
. "| grep -v '<None>'";
### $CmdLine
my @MealsOutput = `$CmdLine`;
### @MealsOutput
foreach my $line (@MealsOutput) {
# Match all remaining items
$line =~ m,^(?<Account>.*?)~
(?<DOW>.*?)~\$
(?<Amount>\s*[0-9]+\.[0-9]+)~\$
(?<RunningTotal>.*?)\s*$,x;
my %TRecord=%+;
$TRecord{'Account'}=~s/^Projects://g;
$TRecord{'DOW'}=~s/^- //g;
# Add to itemized expenses to be printed
push( @MealsReport, \%TRecord );
}
######################################################################
# Total by category
$CmdLine = "$LedgerBin bal $LedgerOpts "
. "\"$LedgerCriteria\" and \"$LedgerAcct\" '--account=tag(\"CATEGORY\")' "
. "--format \"%(account)~%(display_total)\\n\"";
### $CmdLine
my @CategoryOutput = `$CmdLine`;
### @CategoryOutput
my @CategoryReport;
foreach my $line (@CategoryOutput) {
chomp $line;
$line =~ tr/\$,//d;
# Match all remaining items
my @Temp = split(/~/,$line);
my %TRecord= ( 'Category' => $Temp[0],
'Amount' => $Temp[1]);
if ($TRecord{'Category'} eq '') {
$TRecord{'Category'} = '\\hline \\bf Total';
}
# Cleanup
# Take last word of account name as category
$TRecord{'Category'} = ( split( /:/, $TRecord{'Category'} ) )[0];
# Add to itemized expenses to be printed
push( @CategoryReport, \%TRecord );
}
### @CategoryReport
######################################################################
# Output
######################################################################
my $TTVars = {
'Internal' => $Internal,
'SuppressMeals' => $SuppressMeals,
'ExpenseReportCode' => $ExpenseReportCode,
'ProjectCode' => $ProjectCode,
'Description' => $Description,
'ItemizedExpenses' => \@ItemizedExpenses,
'ItemizedTotal' => $ItemizedTotal,
'MealsReport' => \@MealsReport,
'CategoryReport' => \@CategoryReport,
'ItemizedReceipts' => \@ItemizedReceipts,
'ImageDir' => $ImageDir,
'Anonymize' => $Anonymize
};
#### $TTVars
my $LatexTemplate = <<EOF;
[% USE format %][% ToDollars = format('\\\$%.2f') %]
%%%%%%%%%%% Header
\\documentclass[10pt,letterpaper]{article}
\\usepackage[letterpaper,includeheadfoot,top=0.5in,bottom=0.5in,left=0.75in,right=0.75in]{geometry}
\\usepackage[utf8]{inputenc}
\\usepackage[T1]{fontenc}
\\usepackage[scaled]{helvet}
\\renewcommand*\\familydefault{\\sfdefault}
\\usepackage{lastpage}
\\usepackage{fancyhdr}
\\usepackage{graphicx}
\\usepackage{multicol}
\\usepackage[colorlinks,linkcolor=blue]{hyperref}
\\pagestyle{fancy}
\\renewcommand{\\headrulewidth}{1pt}
\\renewcommand{\\footrulewidth}{0.5pt}
\\geometry{headheight=48pt}
\\begin{document}
%%%%%%%%%% Itemized table
\\section{Itemized Expenses}
[% FOREACH Expense IN ItemizedExpenses %]
[% IF loop.first() or ( ( loop.count mod 20 ) == 0 ) %]
\\begin{tabular}{|l|l|p{2in}|r|p{2in}|}
\\hline
\\hline
\\bf Date & \\bf Category & \\bf Expense & \\bf Amount & \\bf Notes \\\\
\\hline \\hline
[% END %]
[% IF Expense.Italics %]\\it[% END %] [% Expense.Date %] & [% IF Expense.Italics %]\\it[% END %] [% Expense.Category %] & [% IF Expense.Italics %]\\it[% END %] [% Expense.Vendor %] & [% IF Expense.Italics %]\\it[% END %] [% ToDollars(Expense.Amount) %] & [% IF Expense.Italics %]\\it[% END %] [% Expense.Note %] \\\\ \\hline
[% IF loop.last() %]
\\hline
& & \\bf Total & \\bf [% ToDollars(ItemizedTotal) %] & \\\\
[% END %]
[% IF ( ( (loop.count + 1) mod 20 ) == 0 ) or loop.last() %]
\\hline
\\hline
\\end{tabular}
[% IF ( ( (loop.count + 1) mod 20 ) == 0 ) %]\\newline {\\it Continued on next page...}[% END %]
\\newpage
[% END %]
[% END %]
[% IF ! Internal %][% IF ! SuppressMeals %]
%%%%%%%%%% Meals summary table
\\section{Meals Summary By Day}
\\begin{tabular}{|l|l|p{2in}|p{2in}|}
\\hline
\\hline
\\bf DOW & \\bf Daily Total & \\bf Running Total \\\\
\\hline \\hline
[% FOREACH Meal IN MealsReport %]
[% Meal.DOW %] & [% ToDollars(Meal.Amount) %] & [% ToDollars(Meal.RunningTotal) %] \\\\ \\hline
[% END %]
\\hline
\\hline
\\end{tabular}
[% END %][% END %]
%%%%%%%%%% Category summary
\\section{Expenses Summary}
\\begin{tabular}{|l|l|}
\\hline
\\hline
\\bf Category & \\bf Total \\\\
\\hline \\hline
[% FOREACH Category IN CategoryReport %]
[% Category.Category %] & [% ToDollars(Category.Amount) %] \\\\ \\hline
[% END %]
\\hline
\\hline
\\end{tabular}
%%%%%%%%%% Begin receipts
\\section{Scanned Receipts}
[% FOREACH Receipt IN ItemizedReceipts %]
\\subsection{[% Receipt.Date %] [% Receipt.Vendor %]: [% ToDollars(Receipt.Amount) %]}
[% FOREACH Image IN Receipt.Images %]
\\includegraphics[angle=90,width=\\textwidth,keepaspectratio]{[% ImageDir %]/[% Image %]} \\\\
[% END %]
[% END %]
%%%%%%%%%% Footer
\\end{document}
EOF
my $LatexFN = $ExpenseReportCode . "-" . $ProjectCode . ".tex";
### $LatexFN
$TT->process( \$LatexTemplate, $TTVars, "./tmp/" . $LatexFN ) || do {
my $error = $TT->error();
print "error type: ", $error->type(), "\n";
print "error info: ", $error->info(), "\n";
die $error;
};
my $LatexOutput = `pdflatex -interaction batchmode -output-directory ./tmp "$LatexFN"`;
### $LatexOutput
$LatexOutput = `pdflatex -interaction batchmode -output-directory ./tmp "$LatexFN"`;
### $LatexOutput
if ($ViewAfter) {
my $ViewFN = $LatexFN;
$ViewFN =~ s/\.tex$/.pdf/;
`acroread "./tmp/$ViewFN"`;
}