=================================================================== RCS file: /cvs/RT/Invoicing/rt_invoices.pl,v retrieving revision 1.2 retrieving revision 1.4 diff -u -r1.2 -r1.4 --- RT/Invoicing/rt_invoices.pl 2011/03/21 01:05:38 1.2 +++ RT/Invoicing/rt_invoices.pl 2011/03/21 05:01:33 1.4 @@ -12,46 +12,53 @@ use DateTime; -my %included_hours; - my $config = RTI::Config->new(); #print Dump $config; exit; -# my $state = RTI::State->new($cust); -# $invoice{state} = $state; -my $lastinvdate; # = $state->{lastinvoicedte}; XXX Needs to be a DateTime - -#$lastinvdate = DateTime->now->subtract( months => 2 ); my $invoiceid = 1; # $state->{lastinvoice} + 1; my $startdate; my @invoices; foreach my $cust ( @{ $config->get('customers') } ) { + + # my $state = RTI::State->new($cust); + # $invoice{state} = $state; + my $lastinvdate; # = $state->{lastinvoicedte}; XXX Needs to be a DateTime + #$lastinvdate = DateTime->now->subtract( months => 2 ) + # ->set( hour => 0, minute => 0, second => 0 ); + my %invoice = ( from => $config->get('from'), + info => $config->get('info'), to => $cust->{address}, rates => $cust->{rates}, match => $cust->{match}, ); if ( $cust->{base_rate} ) { - my $date = DateTime->now; - my $day = $cust->{day} || 1; + my $day = $cust->{day} || 1; my $freq = $cust->{frequency} || 1; - my $diff; + my $day_method; my $per; given ( $cust->{per} ) { - when ('week') { $per = 'weeks'; $diff = $date->dow - $day; } - when ('month') { $per = 'months'; $diff = $date->day - $day; } + when ('week') { $per = 'weeks'; $day_method = 'dow' } + when ('month') { $per = 'months'; $day_method = 'day' } default { die "Unknown per [$cust->{per}]\n" } } - # $day is start day, end should be one day further back - $diff = abs($diff) + 1; - $date->subtract( days => $diff ); + my $lastbill + = DateTime->now->set( hour => 0, minute => 0, second => 0 ); + while ( $lastbill->$day_method != $day ) { + $lastbill->subtract( days => 1 ); + } + my $date + = $lastinvdate + ? $lastinvdate->clone->add( days => 1 ) + : $lastbill->clone->subtract( $per => $freq ); + my $title = $freq == 1 ? ucfirst( $cust->{per} . 'ly' ) @@ -60,46 +67,49 @@ my %project = ( title => $title, fees => [], ); - # XXX need to add them until we get to where we billed already - # if we don't know the last invoice date, assume the day before - $lastinvdate ||= $date->clone->subtract( days => 1 ); - while ( $date > $lastinvdate ) { - $invoice{enddate} = $date->clone; + while ( $date < $lastbill ) { + my $start = $date->clone; + + $date->add( $per => $freq ); + $date = $lastbill->clone if $date > $lastbill; + if ( my $diff = $date->$day_method - $day ) { + $date->subtract( days => $diff ); + } + + my $end = $date->clone; + + $end->subtract( seconds => 1 ); + + $startdate = $start->clone if !$startdate || $startdate > $start; + $invoice{start} ||= $start->clone; + $invoice{end} = $end->clone; my %hours = ( - end => $date->clone, + start => $start->clone, + end => $end->clone, hours => { %{ $cust->{hours} } }, ); - my $contents = ' to ' . $date->ymd; - $date->subtract( $per => $freq )->add( days => 1 ); - $contents = $date->ymd . $contents; - - $invoice{startdate} ||= $date->clone; - $hours{start} = $date->clone; - $startdate = $date->clone; - - unshift @{ $invoice{hours} }, \%hours; - unshift @{ $project{fees} }, + push @{ $invoice{hours} }, \%hours; + push @{ $project{fees} }, { count => 1, rate => $cust->{base_rate}, - contents => $contents, + contents => $start->ymd . ' to ' . $end->ymd, }; - # Next time, one day less - $date->subtract( days => 1 ); } - if (@{ $project{fees} }) { + if ( @{ $project{fees} } ) { push @{ $invoice{projects} }, \%project; } } else { - $invoice{enddate} = DateTime->now->ymd; + $invoice{end} = DateTime->now; push @{ $invoice{hours} }, { end => DateTime->now, hours => $cust->{hours} }; } + next unless $invoice{end}; push @invoices, \%invoice; } @@ -127,12 +137,6 @@ # XXX This should be a config option qw/ open stalled resolved /; -#push @limits, -# { -# attribute => 'resolved', -# operator => '>=', -# value => '2011-03-01' -# }; if ($startdate) { push @limits, { @@ -142,63 +146,40 @@ }; } -#push @limits, { attribute => 'id', operator => '=', value => '51' }; -#push @limits, { attribute => '', operator => '', value => '' }; - my $results = $tickets->search( limits => \@limits, orderby => 'id', ); -#print Dump $ticket, $results; - my $count = $results->count; print "There are $count results that matched your query\n"; my $iterator = $results->get_iterator; while ( my $ticket = &$iterator ) { + my $invoice = find_invoice_for_ticket( \@invoices, $ticket ); + + if ( !$invoice ) { + say "No invoice found for ticket " . $ticket->id; + + # XXX should construct a "new" invoice to pop onto the list + next; + } + my %project = ( id => $ticket->id, queue => $ticket->queue, owner => $ticket->owner, - title => $ticket->id . ': ' . $ticket->subject, - detail => 'Requestors: ' - . join( ', ', $ticket->requestors ) + title => $ticket->subject, + detail => 'Ticket: ' + . $ticket->id . ' Queue: ' - . $ticket->queue, + . $ticket->queue + . ' Requestors: ' + . join( ', ', $ticket->requestors ), fees => [], expenses => [], ); - my $invoice; -INVOICE: foreach my $i (@invoices) { - next INVOICE unless $i->{match}; - foreach my $m ( @{ $i->{match} } ) { - my $type = $m->{type}; - my $thing = join ' ', $ticket->$type; - - if ( $m->{$type} ) { - my $match = lc $m->{$type}; - next INVOICE unless lc($thing) ~~ $match; - } - elsif ( $m->{regex} ) { - next INVOICE unless $thing ~~ /\Q$m->{regex}\E/; - } - else { - warn "Invalid match!"; - next INVOICE; - } - } - $invoice = $i; - } - - if ( !$invoice ) { - say "No invoice found for ticket " . $ticket->id; - - # XXX should construct a "new" invoice to pop onto the list - next; - } - #foreach my $r ($ticket->requestors) { # $users->id( $r ); # $users->retrieve; @@ -210,49 +191,50 @@ while ( my $txn = $txn_i->() ) { next unless $txn->time_taken; - my $work_time = sprintf "%.03f", $txn->time_taken / 60; - my $work_type = $txn->cf('WorkType'); - my $work_rate - = $invoice->{rates}{$work_type} - || $invoice->{rates}{default} - || 0; + my ( $date, $time ) = split ' ', $txn->created; + my ( $year, $month, $day ) = split '-', $date; + my ( $hour, $minute, $second ) = split ':', $time; - my $ih_type - = exists $included_hours{$work_type} - ? $work_type - : 'default'; - - my %fee = ( - id => $txn->id, - contents => $txn->created . ' (' - . $txn->id . ')' . "\n\n" - . ( $txn->data || $ticket->subject ), - count => $work_time, - rate => $work_rate, + my $txn_date = DateTime->new( + year => $year, + month => $month, + day => $day, + hour => $hour, + minute => $minute, + second => $second, ); - if ( $work_type && $work_type ne 'Normal' ) { - $fee{detail} = $work_type . ' rate'; - } - push @{ $project{fees} }, \%fee; + next if $invoice->{start} && $invoice->{start} > $txn_date; + next if $invoice->{end} < $txn_date; - next - unless $included_hours{$ih_type} && $included_hours{$ih_type} > 0; + my $fee = make_fee( $ticket, $txn, $invoice->{rates} ); + push @{ $project{fees} }, $fee; + my $hours = hours_for_date( $invoice, $txn_date ); + + my $h_type + = exists $hours->{ $fee->{type} } + ? $fee->{type} + : 'default'; + + next unless exists $hours->{$h_type} && $hours->{$h_type} > 0; + my $discount_time = 0; - if ( $included_hours{$ih_type} > $work_time ) { - $included_hours{$ih_type} -= $work_time; - $discount_time = $work_time; + if ( $hours->{$h_type} > $fee->{count} ) { + $hours->{$h_type} -= $fee->{count}; + $discount_time = $fee->{count}; } else { - $discount_time = $included_hours{$ih_type}; - $included_hours{$ih_type} = 0; + $discount_time = $hours->{$h_type}; + $hours->{$h_type} = 0; } if ($discount_time) { - $fee{detail} = "$discount_time $work_type Hours Discounted"; - $invoice->{discount}{amount} += $discount_time * $fee{rate}; - $invoice->{discount}{hours}{$work_type} += $discount_time; + $invoice->{discount}{amount} += $discount_time * $fee->{rate}; + $invoice->{discount}{hours}{$h_type} += $discount_time; + + $h_type = '' if $h_type eq 'default'; + $fee->{detail} = "$discount_time $h_type Hours Discounted"; } #print Dump $txn; exit; @@ -299,6 +281,12 @@ $invoice->{total_due} = $invoice->{total} + $invoice->{past_due}; } + foreach my $key (qw/ start end /) { + if ( exists $invoice->{$key} ) { + $invoice->{$key} = $invoice->{$key}->strftime('%B %d, %Y'); + } + } + $invoice->{id} = $invoiceid; $invoice->{file} = 'invoice_' . $invoiceid . '.pdf'; @@ -322,6 +310,71 @@ #$amount =~ s/\.\d\d\K.*$//; #return $amount; return sprintf "%.02f", $amount; +} + +sub find_invoice_for_ticket { + my ( $invoices, $ticket ) = @_; + +INVOICE: foreach my $i ( @{$invoices} ) { + next INVOICE unless $i->{match}; + foreach my $m ( @{ $i->{match} } ) { + my $type = $m->{type}; + my $thing = join ' ', $ticket->$type; + + if ( $m->{$type} ) { + my $match = lc $m->{$type}; + next INVOICE unless lc($thing) ~~ $match; + } + elsif ( $m->{regex} ) { + next INVOICE unless $thing ~~ /\Q$m->{regex}\E/; + } + else { + warn "Invalid match!"; + next INVOICE; + } + } + return $i; + } +} + +sub make_fee { + my ( $ticket, $txn, $rates ) = @_; + + my $work_time = sprintf "%.03f", $txn->time_taken / 60; + my $work_type = $txn->cf('WorkType'); + my $work_rate = $rates->{$work_type} || $rates->{default} || 0; + + my %fee = ( + id => $txn->id, + contents => $txn->created . ' (' + . $txn->id . ')' . "\n\n" + . ( $txn->data || $ticket->subject ), + count => $work_time, + rate => $work_rate, + type => $work_type, + ); + + if ( $work_type && $work_type ne 'Normal' ) { + $fee{detail} = $work_type . ' rate'; + } + + return \%fee; +} + +sub hours_for_date { + my ( $invoice, $date ) = @_; + + my $hours = {}; + if ( $invoice->{hours} ) { + foreach my $h ( @{ $invoice->{hours} } ) { + next if $h->{start} && $h->{start} > $date; + next if $h->{end} < $date; + + $hours = $h->{hours}; + last; + } + } + return $hours; } package RTI::Config;