429 lines
11 KiB
Perl
Executable file
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"`;
|
|
}
|
|
|