]> git.donarmstrong.com Git - bin.git/blob - make_invoice
update make invoice to handle new format
[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<--debug, -d>
69
70 Debug verbosity. (Default 0)
71
72 =item B<--help, -h>
73
74 Display brief useage information.
75
76 =item B<--man, -m>
77
78 Display this manual.
79
80 =back
81
82 =head1 EXAMPLES
83
84
85 =cut
86
87
88 use vars qw($DEBUG);
89
90 use Date::Manip;
91 use POSIX qw(ceil strftime);
92 use Cwd qw(cwd);
93 use Text::Template;
94
95 use Params::Validate qw(validate_with :types);
96
97 my %options = (log             => undef,
98                template        => undef,
99                svn             => undef,
100                invoice         => undef,
101                time_interval   => 0.00,
102                time_granularity => 0.00,
103                hourly_fee      => '50.00',
104                debug           => 0,
105                help            => 0,
106                man             => 0,
107                );
108
109 GetOptions(\%options,
110            'log|l=s','template|t=s','invoice|i=s','svn|s!',
111            'time_granularity|time-granularity|g=s',
112            'time_interval|min-time-interval|T=s',
113            'hourly_fee|hourly-fee|f=s',
114            'debug|d+','help|h|?','man|m');
115
116 pod2usage() if $options{help};
117 pod2usage({verbose=>2}) if $options{man};
118
119 $DEBUG = $options{debug};
120
121 my @USAGE_ERRORS;
122 if (not defined $options{log}) {
123      push @USAGE_ERRORS,"You must pass a log file with --log";
124 }
125 if (not defined $options{template}) {
126      push @USAGE_ERRORS,"You must pass a template file with --template";
127 }
128
129 pod2usage(join("\n",@USAGE_ERRORS)) if @USAGE_ERRORS;
130
131 if (not defined $options{svn}) {
132      $options{svn} = -e '.svn';
133 }
134
135
136 my $log_fh = IO::File->new($options{log},'r') or
137      die "Unable to open $options{log} for reading: $!";
138 my $template_fh = IO::File->new($options{template},'r') or
139      die "Unable to open $options{template} for reading: $!";
140
141
142 my $calc_log = '';
143 my $tex_log = <<'EOF';
144 \setlength\LTleft{0pt plus 1fill minus 1fill}%
145 \let\LTright\LTleft
146 \begin{longtable}{|p{9cm}|r|r|r|r|}%
147 %  \caption*{}
148 \hline
149   \mbox{Description} & Item Cost & Quantity & Cost & Total \\
150 \endhead
151 EOF
152 my $totaldelta = undef;
153
154 my $total = 0;
155
156 my $first_date = undef;
157 my $last_date = undef;
158 my $time = undef;
159 my $date = undef;
160 my $date2 = undef;
161 my @events;
162
163 while (<$log_fh>) {
164      chomp;
165      if (/^\s*\* /) {
166           if (defined $time) {
167                $tex_log .= format_events(date => $date,
168                                          date2 => $date2,
169                                          time => $time,
170                                          total => \$total,
171                                          events => \@events);
172                @events = ();
173                $date = undef;
174                $time = undef;
175           }
176           my $string = $_;
177           my ($d1,$d2) = map {s/^\s*\*\s*//;
178                              ParseDate($_)
179                         } split /\s*-\s*/;
180           if (not defined $first_date) {
181                $first_date = $d1;
182           }
183           $last_date = $d2;
184           my $delta = DateCalc($d1,$d2);
185           my $hours = Delta_Format($delta,0,'%ht');
186           if ($hours < $options{time_interval}) {
187                $hours = $options{time_interval}
188           }
189           if ($options{time_granularity}) {
190                $hours = ceil($hours / $options{time_granularity})*$options{time_granularity};
191           }
192           $delta = ParseDateDelta($hours * 60 * 60 . ' sec');
193           ($date,$date2) = ($d1,$d2);
194           $time = $hours;
195           $totaldelta = defined($totaldelta)?DateCalc($delta,$totaldelta):$delta;
196           $calc_log .= qq($string [).Delta_Format($delta,2,q(%ht)).qq(] [).Delta_Format($totaldelta,2,q(%ht)).qq(]\n);
197      }
198      elsif (/^\s+-\s*(.+)/) {
199           my $event = $1;
200           chomp $event;
201           push @events,$event;
202           $calc_log .= $_.qq(\n);
203      }
204      else {
205           $calc_log .= $_.qq(\n);
206      }
207 }
208 $calc_log .= "\nTotal: ".Delta_Format($totaldelta,2,q(%ht)).qq(\n);
209 if (defined $time) {
210     $tex_log .= format_events(date => $date,
211                                date2 => $date2,
212                               time => $time,
213                               total => \$total,
214                               events => \@events);
215      @events = ();
216      $date = undef;
217      $date2 = undef;
218      $time = undef;
219 }
220
221 $tex_log .= <<'EOF';
222 \hline\hline
223 \multicolumn{4}{|r|}{\textbf{Total}} & \$%
224 EOF
225
226 $tex_log .= sprintf('%.2f',$total)."%\n";
227
228 $tex_log .= <<'EOF';
229 \\
230 \hline
231 \end{longtable}
232 EOF
233
234 my $template;
235 {
236      local $/;
237      $template = <$template_fh>;
238 }
239
240 my $invoice_start = UnixDate($first_date,'%B %e, %Y');
241 my $invoice_stop = UnixDate($last_date,'%B %e, %Y');
242
243 my $tt = Text::Template->new(TYPE=>'string',
244                              SOURCE => $template,
245                              DELIMITERS => ['{--','--}'],
246                             );
247 my $tex_invoice = $tt->fill_in(HASH=>{start => $invoice_start,
248                                       stop  => $invoice_stop,
249                                       log   => $tex_log,
250                                      }
251                               );
252 if (not defined $tex_invoice) {
253      die $Text::Template::ERROR;
254 }
255
256 my $invoice_date = UnixDate($last_date,'%m_%d_%Y');
257 my $invoice_dir = "invoice_$invoice_date";
258
259 if (not -d $invoice_dir) {
260      if ($options{svn}) {
261           system('svn','mkdir',$invoice_dir) == 0 or
262                die "Unable to create invoice directory $invoice_dir";
263      }
264      else {
265           system('mkdir','-p',$invoice_dir) == 0 or
266                die "Unable to create invoice directory $invoice_dir";
267      }
268 }
269
270 my $cwd = cwd;
271 if (-e 'common_makefile' and not -e '$invoice_dir/Makefile') {
272      chdir($invoice_dir);
273      system('ln','-sf','../common_makefile','Makefile') == 0 or
274           die "Unable to link common_makefile to Makefile";
275      if ($options{svn}) {
276           system('svn','add','Makefile') == 0 or
277                die "Unable to add Makefile";
278      }
279      chdir($cwd);
280 }
281
282 # now we write stuff out
283 chdir($invoice_dir);
284 my $calc_log_fh = IO::File->new("log_${invoice_date}",'w') or
285      die "Unable to open log_${invoice_date} for writing: $!";
286 print {$calc_log_fh} $calc_log;
287 close($calc_log_fh);
288
289 my $tex_invoice_fh = IO::File->new("invoice_${invoice_date}.tex",'w') or
290      die "Unable to open log_${invoice_date} for writing: $!";
291 print {$tex_invoice_fh} $tex_invoice;
292 close($tex_invoice_fh);
293
294 if ($options{svn}) {
295      system('svn','add',
296             "log_${invoice_date}",
297             "invoice_${invoice_date}.tex",
298            ) == 0 or die "Unable to add log and invoice to svn";
299      system('svn','propset','svn:ignore',
300             "*.aux\n*.log\n*.dvi\n*.ps\n*.pdf\nauto\n",
301             '.'
302            ) == 0 or die "Unable to set svn:ignore";
303 }
304
305
306 sub format_events{
307      my %param = validate_with(params => \@_,
308                                spec   => {time => {type => SCALAR,
309                                                   },
310                                           date => {type => SCALAR,
311                                                   },
312                                           date2 => {type => SCALAR,
313                                                    },
314                                           total => {type => SCALARREF,
315                                                    },
316                                           events => {type => ARRAYREF,
317                                                     },
318                                          },
319                               );
320      ${$param{total}} += $param{time} * $options{hourly_fee};
321
322 #     $param{date} =~ s/\s+\d+\:\d+\:\d+\s+[A-Z]{0,3}\s*//;
323      my $output = '\hline'."\n".'        \mbox{'.strftime('%A, %B %e, %H:%M',localtime(UnixDate($param{date},'%s'))).
324           ' to '.strftime('%H:%M %Z',localtime(UnixDate($param{date2},'%s')))."}\n\n".
325          '         \begin{itemize*}'."\n";
326      $output .= join('',map { s/_/\\_/g; "           \\item $_\n";} @{$param{events}});
327      $output .= '         \end{itemize*} & \$'.sprintf('%.2f',$options{hourly_fee}).' & '.sprintf('%.2f',$param{time}).
328          ' & \$'.sprintf('%.2f',$param{time}*$options{hourly_fee}).' & \$'.
329              sprintf('%.2f',${$param{total}}) .
330                  " \\\\\n";
331      return $output;
332 }
333
334 ## sub format_events{
335 ##      my ($date,$date2,$time,@events) = @_;
336 ##      my $output = '        \Fee{'.strftime('%A, %B %e, %H:%M',localtime(UnixDate($date,'%s'))).
337 ##        ' to '.strftime('%H:%M %Z',localtime(UnixDate($date2,'%s')))."\n".
338 ##        '         \begin{itemize*}'."\n";
339 ##      $output .= join('',map {"           \\item $_\n"} @events);
340 ##      $output .= '         \end{itemize*}}{50.00}{'.$time.'}'."\n";
341 ##      return $output;
342 ## }
343
344
345 __END__