]> git.donarmstrong.com Git - bin.git/blob - make_invoice
Abstract out GIT_HOST
[bin.git] / make_invoice
1 #! /usr/bin/perl
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 $
7
8
9 use warnings;
10 use strict;
11
12 use Getopt::Long;
13 use Pod::Usage;
14
15 =head1 NAME
16
17 make_invoice - makes invoices using latex
18
19 =head1 SYNOPSIS
20
21  make_invoice [options]
22
23  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
33
34 =head1 OPTIONS
35
36 =over
37
38 =item B<--log, -l>
39
40 The log file to use to generate the invoice
41
42 =item B<--template, -t>
43
44 The tex template to use to generate the invoice
45
46 =item B<--svn, -s>
47
48 Whether to use subversion or not; defaults to yes if .svn exists in
49 the current directory.
50
51 =item B<--invoice,-i>
52
53 Invoice directory to place invoice in (automatically calculated if not
54 passed.)
55
56 =item B<--min-time-interval, -m>
57
58 Minimum time interval to bill, defaults to 0.
59
60 =item B<--time-granularity, -g>
61
62 Time granularity, defaults to 0.
63
64 =item B<--hourly-fee,-f>
65
66 Hourly fee, defaults to 50.00
67
68 =item B<--tex-only>
69
70 Only output the LaTeX file
71
72 =item B<--log-only>
73
74 Only output the log file
75
76 =item B<--debug, -d>
77
78 Debug verbosity. (Default 0)
79
80 =item B<--help, -h>
81
82 Display brief useage information.
83
84 =item B<--man, -m>
85
86 Display this manual.
87
88 =back
89
90 =head1 EXAMPLES
91
92
93 =cut
94
95
96 use vars qw($DEBUG);
97
98 use Date::Manip;
99 use POSIX qw(ceil strftime);
100 use Cwd qw(cwd);
101 use Text::Template;
102
103 use Params::Validate qw(validate_with :types);
104
105 my %options = (log             => undef,
106                template        => undef,
107                svn             => undef,
108                invoice         => undef,
109                time_interval   => 0.00,
110                time_granularity => 0.00,
111                hourly_fee      => '50.00',
112                debug           => 0,
113                help            => 0,
114                man             => 0,
115                log_only        => 0,
116                tex_only        => 0,
117                );
118
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',
124            'log_only|log-only',
125            'tex_only|tex-only',
126            'debug|d+','help|h|?','man|m');
127
128 pod2usage() if $options{help};
129 pod2usage({verbose=>2}) if $options{man};
130
131 $DEBUG = $options{debug};
132
133 my @USAGE_ERRORS;
134 if (not defined $options{log}) {
135      push @USAGE_ERRORS,"You must pass a log file with --log";
136 }
137 if (not defined $options{template}) {
138      push @USAGE_ERRORS,"You must pass a template file with --template";
139 }
140
141 pod2usage(join("\n",@USAGE_ERRORS)) if @USAGE_ERRORS;
142
143 if (not defined $options{svn}) {
144      $options{svn} = -e '.svn';
145 }
146
147
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: $!";
152
153
154 my $calc_log = '';
155 my $tex_log = <<'EOF';
156 \setlength\LTleft{0pt plus 1fill minus 1fill}%
157 \let\LTright\LTleft
158 \begin{longtable}{|p{9cm}|r|r|r|r|}%
159 %  \caption*{}
160 \hline
161   \mbox{Description} & Item Cost & Quantity & Cost & Total \\
162 \endhead
163 EOF
164 my $totaldelta = undef;
165
166 my $total = 0;
167
168 my $first_date = undef;
169 my $last_date = undef;
170 my $time = undef;
171 my $date = undef;
172 my $date2 = undef;
173 my @events;
174
175 while (<$log_fh>) {
176      chomp;
177      next if /^Total: \d+\.\d{2}$/;
178      if (/^\s*\* /) {
179          print STDERR $_."\n";
180           if (defined $time) {
181                $tex_log .= format_events(date => $date,
182                                          date2 => $date2,
183                                          time => $time,
184                                          total => \$total,
185                                          events => \@events);
186                @events = ();
187                $date = undef;
188                $time = undef;
189           }
190           s/\s*\[[\.\d]+\]\s*\[[\.\d]+\]\s*$//;
191           my ($d1,$d2);
192          if (/\s*\*\s*CLOCK:\s+\[([^\]]+)\]--\[([^\]]+)\]/ or
193              /^\s*\*\s*(.+)?\s* - \s*(.+)?\s*$/
194             ) {
195               $d1 = UnixDate(ParseDate($1),'%s');
196               $d2 = UnixDate(ParseDate($2),'%s');
197               if (not defined $d1) {
198                   die "Invalid date: $1";
199               }
200               if (not defined $d2) {
201                   die "Invalid date: $2";
202               }
203           } else {
204               die "malformed line $_";
205           }
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) {
209                $first_date = $d1;
210           }
211           $last_date = $d2;
212           my $delta = $d2-$d1;
213           $date = $d1;
214           $date2 = $d2;
215           my $hours = $delta / (60*60);
216           if ($hours < $options{time_interval}) {
217                $hours = $options{time_interval}
218           }
219           if ($options{time_granularity} > 0) {
220                $hours = ceil($hours / $options{time_granularity})*$options{time_granularity};
221           }
222           $time = $hours;
223           $totaldelta += $delta;
224           $calc_log .= $string.q( [).sprintf('%.2f',$hours).qq(] [).sprintf('%.2f',$totaldelta/(60*60)).qq(]\n);
225      }
226      elsif (/^\s+-\s*(.+)/) {
227           my $event = $1;
228           chomp $event;
229           push @events,$event;
230           $calc_log .= $_.qq(\n);
231      }
232      else {
233           $calc_log .= $_.qq(\n);
234      }
235 }
236 $calc_log .= "\nTotal: ".sprintf('%.2f',$totaldelta/(60*60)).qq(\n);
237 if (defined $time) {
238     $tex_log .= format_events(date => $date,
239                                date2 => $date2,
240                               time => $time,
241                               total => \$total,
242                               events => \@events);
243      @events = ();
244      $date = undef;
245      $date2 = undef;
246      $time = undef;
247 }
248
249 $tex_log .= <<'EOF';
250 \hline\hline
251 \multicolumn{4}{|r|}{\textbf{Total}} & \$%
252 EOF
253
254 $tex_log .= sprintf('%.2f',$total)."%\n";
255
256 $tex_log .= <<'EOF';
257 \\
258 \hline
259 \end{longtable}
260 EOF
261
262 my $template;
263 {
264      local $/;
265      $template = <$template_fh>;
266 }
267
268 my $invoice_start = strftime('%c',localtime($first_date));
269 my $invoice_stop = strftime('%c',localtime($last_date));
270
271 my $tt = Text::Template->new(TYPE=>'string',
272                              SOURCE => $template,
273                              DELIMITERS => ['{--','--}'],
274                             );
275 my $tex_invoice = $tt->fill_in(HASH=>{start => $invoice_start,
276                                       stop  => $invoice_stop,
277                                       log   => $tex_log,
278                                       total => sprintf('%0.2f',$total),
279                                      }
280                               );
281 if (not defined $tex_invoice) {
282      die $Text::Template::ERROR;
283 }
284
285 if ($options{log_only}) {
286     print $calc_log;
287     exit 0;
288 }
289
290 if ($options{tex_only}) {
291     print $tex_invoice;
292     exit 0;
293 }
294
295
296 my $invoice_date = strftime('%Y_%m_%d',localtime($last_date));
297 my $invoice_dir = "invoice_$invoice_date";
298
299 if (not -d $invoice_dir) {
300      if ($options{svn}) {
301           system('svn','mkdir',$invoice_dir) == 0 or
302                die "Unable to create invoice directory $invoice_dir";
303      }
304      else {
305           system('mkdir','-p',$invoice_dir) == 0 or
306                die "Unable to create invoice directory $invoice_dir";
307      }
308 }
309
310 my $cwd = cwd;
311 if (-e 'common_makefile' and not -e '$invoice_dir/Makefile') {
312      chdir($invoice_dir);
313      system('ln','-sf','../common_makefile','Makefile') == 0 or
314           die "Unable to link common_makefile to Makefile";
315      if ($options{svn}) {
316           system('svn','add','Makefile') == 0 or
317                die "Unable to add Makefile";
318      }
319      chdir($cwd);
320 }
321
322 # now we write stuff out
323 chdir($invoice_dir);
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;
327 close($calc_log_fh);
328
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);
333
334 if ($options{svn}) {
335      system('svn','add',
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",
341             '.'
342            ) == 0 or die "Unable to set svn:ignore";
343 }
344
345
346 sub format_events{
347      my %param = validate_with(params => \@_,
348                                spec   => {time => {type => SCALAR,
349                                                   },
350                                           date => {type => SCALAR,
351                                                   },
352                                           date2 => {type => SCALAR,
353                                                    },
354                                           total => {type => SCALARREF,
355                                                    },
356                                           events => {type => ARRAYREF,
357                                                     },
358                                          },
359                               );
360      ${$param{total}} += $param{time} * $options{hourly_fee};
361
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}}) .
370                  " \\\\\n";
371      return $output;
372 }
373
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";
381 ##      return $output;
382 ## }
383
384
385 __END__