=================================================================== RCS file: /cvs/RT/Invoicing/rt_invoices.pl,v retrieving revision 1.5 retrieving revision 1.6 diff -u -r1.5 -r1.6 --- RT/Invoicing/rt_invoices.pl 2011/03/21 18:36:59 1.5 +++ RT/Invoicing/rt_invoices.pl 2011/03/21 21:49:44 1.6 @@ -13,28 +13,26 @@ use DateTime; my $config = RTI::Config->new(); +my $state = RTI::State->new( $config->get('state') ); -#print Dump $config; exit; +my $rt_conf = $config->get('rt'); +my $rt = RT::Client::REST->new( + server => $rt_conf->{server}, + timeout => $rt_conf->{timeout}, +); +my $tickets = RT::Client::REST::Ticket->new( rt => $rt ); +my $users = RT::Client::REST::User->new( rt => $rt ); -my $invoiceid = 1; # $state->{lastinvoice} + 1; +$rt->login( username => $rt_conf->{user}, password => $rt_conf->{pass} ); +#print Dump $config, $state; exit; + 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 $customers = $config->get('customers'); +CUSTOMER: while ( my ( $custid, $cust ) = each %{$customers} ) { - my %invoice = ( - from => $config->get('from'), - info => $config->get('info'), - to => $cust->{address}, - rates => $cust->{rates}, - match => $cust->{match}, - ); + my %invoice = ( end => DateTime->now, ); if ( $cust->{base_rate} ) { my $day = $cust->{day} || 1; @@ -48,17 +46,25 @@ default { die "Unknown per [$cust->{per}]\n" } } - my $lastbill + my $billends = DateTime->now->set( hour => 0, minute => 0, second => 0 ); - while ( $lastbill->$day_method != $day ) { - $lastbill->subtract( days => 1 ); + while ( $billends->$day_method != $day ) { + $billends->subtract( days => 1 ); } - my $date - = $lastinvdate - ? $lastinvdate->clone->add( days => 1 ) - : $lastbill->clone->subtract( $per => $freq ); + my $date = $billends->clone->subtract( $per => $freq ); + my $lastinvoice = $state->last_invoice($custid); + if ( $lastinvoice->{date} ) { + my $last_invoice_date = ymd_to_DateTime( $lastinvoice->{date} ); + + warn "HAVE LASTINVOICE DATE\n"; + next CUSTOMER + if DateTime->compare( $billends, $last_invoice_date ) < 1; + + $date = $last_invoice_date->clone->add( days => 1 ); + } + my $title = $freq == 1 ? ucfirst( $cust->{per} . 'ly' ) @@ -67,19 +73,17 @@ my %project = ( title => $title, fees => [], ); - while ( $date < $lastbill ) { + while ( $date < $billends ) { my $start = $date->clone; $date->add( $per => $freq ); - $date = $lastbill->clone if $date > $lastbill; + $date = $billends->clone if $date > $billends; if ( my $diff = $date->$day_method - $day ) { $date->subtract( days => $diff ); } - my $end = $date->clone; + my $end = $date->clone->subtract( seconds => 1 ); - $end->subtract( seconds => 1 ); - $startdate = $start->clone if !$startdate || $startdate > $start; $invoice{start} ||= $start->clone; $invoice{end} = $end->clone; @@ -103,30 +107,10 @@ push @{ $invoice{projects} }, \%project; } } - else { - $invoice{end} = DateTime->now; - push @{ $invoice{hours} }, - { end => DateTime->now, hours => $cust->{hours} }; - } - next unless $invoice{end}; - push @invoices, \%invoice; + $cust->{invoice} = \%invoice; } -#print Dump \@invoices; exit; - -my $rt_conf = $config->get('rt'); -my $rt = RT::Client::REST->new( - server => $rt_conf->{server}, - timeout => $rt_conf->{timeout}, -); -$rt->login( - username => $rt_conf->{user}, - password => $rt_conf->{pass}, -); -my $tickets = RT::Client::REST::Ticket->new( rt => $rt ); -my $users = RT::Client::REST::User->new( rt => $rt ); - my @limits = map +{ attribute => 'status', operator => '=', @@ -156,62 +140,26 @@ my $iterator = $results->get_iterator; while ( my $ticket = &$iterator ) { - my $invoice = find_invoice_for_ticket( \@invoices, $ticket ); + my $cust = find_customer_for_ticket( $customers, $ticket ); + if ( !$cust ) { + say "No customer found for ticket " . $ticket->id; - if ( !$invoice ) { - say "No invoice found for ticket " . $ticket->id; - # XXX should construct a "new" invoice to pop onto the list next; } + my $invoice = $cust->{invoice}; - my %project = ( - id => $ticket->id, - queue => $ticket->queue, - owner => $ticket->owner, - title => $ticket->subject, - detail => 'Ticket: ' - . $ticket->id - . ' Queue: ' - . $ticket->queue - . ' Requestors: ' - . join( ', ', $ticket->requestors ), - fees => [], - expenses => [], - ); - #foreach my $r ($ticket->requestors) { # $users->id( $r ); # $users->retrieve; # print Dump $users; #} + my $project = make_project( $invoice, $ticket ); + next unless @{ $project->{fees} } || @{ $project->{expenses} }; - my $txns = $ticket->transactions( type => [qw(Comment Correspond)] ); - my $txn_i = $txns->get_iterator; - while ( my $txn = $txn_i->() ) { - next unless $txn->time_taken; + foreach my $fee ( @{ $project->{fees} } ) { + my $hours = hours_for_date( $invoice, $fee->{date} ); - my ( $date, $time ) = split ' ', $txn->created; - my ( $year, $month, $day ) = split '-', $date; - my ( $hour, $minute, $second ) = split ':', $time; - - my $txn_date = DateTime->new( - year => $year, - month => $month, - day => $day, - hour => $hour, - minute => $minute, - second => $second, - ); - - next if $invoice->{start} && $invoice->{start} > $txn_date; - next if $invoice->{end} < $txn_date; - - 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} @@ -236,20 +184,21 @@ $h_type = '' if $h_type eq 'default'; $fee->{detail} = "$discount_time $h_type Hours Discounted"; } - - #print Dump $txn; exit; } - next unless @{ $project{fees} } || @{ $project{expenses} }; - print " Added ticket $project{id}\n"; - push @{ $invoice->{projects} }, \%project; + print " Added ticket $project->{id}\n"; + push @{ $invoice->{projects} }, $project; } -foreach my $invoice (@invoices) { +while ( my ( $custid, $cust ) = each %{$customers} ) { + my $invoice = $cust->{invoice}; next unless $invoice->{projects} && @{ $invoice->{projects} }; foreach my $project ( @{ $invoice->{projects} } ) { print "$project->{title}\n"; + if ( $project->{transactions} ) { + push @{ $cust->{transactions} }, @{ $project->{transactions} }; + } my $subtotal = 0; foreach my $fee ( @{ $project->{fees} } ) { my $amount = round( $fee->{count} * $fee->{rate} ); @@ -281,28 +230,44 @@ $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'); - } + # XXX Here we need to "make_address" + $invoice->{info} = $config->get('info'); + $invoice->{from} = $config->get('from'); + $invoice->{to} = $cust->{address}; + + $state->{lastinvoice}++; + $invoice->{id} = $state->{lastinvoice}; + $invoice->{file} = 'invoice_' . $state->{lastinvoice} . '.pdf'; + + my %li = ( custid => $custid, ); + + foreach my $k (qw/ transactions /) { + if ( $invoice->{$k} ) { $li{$k} = $invoice->{$k} } + elsif ( $cust->{$k} ) { $li{$k} = $cust->{$k} } } - $invoice->{id} = $invoiceid; - $invoice->{file} = 'invoice_' . $invoiceid . '.pdf'; + $state->{invoice}->{ $invoice->{end}->ymd }{ $state->{lastinvoice} } + = \%li; print "Created Invoice\n"; print Dump $invoice; + foreach my $key (qw/ start end /) { + if ( exists $invoice->{$key} ) { + $invoice->{$key} = $invoice->{$key}->strftime('%B %d, %Y'); + } + } + my $tt = Template->new; $tt->process( 'invoice.tex.tt', $invoice, $invoice->{file} ) or die $tt->error . "\n"; print "Generated $invoice->{file}\n"; - - $invoiceid++; } # XXX Save State +print Dump $state; +$state->save; sub round { my ($amount) = @_; @@ -312,12 +277,12 @@ return sprintf "%.02f", $amount; } -sub find_invoice_for_ticket { - my ( $invoices, $ticket ) = @_; +sub find_customer_for_ticket { + my ( $customers, $ticket ) = @_; -INVOICE: foreach my $i ( @{$invoices} ) { - next INVOICE unless $i->{match}; - foreach my $m ( @{ $i->{match} } ) { +INVOICE: foreach my $cust ( values %{$customers} ) { + next INVOICE unless $cust->{match}; + foreach my $m ( @{ $cust->{match} } ) { my $type = $m->{type}; my $thing = join ' ', $ticket->$type; @@ -333,10 +298,59 @@ next INVOICE; } } - return $i; + return $cust; } + + return fake_customer($ticket); } +sub fake_customer { + my ($ticket) = @_; + + # XXX eventually, will generate a customer we can bill! + return; +} + +sub make_project { + my ( $invoice, $ticket ) = @_; + + my %project = ( + id => $ticket->id, + queue => $ticket->queue, + owner => $ticket->owner, + title => $ticket->subject, + detail => 'Ticket: ' + . $ticket->id + . ' Queue: ' + . $ticket->queue + . ' Requestors: ' + . join( ', ', $ticket->requestors ), + fees => [], + expenses => [], + ); + + my $txns = $ticket->transactions( type => [qw(Comment Correspond)] ); + my $txn_i = $txns->get_iterator; + while ( my $txn = $txn_i->() ) { + next unless $txn->time_taken; + next if $state->txn_is_invoiced( $txn->id ); + + my $fee = make_fee( $ticket, $txn, $invoice->{rates} ); + if ( !( $fee->{rate} && $fee->{count} ) ) { + warn "Invalid Fee, no rate or count"; + next; + } + + next if $invoice->{start} && $invoice->{start} > $fee->{date}; + next if $invoice->{end} < $fee->{date}; + + push @{ $project{fees} }, $fee; + push @{ $project{transactions} }, $txn->id; + } + + return \%project; +} + sub make_fee { my ( $ticket, $txn, $rates ) = @_; @@ -352,6 +366,7 @@ count => $work_time, rate => $work_rate, type => $work_type, + date => ymd_to_DateTime( $txn->created ), ); if ( $work_type && $work_type ne 'Normal' ) { @@ -377,6 +392,22 @@ return $hours; } +sub ymd_to_DateTime { + my ($ymd) = @_; + my ( $date, $time ) = split ' ', $ymd; + my ( $year, $month, $day ) = split '-', $date; + my ( $hour, $minute, $second ) = split ':', $time if $time; + + return DateTime->new( + year => $year, + month => $month, + day => $day, + hour => $hour || 0, + minute => $minute || 0, + second => $second || 0, + ); +} + package RTI::Config; use strict; use warnings; @@ -384,6 +415,7 @@ use 5.010; use YAML::Any qw/ LoadFile Dump Load /; +use File::Basename; sub new { my ( $class, $args ) = @_; @@ -402,7 +434,7 @@ # XXX This needs to be better foreach my $file (qw/ rt_invoice.conf rt_invoice.cfg .rt_invoicerc /) { - foreach my $dir ( '.', $ENV{HOME} . '/.rt_invoice', $ENV{HOME} ) { + foreach my $dir ( '.', $ENV{HOME} . '/.rt_invoicing', $ENV{HOME} ) { my $path = join '/', $dir, $file; return $path if -e $path; } @@ -418,9 +450,9 @@ my $c = LoadFile($file) or die "Unable to load $file\n"; - $c->{customers} ||= []; + $c->{customers} ||= {}; if ( $c->{default} ) { - foreach my $cust ( @{ $c->{customers} } ) { + foreach my $cust ( values %{ $c->{customers} } ) { foreach my $k ( keys %{ $c->{default} } ) { $cust->{$k} //= Load( Dump( $c->{default}->{$k} ) ); } @@ -433,7 +465,15 @@ sub get { my ( $self, $key ) = @_; - return Load( Dump( $self->{_config}->{$key} ) ); + my $value = Load( Dump( $self->{_config}->{$key} ) ); + if ( !$value ) { + given ($key) { + when ('state') { + $value = dirname( $self->{file} ) . '/rt_invoice.state' + } + } + } + return $value; } package RTI::State; @@ -444,13 +484,58 @@ use YAML::Any qw/ LoadFile DumpFile /; +my $file = ''; + sub new { - my ( $class, $args ) = @_; + my $class; + ( $class, $file ) = @_; - my $self = { file => '', }; + my $self = { lastinvoice => 0, }; + if ( -e $file ) { + $self = LoadFile($file) or die "Unable to load state: $!"; + } + bless $self, $class; - my $file = $args->{file} || $self->_find_config; - $self->read_config($file); + die "Need to pass filename to new: $!" unless $file; + return $self; +} + +sub last_invoice { + my ( $self, $custid ) = @_; + state %table; + if ( !%table ) { + foreach my $date ( sort keys %{ $self->{invoice} } ) { + while ( my ( $id, $inv ) = each %{ $self->{invoice}->{$date} } ) { + next unless $inv->{custid}; + $table{ $inv->{custid} } = { + id => $id, + date => $date, + %{$inv}, + }; + } + } + } + return $table{$custid} || {}; +} + +sub txn_is_invoiced { + my ( $self, $txn ) = @_; + state %table; + if ( !%table ) { + foreach my $date ( sort keys %{ $self->{invoice} } ) { + foreach my $inv ( values %{ $self->{invoice}->{$date} } ) { + foreach my $t ( @{ $inv->{transactions} } ) { + $table{$t} = 1; + } + } + } + } + return $table{$txn}; +} + +sub save { + my ($self) = @_; + DumpFile( $file, {%$self} ) or die "Unable to save state: $!"; }