Merge commit 'c235b59a68d04ff22a16d317e4d67b69e570a1e4' into kitchen-sink
This commit is contained in:
commit
870c62bf14
9 changed files with 1281 additions and 62 deletions
429
contrib/raw/GenerateLatexExpeneseReport.pl
Executable file
429
contrib/raw/GenerateLatexExpeneseReport.pl
Executable file
|
|
@ -0,0 +1,429 @@
|
|||
#!/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"`;
|
||||
}
|
||||
|
||||
111
contrib/raw/MetadataExample.dat
Normal file
111
contrib/raw/MetadataExample.dat
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
; TAG key: value
|
||||
|
||||
2009/09/27 * (09/28/2009) HUDSON NEWS HOUSTN HBB HOUSTON, TX
|
||||
Source:Visa -$6.55
|
||||
Projects:Meals
|
||||
; ER: AISER0033
|
||||
; PROJECT: PROJXXXX
|
||||
|
||||
2009/09/27 * (09/28/2009) PEET'S COFFEE & TEA KINGWOOD, TX
|
||||
Source:Visa -$2.44
|
||||
Projects:Meals
|
||||
; ER: AISER0033
|
||||
; PROJECT: PROJXXXX
|
||||
|
||||
2009/09/28 * (09/29/2009) FUSIA NEW YORK, NY
|
||||
Source:Visa -$15.25
|
||||
Projects:Meals
|
||||
; ER: AISER0033
|
||||
; PROJECT: PROJXXXX
|
||||
|
||||
|
||||
2009/09/29 * (09/30/2009) BALUCHI'S NEW YORK, NY
|
||||
Source:Visa
|
||||
Projects:Meals $20.00
|
||||
; ER: AISER0033
|
||||
; PROJECT: PROJXXXX
|
||||
Internal:Travel $10.44
|
||||
; ER: AISER0036
|
||||
; PROJECT: Internal
|
||||
|
||||
|
||||
2009/10/01 * Reimbursing AISER0036
|
||||
Bank:AISChecking
|
||||
; ER: AISER0036
|
||||
; PROJECT: Internal
|
||||
Source:Visa $10.44
|
||||
|
||||
|
||||
----------
|
||||
|
||||
$ ./ledger -Ef AISER0033.dat bal '--account=tag("ER")'
|
||||
$-44.24
|
||||
$44.24 AISER0033
|
||||
0 AISER0036
|
||||
--------------------
|
||||
0
|
||||
|
||||
$ ./ledger -Ef AISER0033.dat bal
|
||||
$-10.44 Bank:AISChecking
|
||||
$10.44 Internal:Travel
|
||||
$44.24 Projects:Meals
|
||||
$-44.24 Source:Visa
|
||||
--------------------
|
||||
0
|
||||
|
||||
$ ./ledger -Ef AISER0033.dat bal '--account=tag("PROJECT")'
|
||||
$-44.24
|
||||
0 Internal
|
||||
$44.24 PROJXXXX
|
||||
--------------------
|
||||
0
|
||||
|
||||
$ ./ledger -f AISER0033.dat reg '--account=tag("PROJECT")'
|
||||
09-Sep-27 HUDSON NEWS HOUSTN .. $-6.55 $-6.55
|
||||
09-Sep-27 HUDSON NEWS HOUSTN .. PROJXXXX $6.55 0
|
||||
09-Sep-27 PEET'S COFFEE & TEA.. $-2.44 $-2.44
|
||||
09-Sep-27 PEET'S COFFEE & TEA.. PROJXXXX $2.44 0
|
||||
09-Sep-28 FUSIA NEW YORK, NY $-15.25 $-15.25
|
||||
09-Sep-28 FUSIA NEW YORK, NY PROJXXXX $15.25 0
|
||||
09-Sep-29 BALUCHI'S NEW YORK,.. $-30.44 $-30.44
|
||||
09-Sep-29 BALUCHI'S NEW YORK,.. PROJXXXX $20.00 $-10.44
|
||||
09-Sep-29 BALUCHI'S NEW YORK,.. Internal $10.44 0
|
||||
09-Oct-01 Reimbursing AISER0036 Internal $-10.44 $-10.44
|
||||
09-Oct-01 Reimbursing AISER0036 $10.44 0
|
||||
|
||||
$ ./ledger -f AISER0033.dat reg '--account=tag("ER")'
|
||||
09-Sep-27 HUDSON NEWS HOUSTN .. $-6.55 $-6.55
|
||||
09-Sep-27 HUDSON NEWS HOUSTN .. AISER0033 $6.55 0
|
||||
09-Sep-27 PEET'S COFFEE & TEA.. $-2.44 $-2.44
|
||||
09-Sep-27 PEET'S COFFEE & TEA.. AISER0033 $2.44 0
|
||||
09-Sep-28 FUSIA NEW YORK, NY $-15.25 $-15.25
|
||||
09-Sep-28 FUSIA NEW YORK, NY AISER0033 $15.25 0
|
||||
09-Sep-29 BALUCHI'S NEW YORK,.. $-30.44 $-30.44
|
||||
09-Sep-29 BALUCHI'S NEW YORK,.. AISER0033 $20.00 $-10.44
|
||||
09-Sep-29 BALUCHI'S NEW YORK,.. AISER0036 $10.44 0
|
||||
09-Oct-01 Reimbursing AISER0036 AISER0036 $-10.44 $-10.44
|
||||
09-Oct-01 Reimbursing AISER0036 $10.44 0
|
||||
|
||||
|
||||
$ ./ledger -f AISER0033.dat reg %ER=AISER0033
|
||||
09-Sep-27 HUDSON NEWS HOUSTN .. Projects:Meals $6.55 $6.55
|
||||
09-Sep-27 PEET'S COFFEE & TEA.. Projects:Meals $2.44 $8.99
|
||||
09-Sep-28 FUSIA NEW YORK, NY Projects:Meals $15.25 $24.24
|
||||
09-Sep-29 BALUCHI'S NEW YORK,.. Projects:Meals $20.00 $44.24
|
||||
|
||||
|
||||
$ ./ledger -f AISER0033.dat reg %ER=AISER0036
|
||||
09-Sep-29 BALUCHI'S NEW YORK,.. Internal:Travel $10.44 $10.44
|
||||
09-Oct-01 Reimbursing AISER0036 Bank:AISChecking $-10.44 0
|
||||
|
||||
$ ./ledger -f AISER0033.dat reg %PROJECT=PROJXXXX
|
||||
09-Sep-27 HUDSON NEWS HOUSTN .. Projects:Meals $6.55 $6.55
|
||||
09-Sep-27 PEET'S COFFEE & TEA.. Projects:Meals $2.44 $8.99
|
||||
09-Sep-28 FUSIA NEW YORK, NY Projects:Meals $15.25 $24.24
|
||||
09-Sep-29 BALUCHI'S NEW YORK,.. Projects:Meals $20.00 $44.24
|
||||
|
||||
$ ./ledger -f AISER0033.dat --prepend-format='%(tag("IMG")) ' reg %ER=0033
|
||||
image1.jpg 09-Sep-27 HUDSON NEWS HOUSTN .. Projects:Meals $6.55 $6.55
|
||||
image2.jpg 09-Sep-27 PEET'S COFFEE & TEA.. Projects:Meals $2.44 $8.99
|
||||
image3.jpg 09-Sep-28 FUSIA NEW YORK, NY Projects:Meals $15.25 $24.24
|
||||
image4.jpg 09-Sep-29 BALUCHI'S NEW YORK,.. Projects:Meals $20.00 $44.24
|
||||
5
contrib/raw/README
Normal file
5
contrib/raw/README
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
These scripts are from my (rladams) local ledger customizations.
|
||||
|
||||
They are intended as examples for features that can be made generic to benefit other Ledger users.
|
||||
|
||||
As they become refined, the raw files will be removed and replaced by suitable sources in contrib.
|
||||
14
contrib/raw/VerifyImages.sh
Executable file
14
contrib/raw/VerifyImages.sh
Executable file
|
|
@ -0,0 +1,14 @@
|
|||
#!/bin/sh
|
||||
|
||||
grep -h '; RECEIPT: ' \
|
||||
*.dat \
|
||||
*/*.dat \
|
||||
| sed 's,\W*; RECEIPT: ,,g' \
|
||||
| tr , '\n' \
|
||||
| sort -u \
|
||||
| while read X
|
||||
do
|
||||
[ -f "$X" ] \
|
||||
&& echo OK $X \
|
||||
|| echo XX $X
|
||||
done
|
||||
201
contrib/raw/dotemacs.el
Normal file
201
contrib/raw/dotemacs.el
Normal file
|
|
@ -0,0 +1,201 @@
|
|||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
; Ledger
|
||||
|
||||
;; Maybe later add this to the expense repo once it settles
|
||||
(add-to-list 'load-path "/home/adamsrl/.emacs.d/addons/ledger")
|
||||
|
||||
(add-to-list 'load-path "/home/adamsrl/AdamsInfoServ/BusinessDocuments/Ledger/AdamsRussell/bin")
|
||||
(autoload 'ledger-mode "ldg-new" nil t)
|
||||
(add-to-list 'auto-mode-alist '("\\.dat$" . ledger-mode))
|
||||
|
||||
(add-hook 'ledger-mode-hook
|
||||
(lambda ()
|
||||
(setq truncate-lines 1)
|
||||
(url-handler-mode 1) ; Enable hyperlinks
|
||||
(require 'ledger-matching) ; Requires ldg-report anyway
|
||||
(load-file "/home/adamsrl/.emacs.d/addons/ledger/ldg-xact.el")
|
||||
(let ((map (current-local-map)))
|
||||
(define-key map (kbd "\C-c o") 'find-file-at-point) ; Open images
|
||||
(define-key map (kbd "<f8>") 'ledger-expense-shortcut)
|
||||
(define-key map (kbd "M-i") 'ledger-expense-internal)
|
||||
(define-key map (kbd "M-o") 'ledger-expense-personal)
|
||||
(define-key map (kbd "M-'") 'ledger-expense-split)
|
||||
(define-key map (kbd "M-n") '(lambda ()
|
||||
(interactive)
|
||||
(ledger-post-next-xact)
|
||||
(recenter)
|
||||
(when (get-buffer "*Receipt*")
|
||||
(ledger-expense-show-receipt))))
|
||||
(define-key map (kbd "M-p") '(lambda () (interactive)
|
||||
(ledger-post-prev-xact)
|
||||
(recenter)
|
||||
(when (get-buffer "*Receipt*")
|
||||
(ledger-expense-show-receipt))))
|
||||
(local-unset-key [tab]) ; Ideally this turns off pcomplete
|
||||
(local-unset-key [(control ?i)]) ; Ideally this turns off pcomplete
|
||||
)
|
||||
|
||||
;(defface ledger-report-face-account-ok '((t (:foreground "Cyan"))) "Derp")
|
||||
;(defface ledger-report-face-account-bad '((t (:foreground "Red"))) "Derp")
|
||||
|
||||
(font-lock-add-keywords
|
||||
'ledger-mode
|
||||
'(("Unassigned\\|Unknown\\|; RECEIPT:$" 0 'highlight prepend))) ))
|
||||
|
||||
;; My customizations to make receipt image matching work with ledger-report mode
|
||||
(add-hook 'ledger-report-mode-hook
|
||||
(lambda ()
|
||||
(hl-line-mode 1)
|
||||
(local-set-key (kbd "<RET>") 'ledger-report-visit-source) ; Make return jump to the right txn
|
||||
(local-set-key (kbd "<tab>") 'ledger-report-visit-source) ; Make tab jump to the right txn
|
||||
(local-set-key (kbd "n") '(lambda ()
|
||||
(interactive)
|
||||
(save-selected-window
|
||||
(next-line)
|
||||
(ledger-report-visit-source)))) ; Update a txn window but keep focus
|
||||
(local-set-key (kbd "p") '(lambda ()
|
||||
(interactive)
|
||||
(save-selected-window
|
||||
(previous-line)
|
||||
(ledger-report-visit-source)))) ; Update a txn window but keep focus
|
||||
|
||||
|
||||
(local-set-key (kbd "M-r") 'ledger-receipt-matching) ; Link receipt to current item
|
||||
(local-set-key (kbd "M-l") 'ledger-matching-tie-receipt-to-txn) ; Link receipt to current item
|
||||
(local-set-key (kbd "M-n") '(lambda ()
|
||||
(interactive)
|
||||
(ledger-matching-image-offset-adjust 1))) ; Next receipt image
|
||||
(local-set-key (kbd "M-p") '(lambda ()
|
||||
(interactive)
|
||||
(ledger-matching-image-offset-adjust -1))) ; prev receipt image
|
||||
(local-set-key (kbd "M-s") '(lambda ()
|
||||
(interactive)
|
||||
(ledger-receipt-skip))) ; Skip receipt image
|
||||
(local-set-key (kbd "C-c C-e") '(lambda () (interactive)
|
||||
(save-selected-window
|
||||
(ledger-report-visit-source)
|
||||
(ledger-toggle-current-entry) ))) ; Toggle entry
|
||||
))
|
||||
|
||||
(defvar *ledger-expense-shortcut-ER*
|
||||
"Current expense report number, just last four digits (ie: 1234 results in AISER1234).")
|
||||
|
||||
(defvar *ledger-expense-shortcut-split-ER*
|
||||
"Split (ie: internal) expense report number, just last four digits (ie: 1234 results in AISER1234).")
|
||||
|
||||
(defvar *ledger-expense-shortcut-Proj* ""
|
||||
"Current export report project code (ie: AGIL1292)")
|
||||
|
||||
(defun ledger-expense-shortcut-ER-format-specifier () *ledger-expense-shortcut-ER*)
|
||||
|
||||
(defun ledger-expense-shortcut-setup (ER Split Proj)
|
||||
"Sets the variables expanded into the transaction."
|
||||
(interactive "MER Number (4 digit number only): \nMSplit ER Number (4 digit number only): \nMProject: ")
|
||||
(setq *ledger-expense-shortcut-ER*
|
||||
(concatenate 'string "AISER" ER))
|
||||
(setq *ledger-expense-shortcut-split-ER*
|
||||
(concatenate 'string "AISER" Split))
|
||||
(setq *ledger-expense-shortcut-Proj* Proj)
|
||||
(setq ledger-matching-project Proj)
|
||||
(message "Set Proj to %s and ER to %s, split to %s"
|
||||
*ledger-expense-shortcut-Proj*
|
||||
*ledger-expense-shortcut-ER*
|
||||
*ledger-expense-shortcut-split-ER*))
|
||||
|
||||
(defun ledger-expense-shortcut ()
|
||||
"Updates the ER and Project metadata with the current values of the shortcut variables."
|
||||
(interactive)
|
||||
(when (eq major-mode 'ledger-mode)
|
||||
(if (or (eql *ledger-expense-shortcut-ER* "")
|
||||
(eql *ledger-expense-shortcut-Proj* ""))
|
||||
(message "Run ledger-expense-shortcut-setup first.")
|
||||
(save-excursion
|
||||
(search-forward "; ER:")
|
||||
(kill-line nil)
|
||||
(insert " " *ledger-expense-shortcut-ER*))
|
||||
(save-excursion
|
||||
(search-forward "; PROJECT:")
|
||||
(kill-line nil)
|
||||
(insert " " *ledger-expense-shortcut-Proj*)))))
|
||||
|
||||
(defun ledger-expense-split ()
|
||||
"Splits the current transaction between internal and projects."
|
||||
(interactive)
|
||||
(when (eq major-mode 'ledger-mode) ; I made this local now, should only trigger in ldg-mode
|
||||
(save-excursion
|
||||
(end-of-line)
|
||||
(re-search-backward "^[0-9]\\{4\\}/")
|
||||
(re-search-forward "^ +Dest:Projects")
|
||||
(move-beginning-of-line nil)
|
||||
(let ((begin (point))
|
||||
(end (re-search-forward "^$")))
|
||||
(goto-char end)
|
||||
(insert (buffer-substring begin end))
|
||||
(goto-char end)
|
||||
(re-search-forward "^ Dest:Projects")
|
||||
(replace-match " Dest:Internal")
|
||||
(re-search-forward "; ER: +[A-Za-z0-9]+")
|
||||
(replace-match (concat "; ER: " *ledger-expense-shortcut-split-ER*) t)
|
||||
(when (re-search-forward "; CATEGORY: Meals" (save-excursion (re-search-forward "^$")) t)
|
||||
(replace-match "; CATEGORY: Travel" t))))
|
||||
(re-search-backward "^[0-9]\\{4\\}/")
|
||||
(re-search-forward "^ +Dest:Projects")
|
||||
(insert-string " $") ))
|
||||
|
||||
(defun ledger-expense-internal ()
|
||||
"Makes the expense an internal one."
|
||||
(interactive)
|
||||
(when (eq major-mode 'ledger-mode) ; I made this local now, should only trigger in ldg-mode
|
||||
(save-excursion
|
||||
(end-of-line)
|
||||
(re-search-backward "^[0-9]\\{4\\}/")
|
||||
(let ((begin (point))
|
||||
(end (save-excursion (re-search-forward "^$"))))
|
||||
(when (re-search-forward "^ Dest:Projects" end t)
|
||||
(replace-match " Dest:Internal") )
|
||||
(when (re-search-forward "; CATEGORY: Meals" (save-excursion (re-search-forward "^$")) t)
|
||||
(replace-match "; CATEGORY: Travel" t))))))
|
||||
|
||||
(defun ledger-expense-personal ()
|
||||
"Makes the expense an personal one, eliminating metadata and receipts."
|
||||
(interactive)
|
||||
(when (eq major-mode 'ledger-mode) ; I made this local now, should only trigger in ldg-mode
|
||||
(save-excursion
|
||||
(end-of-line)
|
||||
(re-search-backward "^[0-9]\\{4\\}/")
|
||||
(let ((begin (point))
|
||||
(end (save-excursion (re-search-forward "^$"))))
|
||||
(when (re-search-forward "^ Dest:Projects" end t)
|
||||
(replace-match " Other:Personal"))
|
||||
(goto-char begin)
|
||||
(save-excursion
|
||||
(when (re-search-forward "^ +; ER:" end t)
|
||||
(beginning-of-line)
|
||||
(kill-line 1)))
|
||||
(save-excursion
|
||||
(when (re-search-forward "^ +; PROJECT:" end t)
|
||||
(beginning-of-line)
|
||||
(kill-line 1)))
|
||||
(save-excursion
|
||||
(when (re-search-forward "^ +; CATEGORY:" end t)
|
||||
(beginning-of-line)
|
||||
(kill-line 1)))
|
||||
(save-excursion
|
||||
(when (re-search-forward "^ +; RECEIPT:" end t)
|
||||
(beginning-of-line)
|
||||
(kill-line 1)))
|
||||
(ledger-toggle-current-entry)))))
|
||||
|
||||
(defun ledger-expense-show-receipt ()
|
||||
"Uses the Receipt buffer to show the receipt of the txn we're on."
|
||||
(when (eq major-mode 'ledger-mode) ; I made this local now, should only trigger in ldg-mode
|
||||
(save-excursion
|
||||
(end-of-line)
|
||||
(re-search-backward "^[0-9]\\{4\\}/")
|
||||
(let ((begin (point))
|
||||
(end (save-excursion (re-search-forward "^$"))))
|
||||
(save-excursion
|
||||
(when (re-search-forward "^\\( +; RECEIPT: +\\)\\([^,]+?.jpg\\).*$" end t)
|
||||
(ledger-matching-display-image
|
||||
(concat "/home/adamsrl/AdamsInfoServ/BusinessDocuments/Ledger/AdamsRussell/"
|
||||
(match-string 2))) ))))))
|
||||
342
contrib/raw/ledger-matching.el
Normal file
342
contrib/raw/ledger-matching.el
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
;; This library is intended to allow me to view a receipt on one panel, and tie it to ledger transactions in another
|
||||
|
||||
(require 'ldg-report)
|
||||
|
||||
(defgroup ledger-matching nil
|
||||
"Ledger image matching")
|
||||
|
||||
(defcustom ledger-matching-sourcedir "~/AdamsInfoServ/BusinessDocuments/Ledger/Incoming"
|
||||
"Source directory for images to process, ie: the incoming queue of images."
|
||||
:group 'ledger-matching)
|
||||
|
||||
(defcustom ledger-matching-destdir "~/AdamsInfoServ/BusinessDocuments/Ledger/AdamsRussell/Receipts"
|
||||
"Destination directory for images when matched, will still have a project directory appended to it."
|
||||
:group 'ledger-matching)
|
||||
|
||||
(defcustom ledger-matching-relative-receipt-dir "Receipts"
|
||||
"Relative directory root for destination images used in Ledger entries, will have the project directory appended and receipt filename."
|
||||
:group 'ledger-matching)
|
||||
|
||||
(defcustom ledger-matching-convert-binary "/usr/bin/convert"
|
||||
"Path to the Imagemagick convert command."
|
||||
:group 'ledger-matching)
|
||||
|
||||
(defcustom ledger-matching-scale 50
|
||||
"Scaling parameter to Imagemagick's convert to resize an image for viewing."
|
||||
:group 'ledger-matching)
|
||||
|
||||
(defcustom ledger-matching-rotation 0
|
||||
"Rotation parameter to Imagemagick's convert to rotate an image for viewing. Images on disk should always be upright for reading."
|
||||
:group 'ledger-matching)
|
||||
|
||||
|
||||
(defconst ledger-matching-image-buffer "*Receipt*"
|
||||
"Buffer name we load images into. Created if it doesn't exist, and persists across image loads.")
|
||||
|
||||
|
||||
(defvar ledger-matching-project "Internal"
|
||||
"The directory appended to the destination for the project code where receipts will be stored.")
|
||||
|
||||
(defvar ledger-matching-image-offset 0
|
||||
"The index of the current file from the SORTED source directory contents.")
|
||||
|
||||
(defvar ledger-matching-image-name nil
|
||||
"The filename only of the current image.")
|
||||
|
||||
|
||||
(defun ledger-matching-display-image (image-filename)
|
||||
"Resize the image and load it into our viewing buffer."
|
||||
|
||||
;; Create our viewing buffer if needed, and set it. Do NOT switch,
|
||||
;; this buffer isn't the primary. Let the user leave it where they
|
||||
;; place it.
|
||||
(unless (get-buffer ledger-matching-image-buffer)
|
||||
(get-buffer-create ledger-matching-image-buffer))
|
||||
(set-buffer ledger-matching-image-buffer)
|
||||
(erase-buffer)
|
||||
(goto-char (point-min))
|
||||
(insert-string image-filename "\n")
|
||||
|
||||
;; Convert the source to the temporary dest applying resizing and rotation
|
||||
(let* ((source (expand-file-name image-filename ledger-matching-sourcedir))
|
||||
(dest (make-temp-file "ledger-matching-" nil ".jpg"))
|
||||
(result (call-process ledger-matching-convert-binary nil (get-buffer "*Messages*") nil
|
||||
source
|
||||
"-scale" (concat (number-to-string ledger-matching-scale) "%")
|
||||
"-rotate" (number-to-string ledger-matching-rotation)
|
||||
dest)))
|
||||
|
||||
(if (/= 0 result)
|
||||
|
||||
;; Bomb out if the convert fails
|
||||
(message "Error running convert, see *Messages* buffer for details.")
|
||||
|
||||
;; Insert scaled image into the viewing buffer, replacing
|
||||
;; current contents Temp buffer is to force sync reading into
|
||||
;; memory of the jpeg due to async race condition with display
|
||||
;; and file deletion
|
||||
(let ((image (create-image (with-temp-buffer
|
||||
(insert-file-contents-literally dest)
|
||||
(string-as-unibyte (buffer-string)))
|
||||
'jpeg t)))
|
||||
(insert-image image)
|
||||
(goto-char (point-min))
|
||||
|
||||
;; Redisplay is required to prevent a race condition between displaying the image and the deletion. Apparently its async.
|
||||
;; Either redisplay or the above string method work, both together can't hurt.
|
||||
(redisplay)
|
||||
))
|
||||
|
||||
;; Delete our temporary file
|
||||
(delete-file dest)))
|
||||
|
||||
|
||||
|
||||
(defun ledger-matching-update-current-image ()
|
||||
"Grab the image from the source directory by offset and display"
|
||||
|
||||
(let* ((file-listing (directory-files ledger-matching-sourcedir nil "\.jpg$" nil))
|
||||
(len (safe-length file-listing)))
|
||||
|
||||
;; Ensure our offset doesn't exceed the file list
|
||||
(cond ((= len 0)
|
||||
(message "No files found in source directory."))
|
||||
|
||||
((< len 0)
|
||||
(message "Error, list of files should never be negative. Epic fail."))
|
||||
|
||||
((>= ledger-matching-image-offset len)
|
||||
(message "Hit end of list. Last image.")
|
||||
(setq ledger-matching-image-offset (1- len)))
|
||||
|
||||
((< ledger-matching-image-offset 0)
|
||||
(message "Beginning of list. First image.")
|
||||
(setq ledger-matching-image-offset 0)))
|
||||
|
||||
;; Get the name for the offset
|
||||
(setq ledger-matching-image-name (nth ledger-matching-image-offset file-listing))
|
||||
|
||||
(ledger-matching-display-image ledger-matching-image-name)))
|
||||
|
||||
|
||||
|
||||
(defun ledger-matching-image-offset-adjust (amount)
|
||||
"Incr/decr the offset and update the receipt buffer."
|
||||
|
||||
(setq ledger-matching-image-offset (+ ledger-matching-image-offset amount))
|
||||
(ledger-matching-update-current-image))
|
||||
|
||||
|
||||
|
||||
(defun ledger-receipt-matching ()
|
||||
"Open the receipt buffer and start with the first image."
|
||||
(interactive)
|
||||
(setq ledger-matching-image-offset 0)
|
||||
(ledger-matching-update-current-image))
|
||||
|
||||
|
||||
|
||||
(defun ledger-matching-tie-receipt-to-txn ()
|
||||
(interactive)
|
||||
(save-selected-window
|
||||
(ledger-report-visit-source)
|
||||
|
||||
;; Assumes we're in a narrowed buffer with ONLY this txn
|
||||
(backward-paragraph)
|
||||
(beginning-of-line)
|
||||
|
||||
;; ;; Update the ER and Project while I'm there
|
||||
;; (save-excursion
|
||||
;; (search-forward "; ER:")
|
||||
;; (kill-line nil)
|
||||
;; (insert " " *ledger-expense-shortcut-ER*))
|
||||
;; Just do the project for now.
|
||||
(save-excursion
|
||||
(search-forward "; PROJECT:")
|
||||
(kill-line nil)
|
||||
(insert " " *ledger-expense-shortcut-Proj*))
|
||||
|
||||
;; Goto the receipt line, unless their isn't one then add one
|
||||
(unless (search-forward "RECEIPT:" nil t)
|
||||
|
||||
;; Still at date line if that failed
|
||||
(next-line)
|
||||
(newline)
|
||||
(insert-string " ; RECEIPT:"))
|
||||
|
||||
;; Point immediately after : on tag
|
||||
|
||||
;; Check for existing jpg file
|
||||
(if (search-forward ".jpg" (line-end-position) t)
|
||||
|
||||
;; if present make it a comma delimited list
|
||||
(insert-string ",")
|
||||
|
||||
;; otherwise just add a space to pad
|
||||
(insert-string " "))
|
||||
|
||||
;; Add our relative filename as the value of the RECEIPT tag
|
||||
(insert-string (concat ledger-matching-relative-receipt-dir "/"
|
||||
ledger-matching-project "/"
|
||||
ledger-matching-image-name))
|
||||
|
||||
;; Create the destination project dir if it doesn't exist.
|
||||
(let ((full-destination (concat ledger-matching-destdir "/" ledger-matching-project )))
|
||||
(unless (file-accessible-directory-p full-destination)
|
||||
(make-directory full-destination t)))
|
||||
|
||||
;; Rename the file from the source directory to its permanent home
|
||||
(rename-file (concat ledger-matching-sourcedir "/"
|
||||
ledger-matching-image-name)
|
||||
(concat ledger-matching-destdir "/"
|
||||
ledger-matching-project "/"
|
||||
ledger-matching-image-name))
|
||||
|
||||
;; Update the receipt screen
|
||||
(ledger-matching-update-current-image)
|
||||
|
||||
(message "Filed %s to project %s" ledger-matching-image-name ledger-matching-project)))
|
||||
|
||||
|
||||
|
||||
(defun ledger-receipt-skip ()
|
||||
"Move the current image to the Skip directory because its not relevant."
|
||||
|
||||
(rename-file (concat ledger-matching-sourcedir "/"
|
||||
ledger-matching-image-name)
|
||||
(concat ledger-matching-sourcedir "/Skip/"
|
||||
ledger-matching-image-name))
|
||||
|
||||
;; Update the receipt screen at the same offset
|
||||
(ledger-matching-update-current-image))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; Items below are speed entry macros, and should eventually migrate to their own file.
|
||||
|
||||
(defvar *ledger-expense-shortcut-ER*
|
||||
"Current expense report number, just last four digits (ie: 1234 results in AISER1234).")
|
||||
|
||||
(defvar *ledger-expense-shortcut-split-ER*
|
||||
"Split (ie: internal) expense report number, just last four digits (ie: 1234 results in AISER1234).")
|
||||
|
||||
(defvar *ledger-expense-shortcut-Proj* ""
|
||||
"Current export report project code (ie: AGIL1292)")
|
||||
|
||||
(defun ledger-expense-shortcut-ER-format-specifier () *ledger-expense-shortcut-ER*)
|
||||
|
||||
(defun ledger-expense-shortcut-Project-format-specifier () *ledger-expense-shortcut-Proj*)
|
||||
|
||||
(defun ledger-expense-shortcut-setup (ER Split Proj)
|
||||
"Sets the variables expanded into the transaction."
|
||||
(interactive "MER Number (ER or IN and 4 digit number only): \nMSplit ER Number (ER or IN and 4 digit number only): \nMProject: ")
|
||||
(setq *ledger-expense-shortcut-ER*
|
||||
(concatenate 'string "AIS" ER))
|
||||
(setq *ledger-expense-shortcut-split-ER*
|
||||
(concatenate 'string "AIS" Split))
|
||||
(setq *ledger-expense-shortcut-Proj* Proj)
|
||||
(setq ledger-matching-project Proj)
|
||||
(message "Set Proj to %s and ER to %s, split to %s"
|
||||
*ledger-expense-shortcut-Proj*
|
||||
*ledger-expense-shortcut-ER*
|
||||
*ledger-expense-shortcut-split-ER*))
|
||||
|
||||
(defun ledger-expense-shortcut ()
|
||||
"Updates the ER and Project metadata with the current values of the shortcut variables."
|
||||
(interactive)
|
||||
(when (eq major-mode 'ledger-mode)
|
||||
(if (or (eql *ledger-expense-shortcut-ER* "")
|
||||
(eql *ledger-expense-shortcut-Proj* ""))
|
||||
(message "Run ledger-expense-shortcut-setup first.")
|
||||
(save-excursion
|
||||
(search-forward "; ER:")
|
||||
(kill-line nil)
|
||||
(insert " " *ledger-expense-shortcut-ER*))
|
||||
(save-excursion
|
||||
(search-forward "; PROJECT:")
|
||||
(kill-line nil)
|
||||
(insert " " *ledger-expense-shortcut-Proj*)))))
|
||||
|
||||
(defun ledger-expense-split ()
|
||||
"Splits the current transaction between internal and projects."
|
||||
(interactive)
|
||||
(when (eq major-mode 'ledger-mode) ; I made this local now, should only trigger in ldg-mode
|
||||
(save-excursion
|
||||
(end-of-line)
|
||||
(re-search-backward "^[0-9]\\{4\\}/")
|
||||
(re-search-forward "^ +Dest:Projects")
|
||||
(move-beginning-of-line nil)
|
||||
(let ((begin (point))
|
||||
(end (re-search-forward "^$")))
|
||||
(goto-char end)
|
||||
(insert (buffer-substring begin end))
|
||||
(goto-char end)
|
||||
(re-search-forward "^ Dest:Projects")
|
||||
(replace-match " Dest:Internal")
|
||||
(re-search-forward "; ER: +[A-Za-z0-9]+")
|
||||
(replace-match (concat "; ER: " *ledger-expense-shortcut-split-ER*) t)
|
||||
(when (re-search-forward "; CATEGORY: Meals" (save-excursion (re-search-forward "^$")) t)
|
||||
(replace-match "; CATEGORY: Travel" t))))
|
||||
(re-search-backward "^[0-9]\\{4\\}/")
|
||||
(re-search-forward "^ +Dest:Projects")
|
||||
(insert-string " $") ))
|
||||
|
||||
(defun ledger-expense-internal ()
|
||||
"Makes the expense an internal one."
|
||||
(interactive)
|
||||
(when (eq major-mode 'ledger-mode) ; I made this local now, should only trigger in ldg-mode
|
||||
(save-excursion
|
||||
(end-of-line)
|
||||
(re-search-backward "^[0-9]\\{4\\}/")
|
||||
(let ((begin (point))
|
||||
(end (save-excursion (re-search-forward "^$"))))
|
||||
(when (re-search-forward "^ Dest:Projects" end t)
|
||||
(replace-match " Dest:Internal") )
|
||||
(when (re-search-forward "; CATEGORY: Meals" (save-excursion (re-search-forward "^$")) t)
|
||||
(replace-match "; CATEGORY: Travel" t))))))
|
||||
|
||||
(defun ledger-expense-personal ()
|
||||
"Makes the expense an personal one, eliminating metadata and receipts."
|
||||
(interactive)
|
||||
(when (eq major-mode 'ledger-mode) ; I made this local now, should only trigger in ldg-mode
|
||||
(save-excursion
|
||||
(end-of-line)
|
||||
(re-search-backward "^[0-9]\\{4\\}/")
|
||||
(let ((begin (point))
|
||||
(end (save-excursion (re-search-forward "^$"))))
|
||||
(when (re-search-forward "^ Dest:Projects" end t)
|
||||
(replace-match " Other:Personal"))
|
||||
(goto-char begin)
|
||||
(save-excursion
|
||||
(when (re-search-forward "^ +; ER:" end t)
|
||||
(beginning-of-line)
|
||||
(kill-line 1)))
|
||||
(save-excursion
|
||||
(when (re-search-forward "^ +; PROJECT:" end t)
|
||||
(beginning-of-line)
|
||||
(kill-line 1)))
|
||||
(save-excursion
|
||||
(when (re-search-forward "^ +; CATEGORY:" end t)
|
||||
(beginning-of-line)
|
||||
(kill-line 1)))
|
||||
(save-excursion
|
||||
(when (re-search-forward "^ +; RECEIPT:" end t)
|
||||
(beginning-of-line)
|
||||
(kill-line 1)))
|
||||
(ledger-toggle-current-entry)))))
|
||||
|
||||
(defun ledger-expense-show-receipt ()
|
||||
"Uses the Receipt buffer to show the receipt of the txn we're on."
|
||||
(when (eq major-mode 'ledger-mode) ; I made this local now, should only trigger in ldg-mode
|
||||
(save-excursion
|
||||
(end-of-line)
|
||||
(re-search-backward "^[0-9]\\{4\\}/")
|
||||
(let ((begin (point))
|
||||
(end (save-excursion (re-search-forward "^$"))))
|
||||
(save-excursion
|
||||
(when (re-search-forward "^\\( +; RECEIPT: +\\)\\([^,]+?.jpg\\).*$" end t)
|
||||
(ledger-matching-display-image
|
||||
(concat "/home/adamsrl/AdamsInfoServ/BusinessDocuments/Ledger/AdamsRussell/"
|
||||
(match-string 2))) ))))))
|
||||
|
||||
|
||||
(provide 'ledger-matching)
|
||||
90
contrib/raw/ledger-shell-environment-functions
Normal file
90
contrib/raw/ledger-shell-environment-functions
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
# Environment for ledger expenses
|
||||
|
||||
[ $(whoami) == "adamsrl" ] \
|
||||
&& export LEDGER_HOME="/home/adamsrl/AdamsInfoServ/BusinessDocuments/Ledger/AdamsRussell" \
|
||||
|| export LEDGER_HOME="/home/Heather/AdamsRussell"
|
||||
|
||||
[ $(hostname) == "cardamom" ] \
|
||||
&& export LEDGER_BIN="${LEDGER_HOME}/ledger" \
|
||||
|| export LEDGER_BIN="${LEDGER_HOME}/ledger.exe"
|
||||
|
||||
[ $(whoami) == "andersonll" ] \
|
||||
&& export LEDGER_HOME="/home/andersonll/AdamsInfoServ/Expenses" \
|
||||
&& export LEDGER_BIN="${LEDGER_HOME}/ledger"
|
||||
|
||||
# Common reports
|
||||
|
||||
alias ledger='${LEDGER_BIN} -f "${LEDGER_HOME}/.ledger" -VE '
|
||||
alias ERSummary='ledger --pivot ER bal | egrep "AIS(ER|IN)[0-9]+|Unassigned"'
|
||||
|
||||
function ERTxns() {
|
||||
[ -z "$1" ] && echo "Please specify an ER number (ie: AISER0042)." && return
|
||||
|
||||
ledger reg "%ER=${1}"
|
||||
}
|
||||
|
||||
function ERCategorySummary() {
|
||||
[ -z "$1" ] && echo "Please specify an ER number (ie: AISER0042)." && return
|
||||
|
||||
ledger bal --pivot CATEGORY "%ER=${1}"
|
||||
}
|
||||
|
||||
function ERMealSummary() {
|
||||
[ -z "$1" ] && echo "Please specify an ER number (ie: AISER0042)." && return
|
||||
|
||||
ledger reg "%ER=${1}" and %CATEGORY=Meals -D
|
||||
}
|
||||
|
||||
function ERMeals() {
|
||||
[ -z "$1" ] && echo "Please specify an ER number (ie: AISER0042)." && return
|
||||
|
||||
ledger reg "%ER=${1}" and %CATEGORY=Meals
|
||||
}
|
||||
|
||||
function ERUncleared() {
|
||||
[ -z "$1" ] && echo "Please specify an ER number (ie: AISER0042)." && return
|
||||
|
||||
ledger reg "%ER=${1}" -U
|
||||
}
|
||||
|
||||
function ERMissingReceipts() {
|
||||
[ -z "$1" ] && echo "Please specify an ER number (ie: AISER0042)." && return
|
||||
|
||||
ledger reg "%ER=${1}" and not %RECEIPT
|
||||
}
|
||||
|
||||
function ERVerify() {
|
||||
[ -z "$1" ] && echo "Please specify an ER number (ie: AISER0042)." && return
|
||||
|
||||
echo "========== Uncleared txns below =========="
|
||||
ERUncleared "$1"
|
||||
echo "========== Missing receipts below (miles and stubs ok) =========="
|
||||
ERMissingReceipts "$1"
|
||||
echo "========== Category Summary (airline? mileage? car? hotel? =========="
|
||||
ERCategorySummary "$1"
|
||||
echo "========== Meal summary (<\$50 / day unless otherwise specified) =========="
|
||||
ERMealSummary "$1"
|
||||
echo "========== Account Verification (Internal vs Project ER should be ONE type) =========="
|
||||
echo $1 | grep AISIN >/dev/null 2>&1 \
|
||||
|| { ledger reg "%ER=${1}" | grep Dest:Internal ; } \
|
||||
&& { ledger reg "%ER=${1}" | grep Dest:Projects ; }
|
||||
echo "========== Project Verification (only one project code should be listed) =========="
|
||||
ledger print "%ER=${1}" | grep PROJECT | sort -u
|
||||
echo "========== Receipts missing =========="
|
||||
ledger print "%ER=${1}" | grep -h '; RECEIPT: ' \
|
||||
| sed 's,\W*; RECEIPT: ,,g' \
|
||||
| tr , '\n' \
|
||||
| sort -u \
|
||||
| while read X ; do
|
||||
[ -f "${LEDGER_HOME}/${X}" ] \
|
||||
|| echo XX $X
|
||||
done
|
||||
}
|
||||
|
||||
function ERListing() {
|
||||
ledger reg Stub --register-format="%(tag('ER')) %(tag('NOTE'))\n" | sort -u
|
||||
}
|
||||
|
||||
function ERQueue() {
|
||||
ledger reg %ER=Unassigned --prepend-format="%(filename) "
|
||||
}
|
||||
|
|
@ -2325,7 +2325,9 @@ doing it.
|
|||
|
||||
Journal files are simple free text files easily modified by any text
|
||||
editor. A special mode for EMACS is included with the source
|
||||
distribution.
|
||||
distribution. This mode provides syntax highlighting, a reconcile mode
|
||||
and a report mode. This makes it quote possible to use ledger without
|
||||
ever leaving EMACS.
|
||||
|
||||
@cindex EMACS .emacs file
|
||||
|
||||
|
|
@ -2335,7 +2337,7 @@ Add the following line to your @file{.emacs} (or equivalent,
|
|||
(load "ldg-new")
|
||||
@end smallexample
|
||||
|
||||
Copy the several lisp files from the source lisp directory your your
|
||||
Copy the several lisp files (@file{ldg-*.el}) from the source lisp directory your your
|
||||
@file{site-lisp} directory, or add the ledger lisp source directory to
|
||||
your EMACS load path by adding:
|
||||
@smallexample
|
||||
|
|
@ -2352,7 +2354,8 @@ To enter ledger-mode on a new file, type @command{M-x ledger-mode}.
|
|||
|
||||
Once you have loaded a Journal file into EMACS, you have several
|
||||
commands available to make entering, clearing and reconciling
|
||||
transactions and producing reports:
|
||||
transactions and producing reports (these are also available from the
|
||||
menu if you can't remember the key combinations):
|
||||
|
||||
@cindex EMACS commands
|
||||
@table @code
|
||||
|
|
@ -2431,10 +2434,11 @@ will be interpreted as a new account by ledger.
|
|||
@code{C-c C-a} will run the @code{ledger entry} command (@pxref{entry
|
||||
and xact}) from within EMACS. When typed, the mini-buffer will appear
|
||||
with the current year and month, waiting for you to enter the day and
|
||||
the payee. Ledger will generate a new entry based on the most recent
|
||||
entry for that payee, using the amount and accounts from that
|
||||
transaction. If you have a new amount simply type the amount after the
|
||||
payee. For example, if your journal contains an entry
|
||||
the payee, and optionally, a commoditized amount. Ledger will generate
|
||||
a new entry based on the most recent entry for that payee, using the
|
||||
amount and accounts from that transaction. If you have a new amount
|
||||
simply type the amount after the payee. For example, if your journal
|
||||
contains an entry
|
||||
@smallexample
|
||||
2011/11/25 Viva Italiano
|
||||
Expenses:Food $12.45
|
||||
|
|
@ -2454,7 +2458,10 @@ Entry: 2011/11/28 viva food 34 tip 7 <enter>
|
|||
Liabilities:MasterCard
|
||||
@end smallexample
|
||||
@noindent Notice that the entry will appear at the correct place in the journal
|
||||
ordered by date, not necessarily at the bottom of the file.
|
||||
ordered by date, not at the bottom of the file. If you need to include
|
||||
spaces in the payee name, then surrond the name of the payee with double
|
||||
quotes, otherwise ledger will interpret the second part of the name as
|
||||
an account.
|
||||
@node Clearing Transactions, , Automagically Adding new entries, Working with entries
|
||||
@subsubsection Clearing Transactions and Postings
|
||||
@cindex clearing transactions in EMACS
|
||||
|
|
@ -2483,21 +2490,46 @@ toggled.
|
|||
@node Reconciling accounts, Generating Reports, Working with entries, Using EMACS
|
||||
@subsection Reconciling accounts
|
||||
|
||||
In the reconcile buffer, use SPACE to toggle the cleared status of a
|
||||
transaction, C-x C-s to save changes (to the ledger file as well).
|
||||
Enter the reconcile mode using the menu entry, or @code{C-c C-r}. Emacs
|
||||
will prompt you for an account name, then display a second buffer with
|
||||
all of the uncleared transactions. The reconcile buffer has several functions:
|
||||
|
||||
@table @code
|
||||
@item SPACE
|
||||
toggles the cleared status of a transaction, and show cleared balance inthe minibuffer
|
||||
@item RETURN
|
||||
moves the cursor to that transaction in the ledger.
|
||||
@item C-x C-s
|
||||
to save changes (to the ledger file as well).
|
||||
@item q
|
||||
quite the reconcile mode
|
||||
@item n p
|
||||
next line or previous line
|
||||
@item A
|
||||
add entry
|
||||
@item D
|
||||
delete entry
|
||||
@item C-l
|
||||
refresh display
|
||||
@end table
|
||||
@node Generating Reports, , Reconciling accounts, Using EMACS
|
||||
@subsection Generating Reports
|
||||
|
||||
The ledger reports command asks the user to select a report to run then
|
||||
creates a report buffer containing the results of running the associated
|
||||
command line. Its' behavior is modified by a prefix argument which,
|
||||
when given, causes the generated command line that will be used to
|
||||
create the report to be presented for editing before the report is
|
||||
actually run. Arbitrary unnamed command lines can be run by specifying
|
||||
an empty name for the report. The command line used can later be named
|
||||
and saved for future use as a named report from the generated reports
|
||||
buffer.
|
||||
command line. This allows you to run frequently used reports without
|
||||
retyping the command line, or writing shell scripts for simple one line
|
||||
commands.
|
||||
|
||||
To generate a report, select the @code{Run Reports} menu, or type
|
||||
@code{C-c C-o C-r}. Emacs will prompt for a report name. If it
|
||||
recognizes the name it will run the report again. If it is a new name,
|
||||
or blank it will respond by giving you an example command line to edit.
|
||||
Hitting return willrun the report and present it in a new buffer.
|
||||
|
||||
If you have given it a new name, then @code{s} will save the report for
|
||||
future use.
|
||||
|
||||
|
||||
In a report buffer, the following keys are available:
|
||||
@table @code
|
||||
|
|
@ -2542,7 +2574,7 @@ kill the report buffer
|
|||
* Lot dates::
|
||||
* Lot notes::
|
||||
* Lot value expressions::
|
||||
* Automated Transactions::
|
||||
* Automated Transactions::
|
||||
@end menu
|
||||
|
||||
@node Basic format, Eliding amounts, Transactions , Transactions
|
||||
|
|
@ -4744,6 +4776,7 @@ database files.
|
|||
@menu
|
||||
* accounts::
|
||||
* commodities::
|
||||
* tags::
|
||||
* entry and xact::
|
||||
* payees::
|
||||
@end menu
|
||||
|
|
@ -4753,15 +4786,27 @@ database files.
|
|||
|
||||
The @command{accounts} reports all of the accounts in the journal.
|
||||
Following the command with a regular expression will limit the output to
|
||||
accounts matching the regex.
|
||||
accounts matching the regex. The output is sorted by name. Using the
|
||||
@code{--count} option will tell you haw many entries use each account.
|
||||
|
||||
|
||||
@node commodities, entry and xact, accounts, Reports about your Journals
|
||||
@node commodities, tags, accounts, Reports about your Journals
|
||||
@subsection @command{commodities}
|
||||
Report all commodities present in the journals under consideration.
|
||||
Report all commodities present in the journals under consideration. The
|
||||
output is sorted by name. Using the @code{--count} option will tell
|
||||
you haw many entries use each commodity.
|
||||
|
||||
@node tags, entry and xact, commodities, Reports about your Journals
|
||||
@subsection @command{tags}
|
||||
|
||||
The @command{tags} reports all of the tags in the journal. The output
|
||||
is sorted by name. Using the @code{--count} option will tell you haw
|
||||
many entries use each tag. Using the @code{--values} option will report
|
||||
the values used by each tag.
|
||||
|
||||
|
||||
@node entry and xact, payees, commodities, Reports about your Journals
|
||||
|
||||
@node entry and xact, payees, tags, Reports about your Journals
|
||||
@subsection @command{draft}, @command{entry} and @command{xact}
|
||||
|
||||
The @code{draft}, @code{entry} and @command{xact} commands simplify the
|
||||
|
|
|
|||
|
|
@ -246,72 +246,54 @@ the default."
|
|||
(ledger-reports-custom-save))
|
||||
report-cmd))
|
||||
|
||||
(defvar ledger-report-patch-alist nil)
|
||||
|
||||
(defun ledger-report-patch-reports (buf)
|
||||
(when ledger-report-patch-alist
|
||||
(let ((entry (assoc (expand-file-name (buffer-file-name buf))
|
||||
ledger-report-patch-alist)))
|
||||
(when entry
|
||||
(dolist (b (cdr entry))
|
||||
(if (buffer-live-p b)
|
||||
(with-current-buffer b
|
||||
(save-excursion
|
||||
(goto-char (point-min))
|
||||
(while (not (eobp))
|
||||
(let ((record (get-text-property (point) 'ledger-source)))
|
||||
(if (and record (not (markerp (cdr record))))
|
||||
(setcdr record (with-current-buffer buf
|
||||
(save-excursion
|
||||
(goto-char (point-min))
|
||||
(forward-line (cdr record))
|
||||
(point-marker))))))
|
||||
(forward-line 1))))))))))
|
||||
|
||||
(defun ledger-do-report (cmd)
|
||||
"Run a report command line."
|
||||
(goto-char (point-min))
|
||||
(insert (format "Report: %s\n" ledger-report-name)
|
||||
(format "Command: %s\n" cmd)
|
||||
(make-string (- (window-width) 1) ?=)
|
||||
"\n")
|
||||
(let ((register-report (string-match " reg\\(ister\\)? " cmd))
|
||||
"\n\n")
|
||||
(let ((data-pos (point))
|
||||
(register-report (string-match " reg\\(ister\\)? " cmd))
|
||||
files-in-report)
|
||||
(shell-command
|
||||
(if register-report
|
||||
(concat cmd " --prepend-format='%(filename):%(beg_line):'")
|
||||
cmd) t nil)
|
||||
(when register-report
|
||||
(goto-char (point-min))
|
||||
(goto-char data-pos)
|
||||
(while (re-search-forward "^\\([^:]+\\)?:\\([0-9]+\\)?:" nil t)
|
||||
(let ((file (match-string 1))
|
||||
(line (string-to-number (match-string 2))))
|
||||
(line (string-to-number (match-string 2))))
|
||||
(delete-region (match-beginning 0) (match-end 0))
|
||||
(set-text-properties (line-beginning-position) (line-end-position)
|
||||
(list 'ledger-source (cons file line)))
|
||||
(let* ((fullpath (expand-file-name file))
|
||||
(entry (assoc fullpath ledger-report-patch-alist)))
|
||||
(if entry
|
||||
(nconc (cdr entry) (list (current-buffer)))
|
||||
(push (cons (expand-file-name file)
|
||||
(list (current-buffer)))
|
||||
ledger-report-patch-alist))
|
||||
(add-to-list 'files-in-report fullpath)))
|
||||
(list 'ledger-source (cons file (save-window-excursion
|
||||
(save-excursion
|
||||
(find-file file)
|
||||
(widen)
|
||||
(goto-char (point-min))
|
||||
(forward-line (1- line))
|
||||
(point-marker))))))
|
||||
(end-of-line))))
|
||||
(goto-char data-pos)))
|
||||
|
||||
(dolist (path files-in-report)
|
||||
(let ((buf (get-file-buffer path)))
|
||||
(if (and buf (buffer-live-p buf))
|
||||
(ledger-report-patch-reports buf))))))))
|
||||
|
||||
(defun ledger-report-visit-source ()
|
||||
(interactive)
|
||||
(let ((prop (get-text-property (point) 'ledger-source)))
|
||||
(destructuring-bind (file . line-or-marker) prop
|
||||
(find-file-other-window file)
|
||||
(widen)
|
||||
(if (markerp line-or-marker)
|
||||
(goto-char line-or-marker)
|
||||
(goto-char (point-min))
|
||||
(forward-line (1- line-or-marker))))))
|
||||
(forward-line (1- line-or-marker))
|
||||
(re-search-backward "^[0-9]+")
|
||||
(beginning-of-line)
|
||||
(let ((start-of-txn (point)))
|
||||
(forward-paragraph)
|
||||
(narrow-to-region start-of-txn (point))
|
||||
(backward-paragraph))))))
|
||||
|
||||
(defun ledger-report-goto ()
|
||||
"Goto the ledger report buffer."
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue