#! /usr/bin/perl # make_invoice makes latex invoices, and is released # under the terms of the GPL version 2, or any later version, at your # option. See the file README and COPYING for more information. # Copyright 2008 by Don Armstrong . # $Id: perl_script 495 2006-08-10 08:02:01Z don $ use warnings; use strict; use Getopt::Long; use Pod::Usage; =head1 NAME make_invoice - makes invoices using latex =head1 SYNOPSIS make_invoice [options] Options: --log,-l the log file to use to make the invoice --template,-t the template to use to make the invoice --min-time-interval, -m minimum time to bill, default 0 --time-granularity, -g time granularity, default 0 --hourly-fee, -f hourly fee, default 50.00 --svn,-s whether to use subversion or not --debug, -d debugging level (Default 0) --help, -h display this help --man, -m display manual =head1 OPTIONS =over =item B<--log, -l> The log file to use to generate the invoice =item B<--template, -t> The tex template to use to generate the invoice =item B<--svn, -s> Whether to use subversion or not; defaults to yes if .svn exists in the current directory. =item B<--invoice,-i> Invoice directory to place invoice in (automatically calculated if not passed.) =item B<--min-time-interval, -m> Minimum time interval to bill, defaults to 0. =item B<--time-granularity, -g> Time granularity, defaults to 0. =item B<--hourly-fee,-f> Hourly fee, defaults to 50.00 =item B<--tex-only> Only output the LaTeX file =item B<--log-only> Only output the log file =item B<--debug, -d> Debug verbosity. (Default 0) =item B<--help, -h> Display brief useage information. =item B<--man, -m> Display this manual. =back =head1 EXAMPLES =cut use vars qw($DEBUG); use Date::Manip; use POSIX qw(ceil strftime); use Cwd qw(cwd); use Text::Template; use Params::Validate qw(validate_with :types); my %options = (log => undef, template => undef, svn => undef, invoice => undef, time_interval => 0.00, time_granularity => 0.00, hourly_fee => '50.00', debug => 0, help => 0, man => 0, log_only => 0, tex_only => 0, ); GetOptions(\%options, 'log|l=s','template|t=s','invoice|i=s','svn|s!', 'time_granularity|time-granularity|g=s', 'time_interval|min-time-interval|T=s', 'hourly_fee|hourly-fee|f=s', 'log_only|log-only', 'tex_only|tex-only', 'debug|d+','help|h|?','man|m'); pod2usage() if $options{help}; pod2usage({verbose=>2}) if $options{man}; $DEBUG = $options{debug}; my @USAGE_ERRORS; if (not defined $options{log}) { push @USAGE_ERRORS,"You must pass a log file with --log"; } if (not defined $options{template}) { push @USAGE_ERRORS,"You must pass a template file with --template"; } pod2usage(join("\n",@USAGE_ERRORS)) if @USAGE_ERRORS; if (not defined $options{svn}) { $options{svn} = -e '.svn'; } my $log_fh = IO::File->new($options{log},'r') or die "Unable to open $options{log} for reading: $!"; my $template_fh = IO::File->new($options{template},'r') or die "Unable to open $options{template} for reading: $!"; my $calc_log = ''; my $tex_log = <<'EOF'; \setlength\LTleft{0pt plus 1fill minus 1fill}% \let\LTright\LTleft \begin{longtable}{|p{9cm}|r|r|r|r|}% % \caption*{} \hline \mbox{Description} & Item Cost & Quantity & Cost & Total \\ \endhead EOF my $totaldelta = undef; my $total = 0; my $first_date = undef; my $last_date = undef; my $time = undef; my $date = undef; my $date2 = undef; my @events; while (<$log_fh>) { chomp; next if /^Total: \d+\.\d{2}$/; if (/^\s*\* /) { print STDERR $_."\n"; if (defined $time) { $tex_log .= format_events(date => $date, date2 => $date2, time => $time, total => \$total, events => \@events); @events = (); $date = undef; $time = undef; } s/\s*\[[\.\d]+\]\s*\[[\.\d]+\]\s*$//; my ($d1,$d2); if (/\s*\*\s*CLOCK:\s+\[([^\]]+)\]--\[([^\]]+)\]/ or /^\s*\*\s*(.+)?\s* - \s*(.+)?\s*$/ ) { $d1 = UnixDate(ParseDate($1),'%s'); $d2 = UnixDate(ParseDate($2),'%s'); if (not defined $d1) { die "Invalid date: $1"; } if (not defined $d2) { die "Invalid date: $2"; } } else { die "malformed line $_"; } my $string = '* '.strftime('%A, %B %e, %H:%M:%S',localtime($d1)).' - '. strftime('%A, %B %e, %H:%M:%S',localtime($d2)); if (not defined $first_date) { $first_date = $d1; } $last_date = $d2; my $delta = $d2-$d1; $date = $d1; $date2 = $d2; my $hours = $delta / (60*60); if ($hours < $options{time_interval}) { $hours = $options{time_interval} } if ($options{time_granularity} > 0) { $hours = ceil($hours / $options{time_granularity})*$options{time_granularity}; } $time = $hours; $totaldelta += $delta; $calc_log .= $string.q( [).sprintf('%.2f',$hours).qq(] [).sprintf('%.2f',$totaldelta/(60*60)).qq(]\n); } elsif (/^\s+-\s*(.+)/) { my $event = $1; chomp $event; push @events,$event; $calc_log .= $_.qq(\n); } else { $calc_log .= $_.qq(\n); } } $calc_log .= "\nTotal: ".sprintf('%.2f',$totaldelta/(60*60)).qq(\n); if (defined $time) { $tex_log .= format_events(date => $date, date2 => $date2, time => $time, total => \$total, events => \@events); @events = (); $date = undef; $date2 = undef; $time = undef; } $tex_log .= <<'EOF'; \hline\hline \multicolumn{4}{|r|}{\textbf{Total}} & \$% EOF $tex_log .= sprintf('%.2f',$total)."%\n"; $tex_log .= <<'EOF'; \\ \hline \end{longtable} EOF my $template; { local $/; $template = <$template_fh>; } my $invoice_start = strftime('%c',localtime($first_date)); my $invoice_stop = strftime('%c',localtime($last_date)); my $tt = Text::Template->new(TYPE=>'string', SOURCE => $template, DELIMITERS => ['{--','--}'], ); my $tex_invoice = $tt->fill_in(HASH=>{start => $invoice_start, stop => $invoice_stop, log => $tex_log, total => sprintf('%0.2f',$total), } ); if (not defined $tex_invoice) { die $Text::Template::ERROR; } if ($options{log_only}) { print $calc_log; exit 0; } if ($options{tex_only}) { print $tex_invoice; exit 0; } my $invoice_date = strftime('%Y_%m_%d',localtime($last_date)); my $invoice_dir = "invoice_$invoice_date"; if (not -d $invoice_dir) { if ($options{svn}) { system('svn','mkdir',$invoice_dir) == 0 or die "Unable to create invoice directory $invoice_dir"; } else { system('mkdir','-p',$invoice_dir) == 0 or die "Unable to create invoice directory $invoice_dir"; } } my $cwd = cwd; if (-e 'common_makefile' and not -e '$invoice_dir/Makefile') { chdir($invoice_dir); system('ln','-sf','../common_makefile','Makefile') == 0 or die "Unable to link common_makefile to Makefile"; if ($options{svn}) { system('svn','add','Makefile') == 0 or die "Unable to add Makefile"; } chdir($cwd); } # now we write stuff out chdir($invoice_dir); my $calc_log_fh = IO::File->new("log_${invoice_date}",'w') or die "Unable to open log_${invoice_date} for writing: $!"; print {$calc_log_fh} $calc_log; close($calc_log_fh); my $tex_invoice_fh = IO::File->new("invoice_${invoice_date}.tex",'w') or die "Unable to open log_${invoice_date} for writing: $!"; print {$tex_invoice_fh} $tex_invoice; close($tex_invoice_fh); if ($options{svn}) { system('svn','add', "log_${invoice_date}", "invoice_${invoice_date}.tex", ) == 0 or die "Unable to add log and invoice to svn"; system('svn','propset','svn:ignore', "*.aux\n*.log\n*.dvi\n*.ps\n*.pdf\nauto\n", '.' ) == 0 or die "Unable to set svn:ignore"; } sub format_events{ my %param = validate_with(params => \@_, spec => {time => {type => SCALAR, }, date => {type => SCALAR, }, date2 => {type => SCALAR, }, total => {type => SCALARREF, }, events => {type => ARRAYREF, }, }, ); ${$param{total}} += $param{time} * $options{hourly_fee}; # $param{date} =~ s/\s+\d+\:\d+\:\d+\s+[A-Z]{0,3}\s*//; my $output = '\hline'."\n".' \mbox{'.strftime('%A, %B %e, %H:%M',localtime($param{date})). ' to '.strftime('%H:%M %Z',localtime($param{date2}))."}\n\n". ' \begin{itemize*}'."\n"; $output .= join('',map { s/_/\\_/g; " \\item $_\n";} @{$param{events}}); $output .= ' \end{itemize*} & \$'.sprintf('%.2f',$options{hourly_fee}).' & '.sprintf('%.2f',$param{time}). ' & \$'.sprintf('%.2f',$param{time}*$options{hourly_fee}).' & \$'. sprintf('%.2f',${$param{total}}) . " \\\\\n"; return $output; } ## sub format_events{ ## my ($date,$date2,$time,@events) = @_; ## my $output = ' \Fee{'.strftime('%A, %B %e, %H:%M',localtime(UnixDate($date,'%s'))). ## ' to '.strftime('%H:%M %Z',localtime(UnixDate($date2,'%s')))."\n". ## ' \begin{itemize*}'."\n"; ## $output .= join('',map {" \\item $_\n"} @events); ## $output .= ' \end{itemize*}}{50.00}{'.$time.'}'."\n"; ## return $output; ## } __END__