=================================================================== RCS file: /cvs/RT/Invoicing/rt_invoices.pl,v retrieving revision 1.40 retrieving revision 1.51 diff -u -r1.40 -r1.51 --- RT/Invoicing/rt_invoices.pl 2011/12/22 04:52:28 1.40 +++ RT/Invoicing/rt_invoices.pl 2015/05/07 06:21:49 1.51 @@ -1,5 +1,5 @@ #!/usr/bin/perl -# $AFresh1: rt_invoices.pl,v 1.38 2011/06/21 00:20:28 andrew Exp $ +# $AFresh1: rt_invoices.pl,v 1.50 2013/06/20 04:36:06 andrew Exp $ ######################################################################## # Copyright (c) 2011 Andrew Fresh # @@ -28,9 +28,12 @@ use File::Path; use DateTime; +use List::Util qw/ sum /; + use lib './lib'; # XXX This is fragile, there are better ways use RTI::Config; use RTI::State; +use RTI::Util qw/ round ymd_to_DateTime /; my $config = RTI::Config->new(); my $state = RTI::State->new( $config->get('state') ); @@ -47,7 +50,6 @@ #use YAML; #print Dump $config, $state; exit; - my $customers = $config->get('customers'); my $startdate = set_dates($customers); @@ -78,20 +80,18 @@ my $iterator = $results->get_iterator; while ( my $ticket = &$iterator ) { - my $cust = find_customer_for_ticket($ticket, $customers); + my $cust = find_customer_for_ticket( $ticket, $customers ); if ( !$cust ) { warn "No customer found for ticket " . $ticket->id; next; } - next if $cust->{no_invoices}; + next if $cust->{no_invoice}; $cust->{invoice} ||= make_invoice($cust); - if ( !$cust->{invoice} ) { - $cust->{no_invoices} = 1; - say "$cust->{id} has no open invoices [" . $ticket->id . ']'; - next; - } + die "$cust->{id} has no open invoices [" . $ticket->id . ']' + unless $cust->{invoice}; + say 'Ticket ' . $ticket->id . " belongs to $cust->{id}"; my $project = make_project( $ticket, $cust ); @@ -135,11 +135,95 @@ } +if ( my $unpaid_invoices = $state->unpaid_invoices() ) { + foreach my $custid ( keys %{$unpaid_invoices} ) { + my %project + = ( title => 'Unpaid Invoices', fees => [], no_total => 1 ); + my $past_due = 0; + my $unpaid = 0; + + my $cust; + foreach ( @{$customers} ) { + if ( $_->{id} eq $custid ) { + $cust = $_; + last; + } + } + $cust ||= fake_customer($custid); + + foreach my $id ( + sort { $a <=> $b } + keys %{ $unpaid_invoices->{$custid} } + ) + { + my $unpaid = $state->get_invoice($id); + my $invdate = ymd_to_DateTime( $unpaid->{invdate} ); + + my $content + = sprintf( "Invoice %06d from %s", $id, $invdate->ymd ); + if ( $cust->{duedate} && $invdate < $cust->{duedate}) { + $content = "PAST DUE: $content"; + $past_due += $unpaid_invoices->{$custid}->{$id}; + } + else { + $unpaid += $unpaid_invoices->{$custid}->{$id}; + } + + push @{ $project{fees} }, + { + id => $id, + contents => $content, + count => 1, + rate => $unpaid_invoices->{$custid}->{$id}, + }; + } + + if ($past_due) { + $cust->{invoice} ||= make_invoice($cust); + + $cust->{invoice}->{past_due} = $past_due; + $cust->{invoice}->{unpaid} = $unpaid; + + unshift @{ $cust->{invoice}->{projects} }, \%project; + } + } +} + +if ( my $credits = $state->credits ) { + foreach my $custid ( keys %{$credits} ) { + + my $cust; + foreach ( @{$customers} ) { + if ( $_->{id} eq $custid ) { + $cust = $_; + last; + } + } + + next unless $cust; + next unless $cust->{invoice}; + next unless $credits->{$custid} < 0; + + $cust->{invoice}->{credit} = $credits->{$custid}; + + unshift @{ $cust->{invoice}->{projects} }, { + title => 'Credits', + no_total => 1, + fees => [ + { contents => 'Available Credit', + count => 1, + rate => -$credits->{$custid}, + } + ], + }; + } +} + foreach my $cust ( @{$customers} ) { my $invoice = $cust->{invoice}; next unless $invoice && $invoice->{projects} && @{ $invoice->{projects} }; - $invoice->{custid} = $cust->{id}; + $invoice->{custid} = $cust->{id}; $invoice->{transactions} = []; my %transactions; @@ -156,6 +240,8 @@ $subtotal += round( $expense->{amount} ); } $project->{total} = $subtotal; + + next if $project->{no_total}; $invoice->{total} += $subtotal; } @{ $invoice->{transactions} } = sort { $a <=> $b } keys %transactions; @@ -174,30 +260,11 @@ $invoice->{total} -= round( $invoice->{discount}{amount} ); } - $invoice->{past_due} = 0; - if (my $unpaid_invoices = $state->unpaid_invoices($cust->{id})) { - my %project = ( title => 'Unpaid Invoices', fees => [], ); - - foreach my $id ( sort { $a <=> $b } keys %{$unpaid_invoices} ) { - my $unpaid = $state->get_invoice($id); - $invoice->{past_due} += $unpaid_invoices->{$id}; - push @{ $project{fees} }, { - id => $id, - contents => sprintf( - "Invoice %06d from %s", - $id, ymd_to_DateTime( $unpaid->{invdate} )->ymd - ), - count => 1, - rate => $unpaid_invoices->{$id}, - }; - } - unshift @{ $invoice->{projects} }, \%project; + if ($invoice->{past_due}) { + $invoice->{total_due} + = sum( @{ $invoice }{ qw/ total past_due unpaid / } ); } - if ( $invoice->{past_due} ) { - $invoice->{total_due} = $invoice->{total} + $invoice->{past_due}; - } - next unless $invoice->{total} > 0 || $invoice->{total_due}; $invoice->{info} = $config->get('info'); @@ -212,15 +279,8 @@ $invoice->{to} = make_address( $cust->{address} || $cust->{id} ); $invoice->{logo} = $config->get('logo'); - $state->add_invoice( $invoice ); + $state->add_invoice($invoice); - - foreach my $key (qw/ start end /) { - if ( exists $invoice->{$key} ) { - $invoice->{$key} = $invoice->{$key}->strftime('%B %d, %Y'); - } - } - my $invoice_dir = $config->get('invoice_dir'); File::Path::make_path($invoice_dir); my $file = join '/', $invoice_dir, $invoice->{file}; @@ -238,16 +298,8 @@ $state->save; -sub round { - my ($amount) = @_; - - #$amount =~ s/\.\d\d\K.*$//; - #return $amount; - return sprintf "%.02f", $amount; -} - sub find_customer_for_ticket { - my ($ticket, $customers) = @_; + my ( $ticket, $customers ) = @_; foreach my $cust ( @{$customers} ) { next unless $cust->{match}; @@ -267,13 +319,17 @@ } } - # Fake customer if we didn't find one - my $cust = $config->new_customer; + return fake_customer($ticket->requestors); +} - ( $cust->{id} ) = $ticket->requestors; - return unless $cust->{id}; +sub fake_customer { + my ($custid) = @_; + return unless $custid; + + my $cust = $config->new_customer; push @{$customers}, $cust; + $cust->{id} = $custid; $cust->{match} = [ { type => 'requestors', regex => $cust->{id}, @@ -320,19 +376,15 @@ sub make_invoice { my ($cust) = @_; - return unless $cust->{billstart}; - - my ($freq) = get_billing_frequency($cust); - return if $cust->{billstart}->clone->add($freq) > $cust->{billend}; - - my %invoice = ( end => $cust->{billend}->clone->subtract( seconds => 1 ) ); - $invoice{start} = $cust->{startinvoicedate}->clone + my %invoice = ( + end => $cust->{billend}->clone->subtract( seconds => 1 ), + total => 0, + ); + $invoice{start} = $cust->{startinvoicedate}->clone if $cust->{startinvoicedate}; - return if $invoice{start} && $invoice{start} > $invoice{end}; - if ( $cust->{base_rate} ) { - my ( $project, $hours ) = make_base_project( $cust ); + my ( $project, $hours ) = make_base_project($cust); if ( @{ $project->{fees} } ) { $invoice{end} = $project->{end}; @@ -352,11 +404,11 @@ } sub make_base_project { - my ( $cust ) = @_; + my ($cust) = @_; my $date = $cust->{billstart}->clone; my $billend = $cust->{billend}->clone; - my ($freq) = get_billing_frequency($cust); + my ($freq) = get_billing_frequency($cust); my $title = $cust->{frequency} == 1 @@ -370,7 +422,7 @@ while ( $date < $billend ) { my $start = $date->clone; - $date->add( $freq ); + $date->add_duration($freq); my $end = $date > $billend ? $billend->clone : $date->clone; $end->subtract( seconds => 1 ); @@ -486,7 +538,7 @@ # XXX Only need $ticket for the alternate subject my $work_time = sprintf "%.03f", $txn->time_taken / 60; - my $work_type = $txn->cf('WorkType'); + my $work_type = $txn->cf('WorkType') || ''; if ( $work_type =~ s/\s*Onsite//i ) { @@ -549,23 +601,6 @@ return $hours; } -sub ymd_to_DateTime { - my ($ymd) = @_; - my ( $date, $time ) = split /[\sT]/, $ymd; - my ( $year, $month, $day ) = split '-', $date; - my ( $hour, $minute, $second ) = split ':', $time if $time; - - return DateTime->new( - year => $year, - month => $month, - day => $day, - hour => $hour || 0, - minute => $minute || 0, - second => $second || 0, - time_zone => 'local', - ); -} - sub get_billing_frequency { my ($cust) = @_; my $per = $cust->{per} || 'week'; @@ -578,12 +613,12 @@ default { die "Unknown per [$per]\n" } } - return DateTime::Duration->new($per => $freq), $day_method; + return DateTime::Duration->new( $per => $freq ), $day_method; } sub set_dates { my ($customers) = @_; - + my $newest_invoice; my $max_duration; @@ -602,7 +637,9 @@ } } - return $newest_invoice->clone->subtract($max_duration) + $newest_invoice ||= DateTime->now; + + return $newest_invoice->clone->subtract_duration($max_duration) ->subtract( days => 1 ); } @@ -612,22 +649,26 @@ my $day = $cust->{day} || 0; my ( $freq, $day_method ) = get_billing_frequency($cust); - my $billend = DateTime->now( time_zone => 'local' ) + my $end = DateTime->now( time_zone => 'local' ) ->set( hour => 0, minute => 0, second => 0 ); - my $date = $billend->clone->subtract($freq); + my $start = $end->clone->subtract_duration($freq); # XXX This is helpful, but monthly and billday > 28 == !!! - $billend->subtract( days => 1 ) - while $day && $billend->$day_method != $day; + $end->subtract( days => 1 ) while $day && $end->$day_method != $day; - $cust->{billend} = $billend; - my $lastinvoice = $state->last_invoice( $cust->{id} ); if ( $lastinvoice && $lastinvoice->{end} ) { - $date = ymd_to_DateTime( $lastinvoice->{end} )->add( days => 1 ); - $cust->{startinvoicedate} = $date->clone; + $start = ymd_to_DateTime( $lastinvoice->{end} )->add( days => 1 ); + $cust->{startinvoicedate} = $start->clone; } - $cust->{billstart} = $date; + $cust->{duedate} + = $cust->{net} + ? DateTime->now->subtract( days => $cust->{net} ) + : 0; + + $cust->{no_invoice} = 1 if $start->clone->add_duration($freq) > $end; + $cust->{billend} = $end; + $cust->{billstart} = $start; }