#!/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 <" 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,^(?.*?)~ (?.*?)~ (?.*?)\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, that's now metadata foreach my $line (@MainReport) { ### Processing Main Report... done # Remove bad chars (#&) $line =~ tr/#&//d; # Match all remaining items $line =~ m,^(?[0-9]{4}/[0-9]{2}/[0-9]{2})~ (?.*?)~ (?.*?)~ (?.*?)~ (?.*?)~ (?.*?)\s*$,x; my %Record = %+; $Record{'Amount'}=~tr/$,//d; foreach (keys %Record) { $Record{$_} =~ s/^\s+//g; } # Grab images from <> 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 ''"; ### $CmdLine my @MealsOutput = `$CmdLine`; ### @MealsOutput foreach my $line (@MealsOutput) { # Match all remaining items $line =~ m,^(?.*?)~ (?.*?)~\$ (?\s*[0-9]+\.[0-9]+)~\$ (?.*?)\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 = <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"`; }