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>