Added raw copies of my custom ledger scripts and elisp additions. Next need to isolate features and make them more generic for reuse by other Ledger users.

This commit is contained in:
adamsrl 2012-08-22 18:27:23 -05:00
parent f01e09e519
commit 6de14e0867
6 changed files with 1057 additions and 0 deletions

View 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"`;
}

View 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

14
contrib/raw/VerifyImages.sh Executable file
View 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
View 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))) ))))))

View file

@ -0,0 +1,212 @@
;; 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*))
(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) ))
(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))
(provide 'ledger-matching)

View 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) "
}