| version 1.42, 2011/12/30 03:20:45 |
version 1.46, 2012/01/27 04:13:45 |
|
|
| #!/usr/bin/perl |
#!/usr/bin/perl |
| # $AFresh1: rt_invoices.pl,v 1.41 2011/12/22 05:21:56 andrew Exp $ |
# $AFresh1: rt_invoices.pl,v 1.45 2011/12/31 02:14:32 andrew Exp $ |
| ######################################################################## |
######################################################################## |
| # Copyright (c) 2011 Andrew Fresh <andrew@afresh1.com> |
# Copyright (c) 2011 Andrew Fresh <andrew@afresh1.com> |
| # |
# |
|
|
| 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') ); |
|
|
| #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); |
| |
|
|
|
| |
|
| 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; |
|
|
| |
|
| 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 ); |
|
|
| } |
} |
| |
|
| |
|
| |
if ( my $unpaid_invoices = $state->unpaid_invoices() ) { |
| |
foreach my $custid ( keys %{$unpaid_invoices} ) { |
| |
my %project = ( title => 'Unpaid Invoices', fees => [], ); |
| |
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} |
| |
&& DateTime->compare( $invdate, $cust->{duedate} ) > 0 ) |
| |
{ |
| |
$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->{invoice}->{past_due} = $past_due; |
| |
$cust->{invoice}->{unpaid} = $unpaid; |
| |
$cust->{invoice}->{total_due} |
| |
= $cust->{invoice}->{total} + $past_due + $unpaid; |
| |
|
| |
unshift @{ $cust->{invoice}->{projects} }, \%project; |
| |
} |
| |
} |
| |
} |
| |
|
| foreach my $cust ( @{$customers} ) { |
foreach my $cust ( @{$customers} ) { |
| my $invoice = $cust->{invoice}; |
my $invoice = $cust->{invoice}; |
| 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; |
|
|
| $invoice->{total} -= round( $invoice->{discount}{amount} ); |
$invoice->{total} -= round( $invoice->{discount}{amount} ); |
| } |
} |
| |
|
| if (my $unpaid_invoices = $state->unpaid_invoices($cust->{id})) { |
|
| my %project = ( title => 'Unpaid Invoices', fees => [], ); |
|
| my $past_due = 0; |
|
| |
|
| foreach my $id ( sort { $a <=> $b } keys %{$unpaid_invoices} ) { |
|
| my $unpaid = $state->get_invoice($id); |
|
| my $invdate = ymd_to_DateTime( $unpaid->{invdate} ); |
|
| |
|
| next |
|
| if $cust->{duedate} |
|
| && DateTime->compare( $invdate, $cust->{duedate} ) > 0; |
|
| |
|
| $past_due += $unpaid_invoices->{$id}; |
|
| push @{ $project{fees} }, { |
|
| id => $id, |
|
| contents => sprintf( |
|
| "Invoice %06d from %s", |
|
| $id, $invdate->ymd |
|
| ), |
|
| count => 1, |
|
| rate => $unpaid_invoices->{$id}, |
|
| }; |
|
| } |
|
| |
|
| if ($past_due) { |
|
| $invoice->{past_due} = $past_due; |
|
| $invoice->{total_due} = $invoice->{total} + $invoice->{past_due}; |
|
| |
|
| unshift @{ $invoice->{projects} }, \%project; |
|
| } |
|
| } |
|
| |
|
| 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'); |
|
|
| $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}; |
|
|
| |
|
| $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}; |
|
|
| } |
} |
| } |
} |
| |
|
| # 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}, |
|
|
| 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 ) ); |
| |
$invoice{start} = $cust->{startinvoicedate}->clone |
| my %invoice = ( end => $cust->{billend}->clone->subtract( seconds => 1 ) ); |
|
| $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}; |
|
|
| } |
} |
| |
|
| 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 |
|
|
| while ( $date < $billend ) { |
while ( $date < $billend ) { |
| my $start = $date->clone; |
my $start = $date->clone; |
| |
|
| $date->add( $freq ); |
$date->add($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 ); |
|
|
| 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'; |
|
|
| default { die "Unknown per [$per]\n" } |
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 { |
sub set_dates { |
| my ($customers) = @_; |
my ($customers) = @_; |
| |
|
| my $newest_invoice; |
my $newest_invoice; |
| my $max_duration; |
my $max_duration; |
| |
|
|
|
| my $start = $end->clone->subtract($freq); |
my $start = $end->clone->subtract($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} ) { |