| version 1.6, 2011/03/21 21:49:44 | version 1.24, 2011/04/12 20:32:15 | 
|  |  | 
| #!/usr/bin/perl | #!/usr/bin/perl | 
|  | # $AFresh1: rt_invoices.pl,v 1.23 2011/04/08 18:00:16 andrew Exp $ | 
|  | ######################################################################## | 
|  | # Copyright (c) 2011 Andrew Fresh <andrew@afresh1.com> | 
|  | # | 
|  | # Permission to use, copy, modify, and distribute this software for any | 
|  | # purpose with or without fee is hereby granted, provided that the above | 
|  | # copyright notice and this permission notice appear in all copies. | 
|  | # | 
|  | # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES | 
|  | # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF | 
|  | # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR | 
|  | # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES | 
|  | # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN | 
|  | # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF | 
|  | # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. | 
|  | ######################################################################## | 
| use strict; | use strict; | 
| use warnings; | use warnings; | 
|  |  | 
| use 5.010; | use 5.010; | 
|  |  | 
| use YAML::Any; |  | 
| use Template; | use Template; | 
| use RT::Client::REST; | use RT::Client::REST; | 
| use RT::Client::REST::Ticket; | use RT::Client::REST::Ticket; | 
| use RT::Client::REST::User; | use RT::Client::REST::User; | 
|  |  | 
|  | use File::Path; | 
| use DateTime; | use DateTime; | 
|  |  | 
| my $config = RTI::Config->new(); | my $config = RTI::Config->new(); | 
|  |  | 
| timeout => $rt_conf->{timeout}, | timeout => $rt_conf->{timeout}, | 
| ); | ); | 
| my $tickets = RT::Client::REST::Ticket->new( rt => $rt ); | my $tickets = RT::Client::REST::Ticket->new( rt => $rt ); | 
| my $users = RT::Client::REST::User->new( rt => $rt ); |  | 
|  |  | 
| $rt->login( username => $rt_conf->{user}, password => $rt_conf->{pass} ); | $rt->login( username => $rt_conf->{user}, password => $rt_conf->{pass} ); | 
|  |  | 
|  | #use YAML; | 
| #print Dump $config, $state; exit; | #print Dump $config, $state; exit; | 
|  |  | 
| my $startdate; | my $startdate; | 
|  |  | 
| my $customers = $config->get('customers'); | my $customers = $config->get('customers'); | 
| CUSTOMER: while ( my ( $custid, $cust ) = each %{$customers} ) { | while ( my ( $custid, $cust ) = each %{$customers} ) { | 
|  | $cust->{id} = $custid; | 
|  |  | 
| my %invoice = ( end => DateTime->now, ); | if ( my $invoice = make_invoice($cust) ) { | 
|  | $cust->{invoice} = $invoice; | 
| if ( $cust->{base_rate} ) { |  | 
| my $day  = $cust->{day}       || 1; |  | 
| my $freq = $cust->{frequency} || 1; |  | 
|  |  | 
| my $day_method; |  | 
| my $per; |  | 
| given ( $cust->{per} ) { |  | 
| when ('week')  { $per = 'weeks';  $day_method = 'dow' } |  | 
| when ('month') { $per = 'months'; $day_method = 'day' } |  | 
| default { die "Unknown per [$cust->{per}]\n" } |  | 
| } |  | 
|  |  | 
| my $billends |  | 
| = DateTime->now->set( hour => 0, minute => 0, second => 0 ); |  | 
| while ( $billends->$day_method != $day ) { |  | 
| $billends->subtract( days => 1 ); |  | 
| } |  | 
|  |  | 
| 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' ) |  | 
| : $freq . ' ' . ucfirst( $cust->{per} ); |  | 
| $title .= ' Retainer'; |  | 
|  |  | 
| my %project = ( title => $title, fees => [], ); |  | 
|  |  | 
| while ( $date < $billends ) { |  | 
| my $start = $date->clone; |  | 
|  |  | 
| $date->add( $per => $freq ); |  | 
| $date = $billends->clone if $date > $billends; |  | 
| if ( my $diff = $date->$day_method - $day ) { |  | 
| $date->subtract( days => $diff ); |  | 
| } |  | 
|  |  | 
| my $end = $date->clone->subtract( seconds => 1 ); |  | 
|  |  | 
| $startdate = $start->clone if !$startdate || $startdate > $start; |  | 
| $invoice{start} ||= $start->clone; |  | 
| $invoice{end} = $end->clone; |  | 
| my %hours = ( |  | 
| start => $start->clone, |  | 
| end   => $end->clone, |  | 
| hours => { %{ $cust->{hours} } }, |  | 
| ); |  | 
|  |  | 
| push @{ $invoice{hours} }, \%hours; |  | 
| push @{ $project{fees} }, |  | 
| { |  | 
| count    => 1, |  | 
| rate     => $cust->{base_rate}, |  | 
| contents => $start->ymd . ' to ' . $end->ymd, |  | 
| }; |  | 
|  |  | 
| } |  | 
|  |  | 
| if ( @{ $project{fees} } ) { |  | 
| push @{ $invoice{projects} }, \%project; |  | 
| } |  | 
| } | } | 
|  |  | 
| $cust->{invoice} = \%invoice; |  | 
| } | } | 
|  |  | 
| my @limits = map +{ | my @limits = map +{ | 
|  |  | 
| while ( my $ticket = &$iterator ) { | while ( my $ticket = &$iterator ) { | 
| my $cust = find_customer_for_ticket( $customers, $ticket ); | my $cust = find_customer_for_ticket( $customers, $ticket ); | 
| if ( !$cust ) { | if ( !$cust ) { | 
| say "No customer found for ticket " . $ticket->id; | warn "No customer found for ticket " . $ticket->id; | 
|  |  | 
| # XXX should construct a "new" invoice to pop onto the list |  | 
| next; | next; | 
| } | } | 
|  | if ( !$cust->{invoice} ) { | 
|  | say "$cust->{id} has no open invoices [" . $ticket->id . ']'; | 
|  | next; | 
|  | } | 
|  | say 'Giving ticket ' . $ticket->id . " to $cust->{id}"; | 
| my $invoice = $cust->{invoice}; | my $invoice = $cust->{invoice}; | 
|  |  | 
| #foreach my $r ($ticket->requestors) { | my $project = make_project( $ticket, $cust ); | 
| #    $users->id( $r ); |  | 
| #    $users->retrieve; |  | 
| #    print Dump $users; |  | 
| #} |  | 
| my $project = make_project( $invoice, $ticket ); |  | 
| next unless @{ $project->{fees} } || @{ $project->{expenses} }; | next unless @{ $project->{fees} } || @{ $project->{expenses} }; | 
|  |  | 
| foreach my $fee ( @{ $project->{fees} } ) { | foreach my $fee ( @{ $project->{fees} } ) { | 
|  |  | 
| } | } | 
|  |  | 
| if ($discount_time) { | if ($discount_time) { | 
| $invoice->{discount}{amount} += $discount_time * $fee->{rate}; | $invoice->{discount}{amount} | 
|  | += round( $discount_time * $fee->{rate} ); | 
| $invoice->{discount}{hours}{$h_type} += $discount_time; | $invoice->{discount}{hours}{$h_type} += $discount_time; | 
|  |  | 
| $h_type = '' if $h_type eq 'default'; | $h_type = '' if $h_type eq 'default'; | 
|  |  | 
| } | } | 
| } | } | 
|  |  | 
| print " Added ticket $project->{id}\n"; |  | 
| push @{ $invoice->{projects} }, $project; | push @{ $invoice->{projects} }, $project; | 
| } | } | 
|  |  | 
|  |  | 
| my $invoice = $cust->{invoice}; | my $invoice = $cust->{invoice}; | 
| next unless $invoice->{projects} && @{ $invoice->{projects} }; | next unless $invoice->{projects} && @{ $invoice->{projects} }; | 
|  |  | 
|  | my %li = ( custid => $custid, invdate => DateTime->now->ymd, ); | 
|  |  | 
| foreach my $project ( @{ $invoice->{projects} } ) { | foreach my $project ( @{ $invoice->{projects} } ) { | 
| print "$project->{title}\n"; |  | 
| if ( $project->{transactions} ) { | if ( $project->{transactions} ) { | 
| push @{ $cust->{transactions} }, @{ $project->{transactions} }; | push @{ $li{transactions} }, @{ $project->{transactions} }; | 
| } | } | 
| my $subtotal = 0; | my $subtotal = 0; | 
| foreach my $fee ( @{ $project->{fees} } ) { | foreach my $fee ( @{ $project->{fees} } ) { | 
| my $amount = round( $fee->{count} * $fee->{rate} ); | my $amount = round( $fee->{count} * $fee->{rate} ); | 
| print "  $amount (" . ( $fee->{count} * $fee->{rate} ) . ")\n"; |  | 
| $subtotal += $amount; | $subtotal += $amount; | 
| } | } | 
| foreach my $expense ( @{ $project->{expenses} } ) { | foreach my $expense ( @{ $project->{expenses} } ) { | 
|  |  | 
| } | } | 
| $project->{total} = $subtotal; | $project->{total} = $subtotal; | 
| $invoice->{total} += $subtotal; | $invoice->{total} += $subtotal; | 
| print " Subtotal $subtotal\n"; |  | 
| } | } | 
|  |  | 
| if ( $invoice->{discount} ) { | if ( $invoice->{discount} ) { | 
|  |  | 
| $invoice->{total_due} = $invoice->{total} + $invoice->{past_due}; | $invoice->{total_due} = $invoice->{total} + $invoice->{past_due}; | 
| } | } | 
|  |  | 
| # XXX Here we need to "make_address" | next unless $invoice->{total} > 0 || $invoice->{total_due}; | 
|  |  | 
| $invoice->{info} = $config->get('info'); | $invoice->{info} = $config->get('info'); | 
| $invoice->{from} = $config->get('from'); | my $from = $config->get('from'); | 
| $invoice->{to}   = $cust->{address}; | $from = get_user($from) if !ref $from; | 
|  |  | 
|  | $invoice->{organization} = $from->{organization} || $from->{name}; | 
|  | $invoice->{from}         = make_address($from); | 
|  | $invoice->{to}           = make_address( $cust->{address} || $custid ); | 
|  |  | 
| $state->{lastinvoice}++; | $state->{lastinvoice}++; | 
| $invoice->{id}   = $state->{lastinvoice}; | $invoice->{id} = $state->{lastinvoice}; | 
| $invoice->{file} = 'invoice_' . $state->{lastinvoice} . '.pdf'; | $invoice->{file} = sprintf 'invoice_%06d.pdf', $state->{lastinvoice}; | 
|  | $invoice->{logo} = $config->get('logo'); | 
|  |  | 
| my %li = ( custid => $custid, ); | foreach my $k (qw/ file transactions start end total past_due total_due /) | 
|  | { | 
|  | my $v; | 
|  | if    ( $invoice->{$k} ) { $v = $invoice->{$k} } | 
|  | elsif ( $cust->{$k} )    { $v = $cust->{$k} } | 
|  |  | 
| foreach my $k (qw/ transactions /) { | if ( defined $v && length $v ) { | 
| if    ( $invoice->{$k} ) { $li{$k} = $invoice->{$k} } | if ( ref $v eq 'DateTime' ) { | 
| elsif ( $cust->{$k} )    { $li{$k} = $cust->{$k} } | $li{$k} = $v->ymd; | 
|  | } | 
|  | else { | 
|  | $li{$k} = $v; | 
|  | } | 
|  | } | 
| } | } | 
|  | $state->{invoice}->{ $li{end} }{ $invoice->{id} } = \%li; | 
|  |  | 
| $state->{invoice}->{ $invoice->{end}->ymd }{ $state->{lastinvoice} } |  | 
| = \%li; |  | 
|  |  | 
| print "Created Invoice\n"; |  | 
| print Dump $invoice; |  | 
|  |  | 
| foreach my $key (qw/ start end /) { | foreach my $key (qw/ start end /) { | 
| if ( exists $invoice->{$key} ) { | if ( exists $invoice->{$key} ) { | 
| $invoice->{$key} = $invoice->{$key}->strftime('%B %d, %Y'); | $invoice->{$key} = $invoice->{$key}->strftime('%B %d, %Y'); | 
| } | } | 
| } | } | 
|  |  | 
| my $tt = Template->new; | my $invoice_dir = $config->get('invoice_dir'); | 
| $tt->process( 'invoice.tex.tt', $invoice, $invoice->{file} ) | File::Path::make_path($invoice_dir); | 
|  | my $file = join '/', $invoice_dir, $invoice->{file}; | 
|  |  | 
|  | my $tt = Template->new( INCLUDE_PATH => $config->get('template_dir'), ) | 
|  | || die $Template::ERROR, "\n"; | 
|  |  | 
|  | $tt->process( $config->get('invoice_template'), $invoice, $file ) | 
| or die $tt->error . "\n"; | or die $tt->error . "\n"; | 
|  |  | 
| print "Generated $invoice->{file}\n"; | printf "Generated %s for %s: \$%.02f\n", $invoice->{file}, $custid, | 
|  | $invoice->{total}; | 
| } | } | 
|  |  | 
| # XXX Save State |  | 
| print Dump $state; |  | 
| $state->save; | $state->save; | 
|  |  | 
| sub round { | sub round { | 
|  |  | 
| sub find_customer_for_ticket { | sub find_customer_for_ticket { | 
| my ( $customers, $ticket ) = @_; | my ( $customers, $ticket ) = @_; | 
|  |  | 
| INVOICE: foreach my $cust ( values %{$customers} ) { | foreach my $cust ( values %{$customers} ) { | 
| next INVOICE unless $cust->{match}; | next unless $cust->{match}; | 
| foreach my $m ( @{ $cust->{match} } ) { | foreach my $m ( @{ $cust->{match} } ) { | 
| my $type = $m->{type}; | my $type = $m->{type}; | 
| my $thing = join ' ', $ticket->$type; | my $match = exists $m->{$type} ? lc($m->{$type}) : qr/\Q$m->{regex}\E/; | 
|  | my $thing = [ map { lc } $ticket->$type ]; | 
|  |  | 
| if ( $m->{$type} ) { | if (! $match) { | 
| 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!"; | warn "Invalid match!"; | 
| next INVOICE; | next; | 
| } | } | 
|  | return $cust if ($match ~~ $thing); | 
| } | } | 
| return $cust; |  | 
| } | } | 
|  |  | 
| return fake_customer($ticket); | return fake_customer( $customers, $ticket ); | 
| } | } | 
|  |  | 
| sub fake_customer { | sub fake_customer { | 
| my ($ticket) = @_; | my ( $customers, $ticket ) = @_; | 
|  |  | 
| # XXX eventually, will generate a customer we can bill! | my $cust = $config->get('default') || {}; | 
| return; |  | 
|  | ( $cust->{id} ) = $ticket->requestors; | 
|  | return unless $cust->{id}; | 
|  |  | 
|  | $cust->{match} = [ | 
|  | {   type  => 'requestors', | 
|  | regex => $cust->{id}, | 
|  | } | 
|  | ]; | 
|  |  | 
|  | if ( my $invoice = make_invoice($cust) ) { | 
|  | $cust->{invoice} = $invoice; | 
|  | } | 
|  |  | 
|  | $customers->{ $cust->{id} } = $cust; | 
|  | return $cust; | 
| } | } | 
|  |  | 
|  | sub get_user { | 
|  | my ($id) = @_; | 
|  |  | 
|  | state %users; | 
|  | return $users{$id} if $users{$id}; | 
|  |  | 
|  | my %map = ( | 
|  | address_one   => 'addr1', | 
|  | address_two   => 'addr2', | 
|  | email_address => 'email', | 
|  | real_name     => 'name', | 
|  | name          => 'username', | 
|  | ); | 
|  |  | 
|  | my $users = RT::Client::REST::User->new( rt => $rt, id => $id ); | 
|  | $users->retrieve; | 
|  |  | 
|  | my %user; | 
|  | foreach my $m ( keys %{ $users->_attributes } ) { | 
|  | next unless $users->can($m); | 
|  |  | 
|  | my $v = $users->$m; | 
|  | next unless $v; | 
|  |  | 
|  | $m = $map{$m} if exists $map{$m}; | 
|  |  | 
|  | $user{$m} = $v; | 
|  | } | 
|  |  | 
|  | $users{$id} = \%user; | 
|  | return \%user; | 
|  | } | 
|  |  | 
|  | sub make_invoice { | 
|  | my ($cust) = @_; | 
|  |  | 
|  | my $day  = $cust->{day}       ||= 0; | 
|  | my $per  = $cust->{per}       ||= 'week'; | 
|  | my $freq = $cust->{frequency} ||= 1; | 
|  |  | 
|  | my $day_method; | 
|  | given ($per) { | 
|  | when ('week')  { $per = 'weeks';  $day_method = 'dow' } | 
|  | when ('month') { $per = 'months'; $day_method = 'day' } | 
|  | default { die "Unknown per [$per]\n" } | 
|  | } | 
|  |  | 
|  | my $billends = DateTime->now | 
|  | ->subtract( days => 1 ) | 
|  | ->set( hour => 23, minute => 59, second => 59 ); | 
|  |  | 
|  | # XXX This is helpful, but monthly and billday > 28 == !!! | 
|  | $billends->subtract( days => 1 ) | 
|  | while $day && $billends->$day_method != $day; | 
|  |  | 
|  | my $date = $billends->clone->subtract( $per => $freq ) | 
|  | ->set( hour => 0, minute => 0, second => 0 ); | 
|  |  | 
|  | my %invoice = ( end => $billends->clone->subtract( seconds => 1 ) ); | 
|  |  | 
|  | my $lastinvoice = $state->last_invoice( $cust->{id} ); | 
|  | if ( $lastinvoice->{date} ) { | 
|  | my $last_invoice_date = ymd_to_DateTime( $lastinvoice->{date} ); | 
|  | $date = $last_invoice_date->clone->add( days => 1 ); | 
|  |  | 
|  | $invoice{start} = $date->clone; | 
|  |  | 
|  | $startdate = $date->clone->subtract( $per => $freq ) | 
|  | if !$startdate || $startdate > $date; | 
|  | } | 
|  |  | 
|  | # Is the start date more than $freq $per before the end date? | 
|  | my $diff = $billends - $date; | 
|  | return if $diff->in_units($per) < 1; | 
|  |  | 
|  | if ( $cust->{base_rate} ) { | 
|  | my ( $project, $hours ) = make_base_project( | 
|  | $cust, | 
|  | {   startdate  => $date, | 
|  | enddate    => $billends, | 
|  | per        => $per, | 
|  | freq       => $freq, | 
|  | day        => $day, | 
|  | day_method => $day_method, | 
|  | } | 
|  | ); | 
|  |  | 
|  | if ( @{ $project->{fees} } ) { | 
|  | $invoice{end}   = $project->{end}; | 
|  | $invoice{hours} = $hours; | 
|  | push @{ $invoice{projects} }, $project; | 
|  | } | 
|  | } | 
|  | elsif ( $cust->{hours} ) { | 
|  | $invoice{hours} = [ | 
|  | {   end   => $invoice{end}->clone, | 
|  | hours => $cust->{hours}, | 
|  | } | 
|  | ]; | 
|  | } | 
|  |  | 
|  | return if $invoice{start} && $invoice{end} < $invoice{start}; | 
|  |  | 
|  | return \%invoice; | 
|  | } | 
|  |  | 
|  | sub make_base_project { | 
|  | my ( $cust, $args ) = @_; | 
|  |  | 
|  | my $date       = $args->{startdate}; | 
|  | my $enddate    = $args->{enddate}; | 
|  | my $per        = $args->{per}; | 
|  | my $freq       = $args->{freq}; | 
|  | my $day        = $args->{day}; | 
|  | my $day_method = $args->{day_method}; | 
|  |  | 
|  | my $title | 
|  | = $freq == 1 | 
|  | ? ucfirst( $cust->{per} . 'ly' ) | 
|  | : $freq . ' ' . ucfirst( $cust->{per} ); | 
|  | $title .= ' Retainer'; | 
|  |  | 
|  | my %project = ( title => $title, start => $date->clone, fees => [], ); | 
|  | my @hours; | 
|  |  | 
|  | while ( $date < $enddate ) { | 
|  | my $start = $date->clone; | 
|  |  | 
|  | $date->add( $per => $freq ); | 
|  | $date = $enddate->clone if $date > $enddate; | 
|  |  | 
|  | # XXX This is helpful, but monthly and billday > 28 == !!! | 
|  | $date->subtract( days => 1 ) while $day && $date->$day_method != $day; | 
|  |  | 
|  | my $end = $date->clone->subtract( seconds => 1 ); | 
|  |  | 
|  | last if $end < $start; | 
|  |  | 
|  | $project{end} = $end->clone; | 
|  |  | 
|  | push @{ $project{fees} }, | 
|  | { | 
|  | count    => 1, | 
|  | rate     => $cust->{base_rate}, | 
|  | contents => $start->ymd . ' to ' . $end->ymd, | 
|  | }; | 
|  |  | 
|  | push @hours, | 
|  | { | 
|  | start => $start->clone, | 
|  | end   => $end->clone, | 
|  | hours => { %{ $cust->{hours} } }, | 
|  | }; | 
|  | } | 
|  |  | 
|  | return \%project, \@hours; | 
|  | } | 
|  |  | 
|  | sub make_address { | 
|  | my ($addr) = @_; | 
|  | my @adr; | 
|  |  | 
|  | $addr = get_user($addr) unless ref $addr; | 
|  |  | 
|  | if ( $addr->{organization} ) { | 
|  | push @adr, $addr->{organization}; | 
|  | } | 
|  | elsif ( $addr->{name} && !$addr->{attn} ) { | 
|  | push @adr, $addr->{name}; | 
|  | } | 
|  |  | 
|  | if (   ( $addr->{addr1} || $addr->{addr2} ) | 
|  | && $addr->{city} | 
|  | && $addr->{state} | 
|  | && $addr->{zip} ) | 
|  | { | 
|  | push @adr, $addr->{attn}  if $addr->{attn}; | 
|  | push @adr, $addr->{addr1} if $addr->{addr1}; | 
|  | push @adr, $addr->{addr2} if $addr->{addr2}; | 
|  | push @adr, | 
|  | $addr->{city} . ', ' . $addr->{state} . '  ' . $addr->{zip}; | 
|  | } | 
|  | else { | 
|  | push @adr, $addr->{email} if $addr->{email}; | 
|  | } | 
|  |  | 
|  | return join "\n\n", @adr; | 
|  | } | 
|  |  | 
| sub make_project { | sub make_project { | 
| my ( $invoice, $ticket ) = @_; | my ( $ticket, $cust ) = @_; | 
|  |  | 
| my %project = ( | my %project = ( | 
| id     => $ticket->id, | id     => $ticket->id, | 
|  |  | 
| title  => $ticket->subject, | title  => $ticket->subject, | 
| detail => 'Ticket: ' | detail => 'Ticket: ' | 
| . $ticket->id | . $ticket->id | 
| . ' Queue: ' | . ' Status: ' | 
| . $ticket->queue | . $ticket->status | 
| . ' Requestors: ' | . ' Requestors: ' | 
| . join( ', ', $ticket->requestors ), | . join( ', ', $ticket->requestors ), | 
| fees     => [], | fees     => [], | 
|  |  | 
| next unless $txn->time_taken; | next unless $txn->time_taken; | 
| next if $state->txn_is_invoiced( $txn->id ); | next if $state->txn_is_invoiced( $txn->id ); | 
|  |  | 
| my $fee = make_fee( $ticket, $txn, $invoice->{rates} ); | my $fee = make_fee( $txn, $cust->{rates}, $ticket ); | 
|  |  | 
| if ( !( $fee->{rate} && $fee->{count} ) ) { | if ( !( $fee->{rate} && $fee->{count} ) ) { | 
| warn "Invalid Fee, no rate or count"; | warn "Invalid Fee, no rate or count"; | 
| next; | next; | 
| } | } | 
|  |  | 
| next if $invoice->{start} && $invoice->{start} > $fee->{date}; | my $invoice = $cust->{invoice}; | 
|  | next | 
|  | if $invoice->{start} | 
|  | && $invoice->{start} > $fee->{date}; | 
| next if $invoice->{end} < $fee->{date}; | next if $invoice->{end} < $fee->{date}; | 
|  |  | 
| push @{ $project{fees} },         $fee; | push @{ $project{fees} },         $fee; | 
|  |  | 
| } | } | 
|  |  | 
| sub make_fee { | sub make_fee { | 
| my ( $ticket, $txn, $rates ) = @_; | my ( $txn, $rates, $ticket ) = @_; | 
|  |  | 
|  | # XXX Only need $ticket for the alternate subject | 
|  |  | 
| my $work_time = sprintf "%.03f", $txn->time_taken / 60; | my $work_time = sprintf "%.03f", $txn->time_taken / 60; | 
| my $work_type = $txn->cf('WorkType'); | my $work_type = $txn->cf('WorkType'); | 
| my $work_rate = $rates->{$work_type} || $rates->{default} || 0; |  | 
|  |  | 
| my %fee = ( | my %fee = ( | 
| id       => $txn->id, | id       => $txn->id, | 
|  |  | 
| . $txn->id . ')' . "\n\n" | . $txn->id . ')' . "\n\n" | 
| . ( $txn->data || $ticket->subject ), | . ( $txn->data || $ticket->subject ), | 
| count => $work_time, | count => $work_time, | 
| rate  => $work_rate, |  | 
| type  => $work_type, | type  => $work_type, | 
| date  => ymd_to_DateTime( $txn->created ), | date  => ymd_to_DateTime( $txn->created ), | 
|  | rate  => $rates->{$work_type} || $rates->{default} || 0, | 
| ); | ); | 
|  |  | 
| if ( $work_type && $work_type ne 'Normal' ) { | if ( $work_type && $work_type ne 'Normal' ) { | 
|  |  | 
|  |  | 
| sub ymd_to_DateTime { | sub ymd_to_DateTime { | 
| my ($ymd) = @_; | my ($ymd) = @_; | 
| my ( $date, $time ) = split ' ', $ymd; | my ( $date, $time ) = split /[\sT]/, $ymd; | 
| my ( $year, $month, $day ) = split '-', $date; | my ( $year, $month, $day ) = split '-', $date; | 
| my ( $hour, $minute, $second ) = split ':', $time if $time; | my ( $hour, $minute, $second ) = split ':', $time if $time; | 
|  |  | 
|  |  | 
| use 5.010; | use 5.010; | 
|  |  | 
| use YAML::Any qw/ LoadFile Dump Load /; | use YAML::Any qw/ LoadFile Dump Load /; | 
| use File::Basename; | use File::Spec; | 
|  |  | 
| sub new { | sub new { | 
| my ( $class, $args ) = @_; | my ( $class, $args ) = @_; | 
|  |  | 
| sub get { | sub get { | 
| my ( $self, $key ) = @_; | my ( $self, $key ) = @_; | 
| my $value = Load( Dump( $self->{_config}->{$key} ) ); | my $value = Load( Dump( $self->{_config}->{$key} ) ); | 
| if ( !$value ) { |  | 
| given ($key) { | return $value if $value; | 
| when ('state') { | my ($volume,$directories,$file) =File::Spec->splitpath( | 
| $value = dirname( $self->{file} ) . '/rt_invoice.state' | File::Spec->rel2abs( $self->{file} )); | 
| } |  | 
|  | given ($key) { | 
|  | when ('state') { | 
|  | $value = $self->{file}; | 
|  | $value =~ s/(?:\.[^.]+)?$/\.state/; | 
| } | } | 
|  | when ('invoice_dir') { | 
|  | $value = File::Spec->catdir($volume, $directories, 'invoices' ); | 
|  | } | 
|  | when ('template_dir') { | 
|  | $value = File::Spec->catdir( $volume, $directories ); | 
|  | } | 
|  | when ('invoice_template') { | 
|  | $value = 'invoice.tex.tt'; | 
|  | } | 
|  | when ('logo') { | 
|  | $value = File::Spec->catfile($volume, $directories, 'Logo.pdf' ); | 
|  | } | 
| } | } | 
|  |  | 
| return $value; | return $value; | 
| } | } | 
|  |  |