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

Diff for /RT/Invoicing/rt_invoices.pl between version 1.41 and 1.56

version 1.41, 2011/12/22 05:21:56 version 1.56, 2020/08/02 18:57:28
Line 1 
Line 1 
 #!/usr/bin/perl  #!/usr/bin/perl
 # $AFresh1: rt_invoices.pl,v 1.40 2011/12/22 04:52:28 andrew Exp $  # $AFresh1: rt_invoices.pl,v 1.55 2020/08/02 17:52:40 afresh1 Exp $
 ########################################################################  ########################################################################
 # Copyright (c) 2011 Andrew Fresh <andrew@afresh1.com>  # Copyright (c) 2011 Andrew Fresh <andrew@afresh1.com>
 #  #
Line 20 
Line 20 
   
 use 5.010;  use 5.010;
   
   # Because we don't have a real cert
   $ENV{PERL_LWP_SSL_VERIFY_HOSTNAME} = 0;
   
 use Template;  use Template;
 use RT::Client::REST;  use RT::Client::REST;
 use RT::Client::REST::Ticket;  use RT::Client::REST::Ticket;
Line 28 
Line 31 
 use File::Path;  use File::Path;
 use DateTime;  use DateTime;
   
   use List::Util qw/ sum /;
   
 use lib './lib';    # XXX This is fragile, there are better ways  use lib './lib';    # XXX This is fragile, there are better ways
 use RTI::Config;  use RTI::Config;
 use RTI::State;  use RTI::State;
   use RTI::Util qw/ round ymd_to_DateTime /;
   
 my $config = RTI::Config->new();  my $config = RTI::Config->new();
 my $state  = RTI::State->new( $config->get('state') );  my $state  = RTI::State->new( $config->get('state') );
Line 47 
Line 53 
 #use YAML;  #use YAML;
 #print Dump $config, $state; exit;  #print Dump $config, $state; exit;
   
   
 my $customers = $config->get('customers');  my $customers = $config->get('customers');
 my $startdate = set_dates($customers);  my $startdate = set_dates($customers);
   
Line 78 
Line 83 
   
 my $iterator = $results->get_iterator;  my $iterator = $results->get_iterator;
 while ( my $ticket = &$iterator ) {  while ( my $ticket = &$iterator ) {
     my $cust = find_customer_for_ticket($ticket, $customers);      my $cust = find_customer_for_ticket( $ticket, $customers );
     if ( !$cust ) {      if ( !$cust ) {
         warn "No customer found for ticket " . $ticket->id;          warn "No customer found for ticket " . $ticket->id;
         next;          next;
Line 86 
Line 91 
   
     next if $cust->{no_invoice};      next if $cust->{no_invoice};
     $cust->{invoice} ||= make_invoice($cust);      $cust->{invoice} ||= make_invoice($cust);
     if ( !$cust->{invoice} ) {  
         $cust->{no_invoice} = 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}";      say 'Ticket ' . $ticket->id . " belongs to $cust->{id}";
   
     my $project = make_project( $ticket, $cust );      my $project = make_project( $ticket, $cust );
Line 135 
Line 138 
 }  }
   
   
   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} ) {  foreach my $cust ( @{$customers} ) {
     my $invoice = $cust->{invoice};      my $invoice = $cust->{invoice} ||= make_invoice($cust);
     next unless $invoice && $invoice->{projects} && @{ $invoice->{projects} };      next unless $invoice && $invoice->{projects} && @{ $invoice->{projects} };
   
     $invoice->{custid}  = $cust->{id};      $invoice->{custid}       = $cust->{id};
     $invoice->{transactions} = [];      $invoice->{transactions} = [];
   
     my %transactions;      my %transactions;
Line 156 
Line 243 
             $subtotal += round( $expense->{amount} );              $subtotal += round( $expense->{amount} );
         }          }
         $project->{total} = $subtotal;          $project->{total} = $subtotal;
   
           next if $project->{no_total};
         $invoice->{total} += $subtotal;          $invoice->{total} += $subtotal;
     }      }
     @{ $invoice->{transactions} } = sort { $a <=> $b } keys %transactions;      @{ $invoice->{transactions} } = sort { $a <=> $b } keys %transactions;
