[BACK]Return to rt_invoices.pl CVS log [TXT][DIR] Up to [local] / RT / Invoicing

Annotation of RT/Invoicing/rt_invoices.pl, Revision 1.3

1.1       andrew      1: #!/usr/bin/perl
                      2: use strict;
                      3: use warnings;
                      4:
                      5: use 5.010;
                      6:
                      7: use YAML::Any;
                      8: use Template;
                      9: use RT::Client::REST;
                     10: use RT::Client::REST::Ticket;
                     11: use RT::Client::REST::User;
                     12:
                     13: use DateTime;
                     14:
                     15: my $config = RTI::Config->new();
                     16:
                     17: #print Dump $config; exit;
                     18:
                     19: my $invoiceid = 1;    # $state->{lastinvoice} + 1;
                     20:
                     21: my $startdate;
                     22: my @invoices;
                     23: foreach my $cust ( @{ $config->get('customers') } ) {
1.3     ! andrew     24:
        !            25:     # my $state = RTI::State->new($cust);
        !            26:     # $invoice{state} = $state;
        !            27:     my $lastinvdate;  # = $state->{lastinvoicedte}; XXX Needs to be a DateTime
        !            28:                       #$lastinvdate = DateTime->now->subtract( months => 2 )
        !            29:                       #    ->set( hour => 0, minute => 0, second => 0 );
        !            30:
1.1       andrew     31:     my %invoice = (
                     32:         from  => $config->get('from'),
1.3     ! andrew     33:         info  => $config->get('info'),
1.1       andrew     34:         to    => $cust->{address},
                     35:         rates => $cust->{rates},
                     36:         match => $cust->{match},
                     37:     );
                     38:
                     39:     if ( $cust->{base_rate} ) {
1.3     ! andrew     40:         my $day  = $cust->{day}       || 1;
1.1       andrew     41:         my $freq = $cust->{frequency} || 1;
                     42:
1.3     ! andrew     43:         my $day_method;
1.1       andrew     44:         my $per;
                     45:         given ( $cust->{per} ) {
1.3     ! andrew     46:             when ('week')  { $per = 'weeks';  $day_method = 'dow' }
        !            47:             when ('month') { $per = 'months'; $day_method = 'day' }
1.1       andrew     48:             default { die "Unknown per [$cust->{per}]\n" }
                     49:         }
                     50:
1.3     ! andrew     51:         my $lastbill
        !            52:             = DateTime->now->set( hour => 0, minute => 0, second => 0 );
        !            53:         while ( $lastbill->$day_method != $day ) {
        !            54:             $lastbill->subtract( days => 1 );
        !            55:         }
        !            56:
        !            57:         my $date
        !            58:             = $lastinvdate
        !            59:             ? $lastinvdate->clone->add( days => 1 )
        !            60:             : $lastbill->clone->subtract( $per => $freq );
1.1       andrew     61:
                     62:         my $title
                     63:             = $freq == 1
                     64:             ? ucfirst( $cust->{per} . 'ly' )
                     65:             : $freq . ' ' . ucfirst( $cust->{per} );
                     66:         $title .= ' Retainer';
                     67:
                     68:         my %project = ( title => $title, fees => [], );
                     69:
1.3     ! andrew     70:         while ( $date < $lastbill ) {
        !            71:             my $start = $date->clone;
        !            72:
        !            73:             $date->add( $per => $freq );
        !            74:             $date = $lastbill->clone if $date > $lastbill;
        !            75:             if ( my $diff = $date->$day_method - $day ) {
        !            76:                 $date->subtract( days => $diff );
        !            77:             }
        !            78:
        !            79:             my $end = $date->clone;
        !            80:
        !            81:             $end->subtract( seconds => 1 );
        !            82:
        !            83:             $startdate = $start->clone if $startdate > $start;
        !            84:             $invoice{start} ||= $start->clone;
        !            85:             $invoice{end} = $end->clone;
1.1       andrew     86:             my %hours = (
1.3     ! andrew     87:                 start => $start->clone,
        !            88:                 end   => $end->clone,
1.1       andrew     89:                 hours => { %{ $cust->{hours} } },
                     90:             );
                     91:
1.3     ! andrew     92:             push @{ $invoice{hours} }, \%hours;
        !            93:             push @{ $project{fees} },
1.1       andrew     94:                 {
                     95:                 count    => 1,
                     96:                 rate     => $cust->{base_rate},
1.3     ! andrew     97:                 contents => $start->ymd . ' to ' . $end->ymd,
1.1       andrew     98:                 };
                     99:
                    100:         }
                    101:
1.3     ! andrew    102:         if ( @{ $project{fees} } ) {
1.2       andrew    103:             push @{ $invoice{projects} }, \%project;
                    104:         }
1.1       andrew    105:     }
                    106:     else {
1.3     ! andrew    107:         $invoice{end} = DateTime->now;
1.1       andrew    108:         push @{ $invoice{hours} },
                    109:             { end => DateTime->now, hours => $cust->{hours} };
                    110:     }
                    111:
1.3     ! andrew    112:     next unless $invoice{end};
1.1       andrew    113:     push @invoices, \%invoice;
                    114: }
                    115:
                    116: #print Dump \@invoices; exit;
                    117:
                    118: my $rt_conf = $config->get('rt');
                    119: my $rt      = RT::Client::REST->new(
                    120:     server  => $rt_conf->{server},
                    121:     timeout => $rt_conf->{timeout},
                    122: );
                    123: $rt->login(
                    124:     username => $rt_conf->{user},
                    125:     password => $rt_conf->{pass},
                    126: );
                    127: my $tickets = RT::Client::REST::Ticket->new( rt => $rt );
                    128: my $users = RT::Client::REST::User->new( rt => $rt );
                    129:
                    130: my @limits = map +{
                    131:     attribute  => 'status',
                    132:     operator   => '=',
                    133:     value      => $_,
                    134:     aggregator => 'or',
                    135:     },
                    136:
                    137:     # XXX This should be a config option
                    138:     qw/ open stalled resolved /;
                    139:
                    140: #push @limits,
                    141: #    {
                    142: #    attribute => 'resolved',
                    143: #    operator  => '>=',
                    144: #    value     => '2011-03-01'
                    145: #    };
                    146: if ($startdate) {
                    147:     push @limits,
                    148:         {
                    149:         attribute => 'last_updated',
                    150:         operator  => '>=',
                    151:         value     => $startdate->ymd,
                    152:         };
                    153: }
                    154:
                    155: #push @limits, { attribute => 'id', operator => '=', value => '51' };
                    156: #push @limits, { attribute => '', operator => '', value => '' };
                    157:
                    158: my $results = $tickets->search(
                    159:     limits  => \@limits,
                    160:     orderby => 'id',
                    161: );
                    162:
                    163: #print Dump $ticket, $results;
                    164:
                    165: my $count = $results->count;
                    166: print "There are $count results that matched your query\n";
                    167:
                    168: my $iterator = $results->get_iterator;
                    169: while ( my $ticket = &$iterator ) {
                    170:     my %project = (
                    171:         id     => $ticket->id,
                    172:         queue  => $ticket->queue,
                    173:         owner  => $ticket->owner,
1.3     ! andrew    174:         title  => $ticket->subject,
        !           175:         detail => 'Ticket: '
        !           176:             . $ticket->id
1.1       andrew    177:             . ' Queue: '
1.3     ! andrew    178:             . $ticket->queue
        !           179:             . ' Requestors: '
        !           180:             . join( ', ', $ticket->requestors ),
1.1       andrew    181:         fees     => [],
                    182:         expenses => [],
                    183:     );
                    184:
                    185:     my $invoice;
                    186: INVOICE: foreach my $i (@invoices) {
                    187:         next INVOICE unless $i->{match};
                    188:         foreach my $m ( @{ $i->{match} } ) {
                    189:             my $type = $m->{type};
                    190:             my $thing = join ' ', $ticket->$type;
                    191:
                    192:             if ( $m->{$type} ) {
                    193:                 my $match = lc $m->{$type};
                    194:                 next INVOICE unless lc($thing) ~~ $match;
                    195:             }
                    196:             elsif ( $m->{regex} ) {
                    197:                 next INVOICE unless $thing ~~ /\Q$m->{regex}\E/;
                    198:             }
                    199:             else {
                    200:                 warn "Invalid match!";
                    201:                 next INVOICE;
                    202:             }
                    203:         }
                    204:         $invoice = $i;
                    205:     }
                    206:
                    207:     if ( !$invoice ) {
                    208:         say "No invoice found for ticket " . $ticket->id;
                    209:
                    210:         # XXX should construct a "new" invoice to pop onto the list
                    211:         next;
                    212:     }
                    213:
                    214:     #foreach my $r ($ticket->requestors) {
                    215:     #    $users->id( $r );
                    216:     #    $users->retrieve;
                    217:     #    print Dump $users;
                    218:     #}
                    219:
                    220:     my $txns = $ticket->transactions( type => [qw(Comment Correspond)] );
                    221:     my $txn_i = $txns->get_iterator;
                    222:     while ( my $txn = $txn_i->() ) {
                    223:         next unless $txn->time_taken;
                    224:
1.3     ! andrew    225:         my ( $date, $time ) = split ' ', $txn->created;
        !           226:         my ( $year, $month,  $day )    = split '-', $date;
        !           227:         my ( $hour, $minute, $second ) = split ':', $time;
        !           228:
        !           229:         my $txn_date = DateTime->new(
        !           230:             year   => $year,
        !           231:             month  => $month,
        !           232:             day    => $day,
        !           233:             hour   => $hour,
        !           234:             minute => $minute,
        !           235:             second => $second,
        !           236:         );
        !           237:
        !           238:         next if $invoice->{start} && $invoice->{start} > $txn_date;
        !           239:         next if $invoice->{end} < $txn_date;
        !           240:
        !           241:         my $hours = {};
        !           242:         if ( $invoice->{hours} ) {
        !           243:             foreach my $h ( @{ $invoice->{hours} } ) {
        !           244:                 next if $h->{start} && $h->{start} > $txn_date;
        !           245:                 next if $h->{end} < $txn_date;
        !           246:
        !           247:                 $hours = $h->{hours};
        !           248:                 last;
        !           249:             }
        !           250:         }
        !           251:
1.1       andrew    252:         my $work_time = sprintf "%.03f", $txn->time_taken / 60;
                    253:         my $work_type = $txn->cf('WorkType');
1.2       andrew    254:         my $work_rate
                    255:             = $invoice->{rates}{$work_type}
                    256:             || $invoice->{rates}{default}
                    257:             || 0;
1.1       andrew    258:
1.3     ! andrew    259:         my $h_type
        !           260:             = exists $hours->{$work_type}
1.1       andrew    261:             ? $work_type
                    262:             : 'default';
                    263:
                    264:         my %fee = (
                    265:             id       => $txn->id,
                    266:             contents => $txn->created . ' ('
                    267:                 . $txn->id . ')' . "\n\n"
                    268:                 . ( $txn->data || $ticket->subject ),
                    269:             count => $work_time,
                    270:             rate  => $work_rate,
                    271:         );
                    272:         if ( $work_type && $work_type ne 'Normal' ) {
                    273:             $fee{detail} = $work_type . ' rate';
                    274:         }
                    275:
                    276:         push @{ $project{fees} }, \%fee;
                    277:
1.3     ! andrew    278:         next unless $hours->{$h_type} && $hours->{$h_type} > 0;
1.1       andrew    279:
                    280:         my $discount_time = 0;
1.3     ! andrew    281:         if ( $hours->{$h_type} > $work_time ) {
        !           282:             $hours->{$h_type} -= $work_time;
1.1       andrew    283:             $discount_time = $work_time;
                    284:         }
                    285:         else {
1.3     ! andrew    286:             $discount_time = $hours->{$h_type};
        !           287:             $hours->{$h_type} = 0;
1.1       andrew    288:         }
                    289:
                    290:         if ($discount_time) {
                    291:             $fee{detail} = "$discount_time $work_type Hours Discounted";
                    292:             $invoice->{discount}{amount} += $discount_time * $fee{rate};
                    293:             $invoice->{discount}{hours}{$work_type} += $discount_time;
                    294:         }
                    295:
                    296:         #print Dump $txn; exit;
                    297:     }
                    298:
                    299:     next unless @{ $project{fees} } || @{ $project{expenses} };
                    300:     print " Added ticket $project{id}\n";
                    301:     push @{ $invoice->{projects} }, \%project;
                    302: }
                    303:
                    304: foreach my $invoice (@invoices) {
                    305:     next unless $invoice->{projects} && @{ $invoice->{projects} };
                    306:
                    307:     foreach my $project ( @{ $invoice->{projects} } ) {
                    308:         print "$project->{title}\n";
                    309:         my $subtotal = 0;
                    310:         foreach my $fee ( @{ $project->{fees} } ) {
                    311:             my $amount = round( $fee->{count} * $fee->{rate} );
                    312:             print "  $amount (" . ( $fee->{count} * $fee->{rate} ) . ")\n";
                    313:             $subtotal += $amount;
                    314:         }
                    315:         foreach my $expense ( @{ $project->{expenses} } ) {
                    316:             $subtotal += round( $expense->{amount} );
                    317:         }
                    318:         $project->{total} = $subtotal;
                    319:         $invoice->{total} += $subtotal;
                    320:         print " Subtotal $subtotal\n";
                    321:     }
                    322:
                    323:     if ( $invoice->{discount} ) {
                    324:         my $c = "Included Hours\n";
                    325:         if ( $invoice->{discount}{hours} ) {
                    326:             foreach my $t ( keys %{ $invoice->{discount}{hours} } ) {
                    327:                 $c .= "\n$invoice->{discount}{hours}{$t} $t hour";
                    328:                 $c .= 's' if $invoice->{discount}{hours}{$t} != 1;
                    329:                 $c .= "\n";
                    330:             }
                    331:         }
                    332:         $invoice->{discount}{contents} = $c;
                    333:         $invoice->{total} -= round( $invoice->{discount}{amount} );
                    334:     }
                    335:
                    336:     if ( $invoice->{past_due} ) {
                    337:         $invoice->{total_due} = $invoice->{total} + $invoice->{past_due};
1.3     ! andrew    338:     }
        !           339:
        !           340:     foreach my $key (qw/ start end /) {
        !           341:         if ( exists $invoice->{$key} ) {
        !           342:             $invoice->{$key} = $invoice->{$key}->strftime('%B %d, %Y');
        !           343:         }
1.1       andrew    344:     }
                    345:
                    346:     $invoice->{id}   = $invoiceid;
                    347:     $invoice->{file} = 'invoice_' . $invoiceid . '.pdf';
                    348:
                    349:     print "Created Invoice\n";
                    350:     print Dump $invoice;
                    351:
                    352:     my $tt = Template->new;
                    353:     $tt->process( 'invoice.tex.tt', $invoice, $invoice->{file} )
                    354:         or die $tt->error . "\n";
                    355:
                    356:     print "Generated $invoice->{file}\n";
                    357:
                    358:     $invoiceid++;
                    359: }
                    360:
                    361: # XXX Save State
                    362:
                    363: sub round {
                    364:     my ($amount) = @_;
                    365:
                    366:     #$amount =~ s/\.\d\d\K.*$//;
                    367:     #return $amount;
                    368:     return sprintf "%.02f", $amount;
                    369: }
                    370:
                    371: package RTI::Config;
                    372: use strict;
                    373: use warnings;
                    374:
                    375: use 5.010;
                    376:
                    377: use YAML::Any qw/ LoadFile /;
                    378:
                    379: sub new {
                    380:     my ( $class, $args ) = @_;
                    381:
                    382:     my $self = { file => '', };
                    383:     bless $self, $class;
                    384:     my $file = $args->{file} || $self->_find_config;
                    385:     $self->read_config($file);
                    386:
                    387:     return $self;
                    388: }
                    389:
                    390: sub _find_config {
                    391:     my ($self) = @_;
                    392:
                    393:     # XXX This needs to be better
                    394:     foreach my $file (qw/ rt_invoice.conf rt_invoice.cfg .rt_invoicerc /) {
                    395:         foreach my $dir ( '.', $ENV{HOME} . '/.rt_invoice', $ENV{HOME} ) {
                    396:             my $path = join '/', $dir, $file;
                    397:             return $path if -e $path;
                    398:         }
                    399:     }
                    400:     return;
                    401: }
                    402:
                    403: sub read_config {
                    404:     my ( $self, $file ) = @_;
                    405:
                    406:     $file ||= $self->{file};
                    407:     die "$file: no such file\n" unless -e $file;
                    408:
                    409:     my $c = LoadFile($file) or die "Unable to load $file\n";
                    410:
                    411:     $c->{customers} ||= [];
                    412:     if ( $c->{default} ) {
                    413:         foreach my $cust ( @{ $c->{customers} } ) {
                    414:             foreach my $k ( keys %{ $c->{default} } ) {
                    415:                 $cust->{$k} //= $c->{default}->{$k};
                    416:             }
                    417:         }
                    418:     }
                    419:
                    420:     $self->{_config} = $c;
                    421:     $self->{file}    = $file;
                    422: }
                    423:
                    424: sub get {
                    425:     my ( $self, $key ) = @_;
                    426:
                    427:     # XXX This should deep copy? not a reference would be good
                    428:     return $self->{_config}->{$key};
                    429: }
                    430:
                    431: package RTI::State;
                    432: use strict;
                    433: use warnings;
                    434:
                    435: use 5.010;
                    436:
                    437: use YAML::Any qw/ LoadFile DumpFile /;
                    438:
                    439: sub new {
                    440:     my ( $class, $args ) = @_;
                    441:
                    442:     my $self = { file => '', };
                    443:     bless $self, $class;
                    444:     my $file = $args->{file} || $self->_find_config;
                    445:     $self->read_config($file);
                    446:
                    447:     return $self;
                    448: }

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>