2 # make_invoice makes latex invoices, and is released
3 # under the terms of the GPL version 2, or any later version, at your
4 # option. See the file README and COPYING for more information.
5 # Copyright 2008 by Don Armstrong <don@donarmstrong.com>.
6 # $Id: perl_script 495 2006-08-10 08:02:01Z don $
17 make_invoice - makes invoices using latex
21 make_invoice [options]
24 --log,-l the log file to use to make the invoice
25 --template,-t the template to use to make the invoice
26 --min-time-interval, -m minimum time to bill, default 0
27 --time-granularity, -g time granularity, default 0
28 --hourly-fee, -f hourly fee, default 50.00
29 --svn,-s whether to use subversion or not
30 --debug, -d debugging level (Default 0)
31 --help, -h display this help
32 --man, -m display manual
40 The log file to use to generate the invoice
42 =item B<--template, -t>
44 The tex template to use to generate the invoice
48 Whether to use subversion or not; defaults to yes if .svn exists in
49 the current directory.
53 Invoice directory to place invoice in (automatically calculated if not
56 =item B<--min-time-interval, -m>
58 Minimum time interval to bill, defaults to 0.
60 =item B<--time-granularity, -g>
62 Time granularity, defaults to 0.
64 =item B<--hourly-fee,-f>
66 Hourly fee, defaults to 50.00
70 Only output the LaTeX file
74 Only output the log file
78 Debug verbosity. (Default 0)
82 Display brief useage information.
99 use POSIX qw(ceil strftime);
103 use Params::Validate qw(validate_with :types);
105 my %options = (log => undef,
109 time_interval => 0.00,
110 time_granularity => 0.00,
111 hourly_fee => '50.00',
119 GetOptions(\%options,
120 'log|l=s','template|t=s','invoice|i=s','svn|s!',
121 'time_granularity|time-granularity|g=s',
122 'time_interval|min-time-interval|T=s',
123 'hourly_fee|hourly-fee|f=s',
126 'debug|d+','help|h|?','man|m');
128 pod2usage() if $options{help};
129 pod2usage({verbose=>2}) if $options{man};
131 $DEBUG = $options{debug};
134 if (not defined $options{log}) {
135 push @USAGE_ERRORS,"You must pass a log file with --log";
137 if (not defined $options{template}) {
138 push @USAGE_ERRORS,"You must pass a template file with --template";
141 pod2usage(join("\n",@USAGE_ERRORS)) if @USAGE_ERRORS;
143 if (not defined $options{svn}) {
144 $options{svn} = -e '.svn';
148 my $log_fh = IO::File->new($options{log},'r') or
149 die "Unable to open $options{log} for reading: $!";
150 my $template_fh = IO::File->new($options{template},'r') or
151 die "Unable to open $options{template} for reading: $!";
155 my $tex_log = <<'EOF';
156 \setlength\LTleft{0pt plus 1fill minus 1fill}%
158 \begin{longtable}{|p{9cm}|r|r|r|r|}%
161 \mbox{Description} & Item Cost & Quantity & Cost & Total \\
164 my $totaldelta = undef;
168 my $first_date = undef;
169 my $last_date = undef;
177 next if /^Total: \d+\.\d{2}$/;
179 print STDERR $_."\n";
181 $tex_log .= format_events(date => $date,
190 s/\s*\[[\.\d]+\]\s*\[[\.\d]+\]\s*$//;
192 if (/\s*\*\s*CLOCK:\s+\[([^\]]+)\]--\[([^\]]+)\]/ or
193 /^\s*\*\s*(.+)?\s* - \s*(.+)?\s*$/
195 $d1 = UnixDate(ParseDate($1),'%s');
196 $d2 = UnixDate(ParseDate($2),'%s');
197 if (not defined $d1) {
198 die "Invalid date: $1";
200 if (not defined $d2) {
201 die "Invalid date: $2";
204 die "malformed line $_";
206 my $string = '* '.strftime('%A, %B %e, %H:%M:%S',localtime($d1)).' - '.
207 strftime('%A, %B %e, %H:%M:%S',localtime($d2));
208 if (not defined $first_date) {
215 my $hours = $delta / (60*60);
216 if ($hours < $options{time_interval}) {
217 $hours = $options{time_interval}
219 if ($options{time_granularity} > 0) {
220 $hours = ceil($hours / $options{time_granularity})*$options{time_granularity};
223 $totaldelta += $delta;
224 $calc_log .= $string.q( [).sprintf('%.2f',$hours).qq(] [).sprintf('%.2f',$totaldelta/(60*60)).qq(]\n);
226 elsif (/^\s+-\s*(.+)/) {
230 $calc_log .= $_.qq(\n);
233 $calc_log .= $_.qq(\n);
236 $calc_log .= "\nTotal: ".sprintf('%.2f',$totaldelta/(60*60)).qq(\n);
238 $tex_log .= format_events(date => $date,
251 \multicolumn{4}{|r|}{\textbf{Total}} & \$%
254 $tex_log .= sprintf('%.2f',$total)."%\n";
265 $template = <$template_fh>;
268 my $invoice_start = strftime('%c',localtime($first_date));
269 my $invoice_stop = strftime('%c',localtime($last_date));
271 my $tt = Text::Template->new(TYPE=>'string',
273 DELIMITERS => ['{--','--}'],
275 my $tex_invoice = $tt->fill_in(HASH=>{start => $invoice_start,
276 stop => $invoice_stop,
278 total => sprintf('%0.2f',$total),
281 if (not defined $tex_invoice) {
282 die $Text::Template::ERROR;
285 if ($options{log_only}) {
290 if ($options{tex_only}) {
296 my $invoice_date = strftime('%Y_%m_%d',localtime($last_date));
297 my $invoice_dir = "invoice_$invoice_date";
299 if (not -d $invoice_dir) {
301 system('svn','mkdir',$invoice_dir) == 0 or
302 die "Unable to create invoice directory $invoice_dir";
305 system('mkdir','-p',$invoice_dir) == 0 or
306 die "Unable to create invoice directory $invoice_dir";
311 if (-e 'common_makefile' and not -e '$invoice_dir/Makefile') {
313 system('ln','-sf','../common_makefile','Makefile') == 0 or
314 die "Unable to link common_makefile to Makefile";
316 system('svn','add','Makefile') == 0 or
317 die "Unable to add Makefile";
322 # now we write stuff out
324 my $calc_log_fh = IO::File->new("log_${invoice_date}",'w') or
325 die "Unable to open log_${invoice_date} for writing: $!";
326 print {$calc_log_fh} $calc_log;
329 my $tex_invoice_fh = IO::File->new("invoice_${invoice_date}.tex",'w') or
330 die "Unable to open log_${invoice_date} for writing: $!";
331 print {$tex_invoice_fh} $tex_invoice;
332 close($tex_invoice_fh);
336 "log_${invoice_date}",
337 "invoice_${invoice_date}.tex",
338 ) == 0 or die "Unable to add log and invoice to svn";
339 system('svn','propset','svn:ignore',
340 "*.aux\n*.log\n*.dvi\n*.ps\n*.pdf\nauto\n",
342 ) == 0 or die "Unable to set svn:ignore";
347 my %param = validate_with(params => \@_,
348 spec => {time => {type => SCALAR,
350 date => {type => SCALAR,
352 date2 => {type => SCALAR,
354 total => {type => SCALARREF,
356 events => {type => ARRAYREF,
360 ${$param{total}} += $param{time} * $options{hourly_fee};
362 # $param{date} =~ s/\s+\d+\:\d+\:\d+\s+[A-Z]{0,3}\s*//;
363 my $output = '\hline'."\n".' \mbox{'.strftime('%A, %B %e, %H:%M',localtime($param{date})).
364 ' to '.strftime('%H:%M %Z',localtime($param{date2}))."}\n\n".
365 ' \begin{itemize*}'."\n";
366 $output .= join('',map { s/_/\\_/g; " \\item $_\n";} @{$param{events}});
367 $output .= ' \end{itemize*} & \$'.sprintf('%.2f',$options{hourly_fee}).' & '.sprintf('%.2f',$param{time}).
368 ' & \$'.sprintf('%.2f',$param{time}*$options{hourly_fee}).' & \$'.
369 sprintf('%.2f',${$param{total}}) .
374 ## sub format_events{
375 ## my ($date,$date2,$time,@events) = @_;
376 ## my $output = ' \Fee{'.strftime('%A, %B %e, %H:%M',localtime(UnixDate($date,'%s'))).
377 ## ' to '.strftime('%H:%M %Z',localtime(UnixDate($date2,'%s')))."\n".
378 ## ' \begin{itemize*}'."\n";
379 ## $output .= join('',map {" \\item $_\n"} @events);
380 ## $output .= ' \end{itemize*}}{50.00}{'.$time.'}'."\n";