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

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

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

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