Line 174 
Line 263 
         $invoice->{total} -= round( $invoice->{discount}{amount} );          $invoice->{total} -= round( $invoice->{discount}{amount} );
     }      }
   
     $invoice->{past_due} = 0;      if ($invoice->{past_due}) {
     if (my $unpaid_invoices = $state->unpaid_invoices($cust->{id})) {          $invoice->{total_due}
         my %project = ( title => 'Unpaid Invoices', fees => [], );              = sum( @{ $invoice }{ qw/ total past_due unpaid / } );
   
         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} = $invoice->{total} + $invoice->{past_due};  
     }  
   
     next unless $invoice->{total} > 0 || $invoice->{total_due};      next unless $invoice->{total} > 0 || $invoice->{total_due};
   
     $invoice->{info} = $config->get('info');      $invoice->{info} = $config->get('info');
Line 212 
Line 282 
     $invoice->{to}   = make_address( $cust->{address} || $cust->{id} );      $invoice->{to}   = make_address( $cust->{address} || $cust->{id} );
     $invoice->{logo} = $config->get('logo');      $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');      my $invoice_dir = $config->get('invoice_dir');
     File::Path::make_path($invoice_dir);      File::Path::make_path($invoice_dir);
     my $file = join '/', $invoice_dir, $invoice->{file};      my $file = join '/', $invoice_dir, $invoice->{file};
