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

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

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

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