Annotation of RT/Invoicing/rt_invoices.pl, Revision 1.56
1.1 andrew 1: #!/usr/bin/perl
1.56 ! afresh1 2: # $AFresh1: rt_invoices.pl,v 1.55 2020/08/02 17:52:40 afresh1 Exp $
1.11 andrew 3: ########################################################################
4: # Copyright (c) 2011 Andrew Fresh <andrew@afresh1.com>
5: #
6: # Permission to use, copy, modify, and distribute this software for any
7: # purpose with or without fee is hereby granted, provided that the above
8: # copyright notice and this permission notice appear in all copies.
9: #
10: # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11: # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12: # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13: # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14: # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15: # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16: # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17: ########################################################################
1.1 andrew 18: use strict;
19: use warnings;
20:
21: use 5.010;
1.53 andrew 22:
23: # Because we don't have a real cert
24: $ENV{PERL_LWP_SSL_VERIFY_HOSTNAME} = 0;
1.1 andrew 25:
26: use Template;
27: use RT::Client::REST;
28: use RT::Client::REST::Ticket;
29: use RT::Client::REST::User;
30:
1.18 andrew 31: use File::Path;
1.1 andrew 32: use DateTime;
33:
1.47 andrew 34: use List::Util qw/ sum /;
35:
1.40 andrew 36: use lib './lib'; # XXX This is fragile, there are better ways
37: use RTI::Config;
38: use RTI::State;
1.45 andrew 39: use RTI::Util qw/ round ymd_to_DateTime /;
1.40 andrew 40:
1.1 andrew 41: my $config = RTI::Config->new();
1.6 andrew 42: my $state = RTI::State->new( $config->get('state') );
1.1 andrew 43:
1.6 andrew 44: my $rt_conf = $config->get('rt');
45: my $rt = RT::Client::REST->new(
46: server => $rt_conf->{server},
47: timeout => $rt_conf->{timeout},
48: );
49: my $tickets = RT::Client::REST::Ticket->new( rt => $rt );
1.1 andrew 50:
1.6 andrew 51: $rt->login( username => $rt_conf->{user}, password => $rt_conf->{pass} );
52:
1.22 andrew 53: #use YAML;
1.6 andrew 54: #print Dump $config, $state; exit;
1.1 andrew 55:
1.6 andrew 56: my $customers = $config->get('customers');
1.40 andrew 57: my $startdate = set_dates($customers);
1.1 andrew 58:
59: my @limits = map +{
60: attribute => 'status',
61: operator => '=',
62: value => $_,
63: aggregator => 'or',
64: },
65:
66: # XXX This should be a config option
67: qw/ open stalled resolved /;
68:
1.40 andrew 69: push @limits,
70: {
71: attribute => 'last_updated',
72: operator => '>=',
73: value => $startdate->ymd,
74: };
1.1 andrew 75:
76: my $results = $tickets->search(
77: limits => \@limits,
78: orderby => 'id',
79: );
80:
81: my $count = $results->count;
82: print "There are $count results that matched your query\n";
83:
84: my $iterator = $results->get_iterator;
85: while ( my $ticket = &$iterator ) {
1.43 andrew 86: my $cust = find_customer_for_ticket( $ticket, $customers );
1.6 andrew 87: if ( !$cust ) {
1.10 andrew 88: warn "No customer found for ticket " . $ticket->id;
1.7 andrew 89: next;
90: }
1.25 andrew 91:
1.41 andrew 92: next if $cust->{no_invoice};
1.25 andrew 93: $cust->{invoice} ||= make_invoice($cust);
1.44 andrew 94:
95: die "$cust->{id} has no open invoices [" . $ticket->id . ']'
96: unless $cust->{invoice};
1.1 andrew 97:
1.40 andrew 98: say 'Ticket ' . $ticket->id . " belongs to $cust->{id}";
99:
1.7 andrew 100: my $project = make_project( $ticket, $cust );
1.6 andrew 101: next unless @{ $project->{fees} } || @{ $project->{expenses} };
1.1 andrew 102:
1.6 andrew 103: foreach my $fee ( @{ $project->{fees} } ) {
1.25 andrew 104: my $hours = hours_for_date( $cust->{invoice}, $fee->{date} );
1.1 andrew 105:
1.38 andrew 106: my $type = 'unknown';
107: my $count = $fee->{count};
108: while ( $type && $count > 0 && $type ne 'default' ) {
1.35 andrew 109: $type = exists $hours->{ $fee->{type} }
110: && $hours->{ $fee->{type} } > 0 ? $fee->{type} : 'default';
111:
112: next unless exists $hours->{$type} && $hours->{$type} > 0;
113:
114: my $discount_time = 0;
1.38 andrew 115: if ( $hours->{$type} > $count ) {
116: $hours->{$type} -= $count;
117: $discount_time = $count;
1.35 andrew 118: }
119: else {
120: $discount_time = $hours->{$type};
121: $hours->{$type} = 0;
122: }
123:
124: if ($discount_time) {
125: $cust->{invoice}->{discount}->{amount}
126: += round( $discount_time * $fee->{rate} );
1.36 andrew 127: $cust->{invoice}->{discount}->{hours}{$type}
128: += $discount_time;
1.35 andrew 129:
130: $type = '' if $type eq 'default';
1.38 andrew 131: $count -= $discount_time;
1.35 andrew 132: $fee->{detail} .= " $discount_time $type Hours Discounted";
133: }
1.1 andrew 134: }
135: }
136:
1.25 andrew 137: push @{ $cust->{invoice}->{projects} }, $project;
1.1 andrew 138: }
139:
1.44 andrew 140:
141: if ( my $unpaid_invoices = $state->unpaid_invoices() ) {
142: foreach my $custid ( keys %{$unpaid_invoices} ) {
1.47 andrew 143: my %project
144: = ( title => 'Unpaid Invoices', fees => [], no_total => 1 );
1.44 andrew 145: my $past_due = 0;
1.46 andrew 146: my $unpaid = 0;
1.44 andrew 147:
148: my $cust;
149: foreach ( @{$customers} ) {
150: if ( $_->{id} eq $custid ) {
151: $cust = $_;
152: last;
153: }
154: }
155: $cust ||= fake_customer($custid);
156:
1.46 andrew 157: foreach my $id (
158: sort { $a <=> $b }
159: keys %{ $unpaid_invoices->{$custid} }
160: )
1.44 andrew 161: {
162: my $unpaid = $state->get_invoice($id);
163: my $invdate = ymd_to_DateTime( $unpaid->{invdate} );
164:
1.46 andrew 165: my $content
166: = sprintf( "Invoice %06d from %s", $id, $invdate->ymd );
1.47 andrew 167: if ( $cust->{duedate} && $invdate < $cust->{duedate}) {
1.46 andrew 168: $content = "PAST DUE: $content";
169: $past_due += $unpaid_invoices->{$custid}->{$id};
170: }
171: else {
172: $unpaid += $unpaid_invoices->{$custid}->{$id};
173: }
1.44 andrew 174:
175: push @{ $project{fees} },
176: {
1.46 andrew 177: id => $id,
178: contents => $content,
179: count => 1,
180: rate => $unpaid_invoices->{$custid}->{$id},
1.44 andrew 181: };
182: }
183:
184: if ($past_due) {
1.47 andrew 185: $cust->{invoice} ||= make_invoice($cust);
1.44 andrew 186:
187: $cust->{invoice}->{past_due} = $past_due;
1.46 andrew 188: $cust->{invoice}->{unpaid} = $unpaid;
1.44 andrew 189:
190: unshift @{ $cust->{invoice}->{projects} }, \%project;
191: }
1.48 andrew 192: }
193: }
194:
195: if ( my $credits = $state->credits ) {
196: foreach my $custid ( keys %{$credits} ) {
197:
198: my $cust;
199: foreach ( @{$customers} ) {
200: if ( $_->{id} eq $custid ) {
201: $cust = $_;
202: last;
203: }
204: }
205:
206: next unless $cust;
207: next unless $cust->{invoice};
1.49 andrew 208: next unless $credits->{$custid} < 0;
1.48 andrew 209:
210: $cust->{invoice}->{credit} = $credits->{$custid};
211:
212: unshift @{ $cust->{invoice}->{projects} }, {
213: title => 'Credits',
214: no_total => 1,
215: fees => [
216: { contents => 'Available Credit',
217: count => 1,
218: rate => -$credits->{$custid},
219: }
220: ],
221: };
1.44 andrew 222: }
223: }
224:
1.25 andrew 225: foreach my $cust ( @{$customers} ) {
1.54 andrew 226: my $invoice = $cust->{invoice} ||= make_invoice($cust);
1.25 andrew 227: next unless $invoice && $invoice->{projects} && @{ $invoice->{projects} };
1.1 andrew 228:
1.43 andrew 229: $invoice->{custid} = $cust->{id};
1.40 andrew 230: $invoice->{transactions} = [];
1.7 andrew 231:
1.33 andrew 232: my %transactions;
1.1 andrew 233: foreach my $project ( @{ $invoice->{projects} } ) {
1.6 andrew 234: if ( $project->{transactions} ) {
1.33 andrew 235: %transactions = ( %transactions, %{ $project->{transactions} } );
1.6 andrew 236: }
1.1 andrew 237: my $subtotal = 0;
238: foreach my $fee ( @{ $project->{fees} } ) {
239: my $amount = round( $fee->{count} * $fee->{rate} );
240: $subtotal += $amount;
241: }
242: foreach my $expense ( @{ $project->{expenses} } ) {
243: $subtotal += round( $expense->{amount} );
244: }
245: $project->{total} = $subtotal;
1.47 andrew 246:
247: next if $project->{no_total};
1.1 andrew 248: $invoice->{total} += $subtotal;
249: }
1.40 andrew 250: @{ $invoice->{transactions} } = sort { $a <=> $b } keys %transactions;
1.1 andrew 251:
252: if ( $invoice->{discount} ) {
253: my $c = "Included Hours\n";
254: if ( $invoice->{discount}{hours} ) {
255: foreach my $t ( keys %{ $invoice->{discount}{hours} } ) {
1.35 andrew 256: my $type = $t eq 'default' ? '' : $t;
257: $c .= "\n$invoice->{discount}{hours}{$t} $type hour";
1.1 andrew 258: $c .= 's' if $invoice->{discount}{hours}{$t} != 1;
259: $c .= "\n";
260: }
261: }
262: $invoice->{discount}{contents} = $c;
263: $invoice->{total} -= round( $invoice->{discount}{amount} );
264: }
265:
1.47 andrew 266: if ($invoice->{past_due}) {
267: $invoice->{total_due}
268: = sum( @{ $invoice }{ qw/ total past_due unpaid / } );
269: }
270:
1.9 andrew 271: next unless $invoice->{total} > 0 || $invoice->{total_due};
272:
1.6 andrew 273: $invoice->{info} = $config->get('info');
1.19 andrew 274: my $from = $config->get('from');
275: $from = get_user($from) if !ref $from;
276:
1.40 andrew 277: $invoice->{id} = $state->next_invoice_id;
278: $invoice->{file} = sprintf 'invoice_%06d.pdf', $invoice->{id};
279:
1.19 andrew 280: $invoice->{organization} = $from->{organization} || $from->{name};
1.29 andrew 281: $invoice->{from} = make_address($from);
1.40 andrew 282: $invoice->{to} = make_address( $cust->{address} || $cust->{id} );
283: $invoice->{logo} = $config->get('logo');
1.6 andrew 284:
1.43 andrew 285: $state->add_invoice($invoice);
1.10 andrew 286:
1.18 andrew 287: my $invoice_dir = $config->get('invoice_dir');
288: File::Path::make_path($invoice_dir);
289: my $file = join '/', $invoice_dir, $invoice->{file};
290:
1.21 andrew 291: my $tt = Template->new( INCLUDE_PATH => $config->get('template_dir'), )
292: || die $Template::ERROR, "\n";
1.20 andrew 293:
294: $tt->process( $config->get('invoice_template'), $invoice, $file )
1.1 andrew 295: or die $tt->error . "\n";
296:
1.25 andrew 297: printf "Generated %s for %s: \$%.02f\n", $invoice->{file}, $cust->{id},
1.9 andrew 298: $invoice->{total};
1.40 andrew 299:
1.1 andrew 300: }
301:
1.6 andrew 302: $state->save;
1.1 andrew 303:
1.6 andrew 304: sub find_customer_for_ticket {
1.43 andrew 305: my ( $ticket, $customers ) = @_;
1.4 andrew 306:
1.25 andrew 307: foreach my $cust ( @{$customers} ) {
1.22 andrew 308: next unless $cust->{match};
1.6 andrew 309: foreach my $m ( @{ $cust->{match} } ) {
1.4 andrew 310: my $type = $m->{type};
1.55 afresh1 311: my @things = map {lc} $ticket->$type;
312: if ( exists $m->{$type} ) {
313: if ( !$m->{$type} ) {
314: warn "Invalid match!";
315: next;
316: }
317: my $match = lc $m->{$type};
318: for my $thing (@things) {
319: return $cust if $thing eq $match;
320: }
321: }
322: else {
323: my $match = qr/\Q$m->{regex}\E/;
324: for my $thing (@things) {
325: return $cust if $thing =~ $match;
326: }
1.4 andrew 327: }
328: }
1.6 andrew 329: }
330:
1.44 andrew 331: return fake_customer($ticket->requestors);
332: }
333:
334: sub fake_customer {
335: my ($custid) = @_;
336: return unless $custid;
337:
1.40 andrew 338: my $cust = $config->new_customer;
1.25 andrew 339: push @{$customers}, $cust;
1.8 andrew 340:
1.44 andrew 341: $cust->{id} = $custid;
1.14 andrew 342: $cust->{match} = [
343: { type => 'requestors',
1.16 andrew 344: regex => $cust->{id},
1.14 andrew 345: }
346: ];
1.13 andrew 347:
1.40 andrew 348: set_cust_dates($cust);
1.8 andrew 349: return $cust;
350: }
351:
1.9 andrew 352: sub get_user {
1.8 andrew 353: my ($id) = @_;
354:
1.9 andrew 355: state %users;
356: return $users{$id} if $users{$id};
1.8 andrew 357:
358: my %map = (
359: address_one => 'addr1',
360: address_two => 'addr2',
361: email_address => 'email',
1.9 andrew 362: real_name => 'name',
363: name => 'username',
1.8 andrew 364: );
365:
1.12 andrew 366: my $users = RT::Client::REST::User->new( rt => $rt, id => $id );
1.8 andrew 367: $users->retrieve;
368:
1.9 andrew 369: my %user;
1.8 andrew 370: foreach my $m ( keys %{ $users->_attributes } ) {
371: next unless $users->can($m);
372:
373: my $v = $users->$m;
374: next unless $v;
375:
376: $m = $map{$m} if exists $map{$m};
377:
1.9 andrew 378: $user{$m} = $v;
1.8 andrew 379: }
1.6 andrew 380:
1.9 andrew 381: $users{$id} = \%user;
382: return \%user;
1.13 andrew 383: }
384:
385: sub make_invoice {
1.14 andrew 386: my ($cust) = @_;
387:
1.47 andrew 388: my %invoice = (
389: end => $cust->{billend}->clone->subtract( seconds => 1 ),
390: total => 0,
391: );
1.43 andrew 392: $invoice{start} = $cust->{startinvoicedate}->clone
1.40 andrew 393: if $cust->{startinvoicedate};
1.16 andrew 394:
395: if ( $cust->{base_rate} ) {
1.43 andrew 396: my ( $project, $hours ) = make_base_project($cust);
1.16 andrew 397:
398: if ( @{ $project->{fees} } ) {
399: $invoice{end} = $project->{end};
400: $invoice{hours} = $hours;
401: push @{ $invoice{projects} }, $project;
402: }
403: }
404: elsif ( $cust->{hours} ) {
1.14 andrew 405: $invoice{hours} = [
1.16 andrew 406: { end => $invoice{end}->clone,
1.14 andrew 407: hours => $cust->{hours},
408: }
409: ];
410: }
1.13 andrew 411:
412: return \%invoice;
1.12 andrew 413: }
414:
1.16 andrew 415: sub make_base_project {
1.43 andrew 416: my ($cust) = @_;
1.16 andrew 417:
1.40 andrew 418: my $date = $cust->{billstart}->clone;
419: my $billend = $cust->{billend}->clone;
1.43 andrew 420: my ($freq) = get_billing_frequency($cust);
1.16 andrew 421:
422: my $title
1.40 andrew 423: = $cust->{frequency} == 1
1.16 andrew 424: ? ucfirst( $cust->{per} . 'ly' )
425: : $freq . ' ' . ucfirst( $cust->{per} );
426: $title .= ' Retainer';
427:
428: my %project = ( title => $title, start => $date->clone, fees => [], );
429: my @hours;
430:
1.28 andrew 431: while ( $date < $billend ) {
1.16 andrew 432: my $start = $date->clone;
433:
1.51 andrew 434: $date->add_duration($freq);
1.16 andrew 435:
1.40 andrew 436: my $end = $date > $billend ? $billend->clone : $date->clone;
437: $end->subtract( seconds => 1 );
1.16 andrew 438:
439: $project{end} = $end->clone;
440:
441: push @{ $project{fees} },
442: {
443: count => 1,
444: rate => $cust->{base_rate},
445: contents => $start->ymd . ' to ' . $end->ymd,
446: };
447:
448: push @hours,
449: {
450: start => $start->clone,
451: end => $end->clone,
452: hours => { %{ $cust->{hours} } },
453: };
454: }
455:
456: return \%project, \@hours;
457: }
458:
1.12 andrew 459: sub make_address {
460: my ($addr) = @_;
461: my @adr;
1.16 andrew 462:
463: $addr = get_user($addr) unless ref $addr;
1.12 andrew 464:
465: if ( $addr->{organization} ) {
466: push @adr, $addr->{organization};
467: }
468: elsif ( $addr->{name} && !$addr->{attn} ) {
469: push @adr, $addr->{name};
470: }
471:
472: if ( ( $addr->{addr1} || $addr->{addr2} )
473: && $addr->{city}
474: && $addr->{state}
475: && $addr->{zip} )
476: {
477: push @adr, $addr->{attn} if $addr->{attn};
478: push @adr, $addr->{addr1} if $addr->{addr1};
479: push @adr, $addr->{addr2} if $addr->{addr2};
480: push @adr,
481: $addr->{city} . ', ' . $addr->{state} . ' ' . $addr->{zip};
482: }
483: else {
484: push @adr, $addr->{email} if $addr->{email};
485: }
486:
487: return join "\n\n", @adr;
1.6 andrew 488: }
489:
490: sub make_project {
1.7 andrew 491: my ( $ticket, $cust ) = @_;
1.6 andrew 492:
493: my %project = (
494: id => $ticket->id,
495: queue => $ticket->queue,
496: owner => $ticket->owner,
497: title => $ticket->subject,
498: detail => 'Ticket: '
499: . $ticket->id
1.17 andrew 500: . ' Status: '
501: . $ticket->status
1.6 andrew 502: . ' Requestors: '
503: . join( ', ', $ticket->requestors ),
504: fees => [],
505: expenses => [],
506: );
507:
508: my $txns = $ticket->transactions( type => [qw(Comment Correspond)] );
509: my $txn_i = $txns->get_iterator;
510: while ( my $txn = $txn_i->() ) {
1.33 andrew 511: next if $state->txn_is_invoiced( $txn->id );
512:
1.36 andrew 513: if ( my $expense = make_expense( $txn, $ticket ) ) {
1.33 andrew 514: push @{ $project{expenses} }, $expense;
515: $project{transactions}{ $txn->id } = 1;
516: }
517:
1.6 andrew 518: next unless $txn->time_taken;
519:
1.7 andrew 520: my $fee = make_fee( $txn, $cust->{rates}, $ticket );
521:
1.6 andrew 522: if ( !( $fee->{rate} && $fee->{count} ) ) {
523: warn "Invalid Fee, no rate or count";
524: next;
525: }
526:
1.7 andrew 527: my $invoice = $cust->{invoice};
1.29 andrew 528: if ( $invoice->{start} && $invoice->{start} > $fee->{date} ) {
529: warn "Ticket "
530: . $ticket->id
531: . " has uninvoiced Transaction "
532: . $txn->id . "\n";
1.27 andrew 533: next;
534: }
1.6 andrew 535: next if $invoice->{end} < $fee->{date};
536:
1.36 andrew 537: push @{ $project{fees} }, $fee;
1.33 andrew 538: $project{transactions}{ $txn->id } = 1;
1.4 andrew 539: }
1.6 andrew 540:
541: return \%project;
1.4 andrew 542: }
543:
544: sub make_fee {
1.7 andrew 545: my ( $txn, $rates, $ticket ) = @_;
546:
547: # XXX Only need $ticket for the alternate subject
1.4 andrew 548:
549: my $work_time = sprintf "%.03f", $txn->time_taken / 60;
1.50 andrew 550: my $work_type = $txn->cf('WorkType') || '';
1.34 andrew 551:
552: if ( $work_type =~ s/\s*Onsite//i ) {
1.36 andrew 553:
1.34 andrew 554: # XXX Do something special for onsite activities
555: }
556:
557: $work_type =~ s/^\s+|\s+$//g;
558: $work_type ||= 'Normal';
1.4 andrew 559:
560: my %fee = (
561: id => $txn->id,
562: contents => $txn->created . ' ('
563: . $txn->id . ')' . "\n\n"
564: . ( $txn->data || $ticket->subject ),
565: count => $work_time,
566: type => $work_type,
1.6 andrew 567: date => ymd_to_DateTime( $txn->created ),
1.7 andrew 568: rate => $rates->{$work_type} || $rates->{default} || 0,
1.4 andrew 569: );
570:
571: if ( $work_type && $work_type ne 'Normal' ) {
572: $fee{detail} = $work_type . ' rate';
573: }
574:
575: return \%fee;
1.33 andrew 576: }
577:
578: sub make_expense {
579: my ( $txn, $ticket ) = @_;
580:
581: my $amount = $txn->cf('ExpenseAmount') or return;
582:
583: my %expense = (
584: id => $txn->id,
585: contents => $txn->created . ' ('
586: . $txn->id . ')' . "\n\n"
587: . ( $txn->data || $ticket->subject ),
588: amount => $amount,
589: date => ymd_to_DateTime( $txn->created ),
1.36 andrew 590:
1.33 andrew 591: # detail => ???,
592: );
593:
594: return \%expense;
1.4 andrew 595: }
596:
597: sub hours_for_date {
598: my ( $invoice, $date ) = @_;
599:
600: my $hours = {};
601: if ( $invoice->{hours} ) {
602: foreach my $h ( @{ $invoice->{hours} } ) {
603: next if $h->{start} && $h->{start} > $date;
604: next if $h->{end} < $date;
605:
606: $hours = $h->{hours};
607: last;
608: }
609: }
610: return $hours;
1.6 andrew 611: }
612:
1.40 andrew 613: sub get_billing_frequency {
614: my ($cust) = @_;
615: my $per = $cust->{per} || 'week';
616: my $freq = $cust->{frequency} || 1;
1.1 andrew 617:
1.55 afresh1 618: my $day_method
1.56 ! afresh1 619: = $per eq 'week' ? 'dow'
! 620: : $per eq 'month' ? 'day'
1.55 afresh1 621: : die "Unknown per [$per]\n";
1.1 andrew 622:
1.55 afresh1 623: return DateTime::Duration->new( "${per}s" => $freq ), $day_method;
1.1 andrew 624: }
625:
1.40 andrew 626: sub set_dates {
627: my ($customers) = @_;
1.43 andrew 628:
1.40 andrew 629: my $newest_invoice;
630: my $max_duration;
1.1 andrew 631:
1.40 andrew 632: foreach my $cust ( @{$customers} ) {
633: set_cust_dates($cust);
1.1 andrew 634:
1.40 andrew 635: my ($freq) = get_billing_frequency($cust);
636: $max_duration = $freq
637: if !$max_duration
638: || DateTime::Duration->compare( $freq, $max_duration ) > 0;
1.1 andrew 639:
1.40 andrew 640: if ( $cust->{startinvoicedate} ) {
641: my $date = $cust->{startinvoicedate}->clone;
642: $newest_invoice = $date->clone
643: if !$newest_invoice || $newest_invoice < $date;
1.1 andrew 644: }
645: }
646:
1.51 andrew 647: $newest_invoice ||= DateTime->now;
648:
649: return $newest_invoice->clone->subtract_duration($max_duration)
1.40 andrew 650: ->subtract( days => 1 );
1.1 andrew 651: }
652:
1.40 andrew 653: sub set_cust_dates {
654: my ($cust) = @_;
1.18 andrew 655:
1.40 andrew 656: my $day = $cust->{day} || 0;
657: my ( $freq, $day_method ) = get_billing_frequency($cust);
1.18 andrew 658:
1.41 andrew 659: my $end = DateTime->now( time_zone => 'local' )
1.40 andrew 660: ->set( hour => 0, minute => 0, second => 0 );
1.18 andrew 661:
1.51 andrew 662: my $start = $end->clone->subtract_duration($freq);
1.1 andrew 663:
1.40 andrew 664: # XXX This is helpful, but monthly and billday > 28 == !!!
1.43 andrew 665: $end->subtract( days => 1 ) while $day && $end->$day_method != $day;
1.1 andrew 666:
1.40 andrew 667: my $lastinvoice = $state->last_invoice( $cust->{id} );
668: if ( $lastinvoice && $lastinvoice->{end} ) {
1.41 andrew 669: $start = ymd_to_DateTime( $lastinvoice->{end} )->add( days => 1 );
670: $cust->{startinvoicedate} = $start->clone;
1.6 andrew 671: }
1.42 andrew 672:
673: $cust->{duedate}
674: = $cust->{net}
675: ? DateTime->now->subtract( days => $cust->{net} )
676: : 0;
1.1 andrew 677:
1.51 andrew 678: $cust->{no_invoice} = 1 if $start->clone->add_duration($freq) > $end;
1.41 andrew 679: $cust->{billend} = $end;
680: $cust->{billstart} = $start;
1.1 andrew 681: }
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>