Line 238 
Line 301 
   
 $state->save;  $state->save;
   
 sub round {  
     my ($amount) = @_;  
   
     #$amount =~ s/\.\d\d\K.*$//;  
     #return $amount;  
     return sprintf "%.02f", $amount;  
 }  
   
 sub find_customer_for_ticket {  sub find_customer_for_ticket {
     my ($ticket, $customers) = @_;      my ( $ticket, $customers ) = @_;
   
     foreach my $cust ( @{$customers} ) {      foreach my $cust ( @{$customers} ) {
         next unless $cust->{match};          next unless $cust->{match};
         foreach my $m ( @{ $cust->{match} } ) {          foreach my $m ( @{ $cust->{match} } ) {
             my $type = $m->{type};              my $type = $m->{type};
             my $match              my @things = map {lc} $ticket->$type;
                 = exists $m->{$type}              if ( exists $m->{$type} ) {
                 ? lc( $m->{$type} )                  if ( !$m->{$type} ) {
                 : qr/\Q$m->{regex}\E/;                      warn "Invalid match!";
             my $thing = [ map {lc} $ticket->$type ];                      next;
                   }
             if ( !$match ) {                  my $match = lc $m->{$type};
                 warn "Invalid match!";                  for my $thing (@things) {
                 next;                       return $cust if $thing eq $match;
                   }
             }              }
             return $cust if ( $match ~~ $thing );              else {
                   my $match = qr/\Q$m->{regex}\E/;
                   for my $thing (@things) {
                        return $cust if $thing =~ $match;
                   }
               }
         }          }
     }      }
   
     # Fake customer if we didn't find one      return fake_customer($ticket->requestors);
     my $cust = $config->new_customer;  }
   
     ( $cust->{id} ) = $ticket->requestors;  sub fake_customer {
     return unless $cust->{id};      my ($custid) = @_;
       return unless $custid;
   
       my $cust = $config->new_customer;
     push @{$customers}, $cust;      push @{$customers}, $cust;
   
       $cust->{id} = $custid;
     $cust->{match} = [      $cust->{match} = [
         {   type  => 'requestors',          {   type  => 'requestors',
             regex => $cust->{id},              regex => $cust->{id},
Line 320 
Line 385 
 sub make_invoice {  sub make_invoice {
     my ($cust) = @_;      my ($cust) = @_;
   
     return if $cust->{no_invoice};      my %invoice = (
     return unless $cust->{billstart};          end   => $cust->{billend}->clone->subtract( seconds => 1 ),
           total => 0,
     my %invoice = ( end => $cust->{billend}->clone->subtract( seconds => 1 ) );      );
     $invoice{start} = $cust->{startinvoicedate}->clone      $invoice{start} = $cust->{startinvoicedate}->clone
         if $cust->{startinvoicedate};          if $cust->{startinvoicedate};
   
     return if $invoice{start} && $invoice{start} > $invoice{end};  
   
     if ( $cust->{base_rate} ) {      if ( $cust->{base_rate} ) {
         my ( $project, $hours ) = make_base_project( $cust );          my ( $project, $hours ) = make_base_project($cust);
   
         if ( @{ $project->{fees} } ) {          if ( @{ $project->{fees} } ) {
             $invoice{end}   = $project->{end};              $invoice{end}   = $project->{end};
Line 350 
Line 413 
 }  }
   
 sub make_base_project {  sub make_base_project {
     my ( $cust ) = @_;      my ($cust) = @_;
   
     my $date    = $cust->{billstart}->clone;      my $date    = $cust->{billstart}->clone;
     my $billend = $cust->{billend}->clone;      my $billend = $cust->{billend}->clone;
     my ($freq) = get_billing_frequency($cust);      my ($freq)  = get_billing_frequency($cust);
   
     my $title      my $title
         = $cust->{frequency} == 1          = $cust->{frequency} == 1
Line 368 
Line 431 
     while ( $date < $billend ) {      while ( $date < $billend ) {
         my $start = $date->clone;          my $start = $date->clone;
   
         $date->add( $freq );          $date->add_duration($freq);
   
         my $end = $date > $billend ? $billend->clone : $date->clone;          my $end = $date > $billend ? $billend->clone : $date->clone;
         $end->subtract( seconds => 1 );          $end->subtract( seconds => 1 );
Line 484 
Line 547 
     # XXX Only need $ticket for the alternate subject      # XXX Only need $ticket for the alternate subject
   
     my $work_time = sprintf "%.03f", $txn->time_taken / 60;      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 ) {      if ( $work_type =~ s/\s*Onsite//i ) {
   
Line 547 
Line 610 
     return $hours;      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 {  sub get_billing_frequency {
     my ($cust) = @_;      my ($cust) = @_;
     my $per  = $cust->{per}       || 'week';      my $per  = $cust->{per}       || 'week';
     my $freq = $cust->{frequency} || 1;      my $freq = $cust->{frequency} || 1;
   
     my $day_method;      my $day_method
     given ($per) {          = $per eq 'week'  ? 'dow'
         when ('week')  { $per = 'weeks';  $day_method = 'dow' }          : $per eq 'month' ? 'day'
         when ('month') { $per = 'months'; $day_method = 'day' }          : die "Unknown per [$per]\n";
         default { die "Unknown per [$per]\n" }  
     }  
   
     return DateTime::Duration->new($per => $freq), $day_method;      return DateTime::Duration->new( "${per}s" => $freq ), $day_method;
 }  }
   
 sub set_dates {  sub set_dates {
     my ($customers) = @_;      my ($customers) = @_;
   
     my $newest_invoice;      my $newest_invoice;
     my $max_duration;      my $max_duration;
   
Line 600 
Line 644 
         }          }
     }      }
   
     return $newest_invoice->clone->subtract($max_duration)      $newest_invoice ||= DateTime->now;
   
       return $newest_invoice->clone->subtract_duration($max_duration)
         ->subtract( days => 1 );          ->subtract( days => 1 );
 }  }
   
Line 613 
Line 659 
     my $end = DateTime->now( time_zone => 'local' )      my $end = DateTime->now( time_zone => 'local' )
         ->set( hour => 0, minute => 0, second => 0 );          ->set( hour => 0, minute => 0, second => 0 );
   
     my $start = $end->clone->subtract($freq);      my $start = $end->clone->subtract_duration($freq);
   
     # XXX This is helpful, but monthly and billday > 28 == !!!      # XXX This is helpful, but monthly and billday > 28 == !!!
     $end->subtract( days => 1 )      $end->subtract( days => 1 ) while $day && $end->$day_method != $day;
         while $day && $end->$day_method != $day;  
   
     my $lastinvoice = $state->last_invoice( $cust->{id} );      my $lastinvoice = $state->last_invoice( $cust->{id} );
     if ( $lastinvoice && $lastinvoice->{end} ) {      if ( $lastinvoice && $lastinvoice->{end} ) {
Line 625 
Line 670 
         $cust->{startinvoicedate} = $start->clone;          $cust->{startinvoicedate} = $start->clone;
     }      }
   
     $cust->{no_invoice} = 1 if $start->clone->add($freq) > $end;      $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->{billend}    = $end;
     $cust->{billstart}  = $start;      $cust->{billstart}  = $start;
 }  }

Legend:
Removed from v.1.41  
changed lines
  Added in v.1.56

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