add some debug tracing to invoice generation, RT#11452
[freeside.git] / FS / FS / cust_bill.pm
1 package FS::cust_bill;
2
3 use strict;
4 use vars qw( @ISA $DEBUG $me $conf
5              $money_char $date_format $rdate_format $date_format_long );
6 use vars qw( $invoice_lines @buf ); #yuck
7 use Fcntl qw(:flock); #for spool_csv
8 use List::Util qw(min max);
9 use Date::Format;
10 use Text::Template 1.20;
11 use File::Temp 0.14;
12 use String::ShellQuote;
13 use HTML::Entities;
14 use Locale::Country;
15 use Storable qw( freeze thaw );
16 use FS::UID qw( datasrc );
17 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
18 use FS::Record qw( qsearch qsearchs dbh );
19 use FS::cust_main_Mixin;
20 use FS::cust_main;
21 use FS::cust_statement;
22 use FS::cust_bill_pkg;
23 use FS::cust_bill_pkg_display;
24 use FS::cust_bill_pkg_detail;
25 use FS::cust_credit;
26 use FS::cust_pay;
27 use FS::cust_pkg;
28 use FS::cust_credit_bill;
29 use FS::pay_batch;
30 use FS::cust_pay_batch;
31 use FS::cust_bill_event;
32 use FS::cust_event;
33 use FS::part_pkg;
34 use FS::cust_bill_pay;
35 use FS::cust_bill_pay_batch;
36 use FS::part_bill_event;
37 use FS::payby;
38 use FS::bill_batch;
39 use FS::cust_bill_batch;
40
41 @ISA = qw( FS::cust_main_Mixin FS::Record );
42
43 $DEBUG = 0;
44 $me = '[FS::cust_bill]';
45
46 #ask FS::UID to run this stuff for us later
47 FS::UID->install_callback( sub { 
48   $conf = new FS::Conf;
49   $money_char       = $conf->config('money_char')       || '$';  
50   $date_format      = $conf->config('date_format')      || '%x'; #/YY
51   $rdate_format     = $conf->config('date_format')      || '%m/%d/%Y';  #/YYYY
52   $date_format_long = $conf->config('date_format_long') || '%b %o, %Y';
53 } );
54
55 =head1 NAME
56
57 FS::cust_bill - Object methods for cust_bill records
58
59 =head1 SYNOPSIS
60
61   use FS::cust_bill;
62
63   $record = new FS::cust_bill \%hash;
64   $record = new FS::cust_bill { 'column' => 'value' };
65
66   $error = $record->insert;
67
68   $error = $new_record->replace($old_record);
69
70   $error = $record->delete;
71
72   $error = $record->check;
73
74   ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
75
76   @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
77
78   ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
79
80   @cust_pay_objects = $cust_bill->cust_pay;
81
82   $tax_amount = $record->tax;
83
84   @lines = $cust_bill->print_text;
85   @lines = $cust_bill->print_text $time;
86
87 =head1 DESCRIPTION
88
89 An FS::cust_bill object represents an invoice; a declaration that a customer
90 owes you money.  The specific charges are itemized as B<cust_bill_pkg> records
91 (see L<FS::cust_bill_pkg>).  FS::cust_bill inherits from FS::Record.  The
92 following fields are currently supported:
93
94 Regular fields
95
96 =over 4
97
98 =item invnum - primary key (assigned automatically for new invoices)
99
100 =item custnum - customer (see L<FS::cust_main>)
101
102 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
103 L<Time::Local> and L<Date::Parse> for conversion functions.
104
105 =item charged - amount of this invoice
106
107 =item invoice_terms - optional terms override for this specific invoice
108
109 =back
110
111 Customer info at invoice generation time
112
113 =over 4
114
115 =item previous_balance
116
117 =item billing_balance
118
119 =back
120
121 Deprecated
122
123 =over 4
124
125 =item printed - deprecated
126
127 =back
128
129 Specific use cases
130
131 =over 4
132
133 =item closed - books closed flag, empty or `Y'
134
135 =item statementnum - invoice aggregation (see L<FS::cust_statement>)
136
137 =item agent_invid - legacy invoice number
138
139 =back
140
141 =head1 METHODS
142
143 =over 4
144
145 =item new HASHREF
146
147 Creates a new invoice.  To add the invoice to the database, see L<"insert">.
148 Invoices are normally created by calling the bill method of a customer object
149 (see L<FS::cust_main>).
150
151 =cut
152
153 sub table { 'cust_bill'; }
154
155 sub cust_linked { $_[0]->cust_main_custnum; } 
156 sub cust_unlinked_msg {
157   my $self = shift;
158   "WARNING: can't find cust_main.custnum ". $self->custnum.
159   ' (cust_bill.invnum '. $self->invnum. ')';
160 }
161
162 =item insert
163
164 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
165 returns the error, otherwise returns false.
166
167 =cut
168
169 sub insert {
170   my $self = shift;
171   warn "$me insert called\n" if $DEBUG;
172
173   local $SIG{HUP} = 'IGNORE';
174   local $SIG{INT} = 'IGNORE';
175   local $SIG{QUIT} = 'IGNORE';
176   local $SIG{TERM} = 'IGNORE';
177   local $SIG{TSTP} = 'IGNORE';
178   local $SIG{PIPE} = 'IGNORE';
179
180   my $oldAutoCommit = $FS::UID::AutoCommit;
181   local $FS::UID::AutoCommit = 0;
182   my $dbh = dbh;
183
184   my $error = $self->SUPER::insert;
185   if ( $error ) {
186     $dbh->rollback if $oldAutoCommit;
187     return $error;
188   }
189
190   if ( $self->get('cust_bill_pkg') ) {
191     foreach my $cust_bill_pkg ( @{$self->get('cust_bill_pkg')} ) {
192       $cust_bill_pkg->invnum($self->invnum);
193       my $error = $cust_bill_pkg->insert;
194       if ( $error ) {
195         $dbh->rollback if $oldAutoCommit;
196         return "can't create invoice line item: $error";
197       }
198     }
199   }
200
201   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
202   '';
203
204 }
205
206 =item delete
207
208 This method now works but you probably shouldn't use it.  Instead, apply a
209 credit against the invoice.
210
211 Using this method to delete invoices outright is really, really bad.  There
212 would be no record you ever posted this invoice, and there are no check to
213 make sure charged = 0 or that there are no associated cust_bill_pkg records.
214
215 Really, don't use it.
216
217 =cut
218
219 sub delete {
220   my $self = shift;
221   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
222
223   local $SIG{HUP} = 'IGNORE';
224   local $SIG{INT} = 'IGNORE';
225   local $SIG{QUIT} = 'IGNORE';
226   local $SIG{TERM} = 'IGNORE';
227   local $SIG{TSTP} = 'IGNORE';
228   local $SIG{PIPE} = 'IGNORE';
229
230   my $oldAutoCommit = $FS::UID::AutoCommit;
231   local $FS::UID::AutoCommit = 0;
232   my $dbh = dbh;
233
234   foreach my $table (qw(
235     cust_bill_event
236     cust_event
237     cust_credit_bill
238     cust_bill_pay
239     cust_bill_pay
240     cust_credit_bill
241     cust_pay_batch
242     cust_bill_pay_batch
243     cust_bill_pkg
244   )) {
245
246     foreach my $linked ( $self->$table() ) {
247       my $error = $linked->delete;
248       if ( $error ) {
249         $dbh->rollback if $oldAutoCommit;
250         return $error;
251       }
252     }
253
254   }
255
256   my $error = $self->SUPER::delete(@_);
257   if ( $error ) {
258     $dbh->rollback if $oldAutoCommit;
259     return $error;
260   }
261
262   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
263
264   '';
265
266 }
267
268 =item replace [ OLD_RECORD ]
269
270 You can, but probably shouldn't modify invoices...
271
272 Replaces the OLD_RECORD with this one in the database, or, if OLD_RECORD is not
273 supplied, replaces this record.  If there is an error, returns the error,
274 otherwise returns false.
275
276 =cut
277
278 #replace can be inherited from Record.pm
279
280 # replace_check is now the preferred way to #implement replace data checks
281 # (so $object->replace() works without an argument)
282
283 sub replace_check {
284   my( $new, $old ) = ( shift, shift );
285   return "Can't modify closed invoice" if $old->closed =~ /^Y/i;
286   #return "Can't change _date!" unless $old->_date eq $new->_date;
287   return "Can't change _date" unless $old->_date == $new->_date;
288   return "Can't change charged" unless $old->charged == $new->charged
289                                     || $old->charged == 0
290                                     || $new->{'Hash'}{'cc_surcharge_replace_hack'};
291
292   '';
293 }
294
295
296 =item add_cc_surcharge
297
298 Giant hack
299
300 =cut
301
302 sub add_cc_surcharge {
303     my ($self, $pkgnum, $amount) = (shift, shift, shift);
304
305     my $error;
306     my $cust_bill_pkg = new FS::cust_bill_pkg({
307                                     'invnum' => $self->invnum,
308                                     'pkgnum' => $pkgnum,
309                                     'setup' => $amount,
310                         });
311     $error = $cust_bill_pkg->insert;
312     return $error if $error;
313
314     $self->{'Hash'}{'cc_surcharge_replace_hack'} = 1;
315     $self->charged($self->charged+$amount);
316     $error = $self->replace;
317     return $error if $error;
318
319     $self->apply_payments_and_credits;
320 }
321
322
323 =item check
324
325 Checks all fields to make sure this is a valid invoice.  If there is an error,
326 returns the error, otherwise returns false.  Called by the insert and replace
327 methods.
328
329 =cut
330
331 sub check {
332   my $self = shift;
333
334   my $error =
335     $self->ut_numbern('invnum')
336     || $self->ut_foreign_key('custnum', 'cust_main', 'custnum' )
337     || $self->ut_numbern('_date')
338     || $self->ut_money('charged')
339     || $self->ut_numbern('printed')
340     || $self->ut_enum('closed', [ '', 'Y' ])
341     || $self->ut_foreign_keyn('statementnum', 'cust_statement', 'statementnum' )
342     || $self->ut_numbern('agent_invid') #varchar?
343   ;
344   return $error if $error;
345
346   $self->_date(time) unless $self->_date;
347
348   $self->printed(0) if $self->printed eq '';
349
350   $self->SUPER::check;
351 }
352
353 =item display_invnum
354
355 Returns the displayed invoice number for this invoice: agent_invid if
356 cust_bill-default_agent_invid is set and it has a value, invnum otherwise.
357
358 =cut
359
360 sub display_invnum {
361   my $self = shift;
362   if ( $conf->exists('cust_bill-default_agent_invid') && $self->agent_invid ){
363     return $self->agent_invid;
364   } else {
365     return $self->invnum;
366   }
367 }
368
369 =item previous
370
371 Returns a list consisting of the total previous balance for this customer, 
372 followed by the previous outstanding invoices (as FS::cust_bill objects also).
373
374 =cut
375
376 sub previous {
377   my $self = shift;
378   my $total = 0;
379   my @cust_bill = sort { $a->_date <=> $b->_date }
380     grep { $_->owed != 0 && $_->_date < $self->_date }
381       qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) 
382   ;
383   foreach ( @cust_bill ) { $total += $_->owed; }
384   $total, @cust_bill;
385 }
386
387 =item cust_bill_pkg
388
389 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
390
391 =cut
392
393 sub cust_bill_pkg {
394   my $self = shift;
395   qsearch(
396     { 'table'    => 'cust_bill_pkg',
397       'hashref'  => { 'invnum' => $self->invnum },
398       'order_by' => 'ORDER BY billpkgnum',
399     }
400   );
401 }
402
403 =item cust_bill_pkg_pkgnum PKGNUM
404
405 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
406 specified pkgnum.
407
408 =cut
409
410 sub cust_bill_pkg_pkgnum {
411   my( $self, $pkgnum ) = @_;
412   qsearch(
413     { 'table'    => 'cust_bill_pkg',
414       'hashref'  => { 'invnum' => $self->invnum,
415                       'pkgnum' => $pkgnum,
416                     },
417       'order_by' => 'ORDER BY billpkgnum',
418     }
419   );
420 }
421
422 =item cust_pkg
423
424 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
425 this invoice.
426
427 =cut
428
429 sub cust_pkg {
430   my $self = shift;
431   my @cust_pkg = map { $_->pkgnum > 0 ? $_->cust_pkg : () }
432                      $self->cust_bill_pkg;
433   my %saw = ();
434   grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
435 }
436
437 =item no_auto
438
439 Returns true if any of the packages (or their definitions) corresponding to the
440 line items for this invoice have the no_auto flag set.
441
442 =cut
443
444 sub no_auto {
445   my $self = shift;
446   grep { $_->no_auto || $_->part_pkg->no_auto } $self->cust_pkg;
447 }
448
449 =item open_cust_bill_pkg
450
451 Returns the open line items for this invoice.
452
453 Note that cust_bill_pkg with both setup and recur fees are returned as two
454 separate line items, each with only one fee.
455
456 =cut
457
458 # modeled after cust_main::open_cust_bill
459 sub open_cust_bill_pkg {
460   my $self = shift;
461
462   # grep { $_->owed > 0 } $self->cust_bill_pkg
463
464   my %other = ( 'recur' => 'setup',
465                 'setup' => 'recur', );
466   my @open = ();
467   foreach my $field ( qw( recur setup )) {
468     push @open, map  { $_->set( $other{$field}, 0 ); $_; }
469                 grep { $_->owed($field) > 0 }
470                 $self->cust_bill_pkg;
471   }
472
473   @open;
474 }
475
476 =item cust_bill_event
477
478 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
479
480 =cut
481
482 sub cust_bill_event {
483   my $self = shift;
484   qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
485 }
486
487 =item num_cust_bill_event
488
489 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
490
491 =cut
492
493 sub num_cust_bill_event {
494   my $self = shift;
495   my $sql =
496     "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
497   my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql"; 
498   $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
499   $sth->fetchrow_arrayref->[0];
500 }
501
502 =item cust_event
503
504 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
505
506 =cut
507
508 #false laziness w/cust_pkg.pm
509 sub cust_event {
510   my $self = shift;
511   qsearch({
512     'table'     => 'cust_event',
513     'addl_from' => 'JOIN part_event USING ( eventpart )',
514     'hashref'   => { 'tablenum' => $self->invnum },
515     'extra_sql' => " AND eventtable = 'cust_bill' ",
516   });
517 }
518
519 =item num_cust_event
520
521 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
522
523 =cut
524
525 #false laziness w/cust_pkg.pm
526 sub num_cust_event {
527   my $self = shift;
528   my $sql =
529     "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
530     "  WHERE tablenum = ? AND eventtable = 'cust_bill'";
531   my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql"; 
532   $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
533   $sth->fetchrow_arrayref->[0];
534 }
535
536 =item cust_main
537
538 Returns the customer (see L<FS::cust_main>) for this invoice.
539
540 =cut
541
542 sub cust_main {
543   my $self = shift;
544   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
545 }
546
547 =item cust_suspend_if_balance_over AMOUNT
548
549 Suspends the customer associated with this invoice if the total amount owed on
550 this invoice and all older invoices is greater than the specified amount.
551
552 Returns a list: an empty list on success or a list of errors.
553
554 =cut
555
556 sub cust_suspend_if_balance_over {
557   my( $self, $amount ) = ( shift, shift );
558   my $cust_main = $self->cust_main;
559   if ( $cust_main->total_owed_date($self->_date) < $amount ) {
560     return ();
561   } else {
562     $cust_main->suspend(@_);
563   }
564 }
565
566 =item cust_credit
567
568 Depreciated.  See the cust_credited method.
569
570  #Returns a list consisting of the total previous credited (see
571  #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
572  #outstanding credits (FS::cust_credit objects).
573
574 =cut
575
576 sub cust_credit {
577   use Carp;
578   croak "FS::cust_bill->cust_credit depreciated; see ".
579         "FS::cust_bill->cust_credit_bill";
580   #my $self = shift;
581   #my $total = 0;
582   #my @cust_credit = sort { $a->_date <=> $b->_date }
583   #  grep { $_->credited != 0 && $_->_date < $self->_date }
584   #    qsearch('cust_credit', { 'custnum' => $self->custnum } )
585   #;
586   #foreach (@cust_credit) { $total += $_->credited; }
587   #$total, @cust_credit;
588 }
589
590 =item cust_pay
591
592 Depreciated.  See the cust_bill_pay method.
593
594 #Returns all payments (see L<FS::cust_pay>) for this invoice.
595
596 =cut
597
598 sub cust_pay {
599   use Carp;
600   croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
601   #my $self = shift;
602   #sort { $a->_date <=> $b->_date }
603   #  qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
604   #;
605 }
606
607 sub cust_pay_batch {
608   my $self = shift;
609   qsearch('cust_pay_batch', { 'invnum' => $self->invnum } );
610 }
611
612 sub cust_bill_pay_batch {
613   my $self = shift;
614   qsearch('cust_bill_pay_batch', { 'invnum' => $self->invnum } );
615 }
616
617 =item cust_bill_pay
618
619 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
620
621 =cut
622
623 sub cust_bill_pay {
624   my $self = shift;
625   map { $_ } #return $self->num_cust_bill_pay unless wantarray;
626   sort { $a->_date <=> $b->_date }
627     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
628 }
629
630 =item cust_credited
631
632 =item cust_credit_bill
633
634 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
635
636 =cut
637
638 sub cust_credited {
639   my $self = shift;
640   map { $_ } #return $self->num_cust_credit_bill unless wantarray;
641   sort { $a->_date <=> $b->_date }
642     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
643   ;
644 }
645
646 sub cust_credit_bill {
647   shift->cust_credited(@_);
648 }
649
650 =item cust_bill_pay_pkgnum PKGNUM
651
652 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
653 with matching pkgnum.
654
655 =cut
656
657 sub cust_bill_pay_pkgnum {
658   my( $self, $pkgnum ) = @_;
659   map { $_ } #return $self->num_cust_bill_pay_pkgnum($pkgnum) unless wantarray;
660   sort { $a->_date <=> $b->_date }
661     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
662                                 'pkgnum' => $pkgnum,
663                               }
664            );
665 }
666
667 =item cust_credited_pkgnum PKGNUM
668
669 =item cust_credit_bill_pkgnum PKGNUM
670
671 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
672 with matching pkgnum.
673
674 =cut
675
676 sub cust_credited_pkgnum {
677   my( $self, $pkgnum ) = @_;
678   map { $_ } #return $self->num_cust_credit_bill_pkgnum($pkgnum) unless wantarray;
679   sort { $a->_date <=> $b->_date }
680     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
681                                    'pkgnum' => $pkgnum,
682                                  }
683            );
684 }
685
686 sub cust_credit_bill_pkgnum {
687   shift->cust_credited_pkgnum(@_);
688 }
689
690 =item tax
691
692 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
693
694 =cut
695
696 sub tax {
697   my $self = shift;
698   my $total = 0;
699   my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
700                                              'pkgnum' => 0 } );
701   foreach (@taxlines) { $total += $_->setup; }
702   $total;
703 }
704
705 =item owed
706
707 Returns the amount owed (still outstanding) on this invoice, which is charged
708 minus all payment applications (see L<FS::cust_bill_pay>) and credit
709 applications (see L<FS::cust_credit_bill>).
710
711 =cut
712
713 sub owed {
714   my $self = shift;
715   my $balance = $self->charged;
716   $balance -= $_->amount foreach ( $self->cust_bill_pay );
717   $balance -= $_->amount foreach ( $self->cust_credited );
718   $balance = sprintf( "%.2f", $balance);
719   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
720   $balance;
721 }
722
723 sub owed_pkgnum {
724   my( $self, $pkgnum ) = @_;
725
726   #my $balance = $self->charged;
727   my $balance = 0;
728   $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
729
730   $balance -= $_->amount            for $self->cust_bill_pay_pkgnum($pkgnum);
731   $balance -= $_->amount            for $self->cust_credited_pkgnum($pkgnum);
732
733   $balance = sprintf( "%.2f", $balance);
734   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
735   $balance;
736 }
737
738 =item apply_payments_and_credits [ OPTION => VALUE ... ]
739
740 Applies unapplied payments and credits to this invoice.
741
742 A hash of optional arguments may be passed.  Currently "manual" is supported.
743 If true, a payment receipt is sent instead of a statement when
744 'payment_receipt_email' configuration option is set.
745
746 If there is an error, returns the error, otherwise returns false.
747
748 =cut
749
750 sub apply_payments_and_credits {
751   my( $self, %options ) = @_;
752
753   local $SIG{HUP} = 'IGNORE';
754   local $SIG{INT} = 'IGNORE';
755   local $SIG{QUIT} = 'IGNORE';
756   local $SIG{TERM} = 'IGNORE';
757   local $SIG{TSTP} = 'IGNORE';
758   local $SIG{PIPE} = 'IGNORE';
759
760   my $oldAutoCommit = $FS::UID::AutoCommit;
761   local $FS::UID::AutoCommit = 0;
762   my $dbh = dbh;
763
764   $self->select_for_update; #mutex
765
766   my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
767   my @credits  = grep { $_->credited > 0 } $self->cust_main->cust_credit;
768
769   if ( $conf->exists('pkg-balances') ) {
770     # limit @payments & @credits to those w/ a pkgnum grepped from $self
771     my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
772     @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
773     @credits  = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
774   }
775
776   while ( $self->owed > 0 and ( @payments || @credits ) ) {
777
778     my $app = '';
779     if ( @payments && @credits ) {
780
781       #decide which goes first by weight of top (unapplied) line item
782
783       my @open_lineitems = $self->open_cust_bill_pkg;
784
785       my $max_pay_weight =
786         max( map  { $_->part_pkg->pay_weight || 0 }
787              grep { $_ }
788              map  { $_->cust_pkg }
789                   @open_lineitems
790            );
791       my $max_credit_weight =
792         max( map  { $_->part_pkg->credit_weight || 0 }
793              grep { $_ } 
794              map  { $_->cust_pkg }
795                   @open_lineitems
796            );
797
798       #if both are the same... payments first?  it has to be something
799       if ( $max_pay_weight >= $max_credit_weight ) {
800         $app = 'pay';
801       } else {
802         $app = 'credit';
803       }
804     
805     } elsif ( @payments ) {
806       $app = 'pay';
807     } elsif ( @credits ) {
808       $app = 'credit';
809     } else {
810       die "guru meditation #12 and 35";
811     }
812
813     my $unapp_amount;
814     if ( $app eq 'pay' ) {
815
816       my $payment = shift @payments;
817       $unapp_amount = $payment->unapplied;
818       $app = new FS::cust_bill_pay { 'paynum'  => $payment->paynum };
819       $app->pkgnum( $payment->pkgnum )
820         if $conf->exists('pkg-balances') && $payment->pkgnum;
821
822     } elsif ( $app eq 'credit' ) {
823
824       my $credit = shift @credits;
825       $unapp_amount = $credit->credited;
826       $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
827       $app->pkgnum( $credit->pkgnum )
828         if $conf->exists('pkg-balances') && $credit->pkgnum;
829
830     } else {
831       die "guru meditation #12 and 35";
832     }
833
834     my $owed;
835     if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
836       warn "owed_pkgnum ". $app->pkgnum;
837       $owed = $self->owed_pkgnum($app->pkgnum);
838     } else {
839       $owed = $self->owed;
840     }
841     next unless $owed > 0;
842
843     warn "min ( $unapp_amount, $owed )\n" if $DEBUG;
844     $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
845
846     $app->invnum( $self->invnum );
847
848     my $error = $app->insert(%options);
849     if ( $error ) {
850       $dbh->rollback if $oldAutoCommit;
851       return "Error inserting ". $app->table. " record: $error";
852     }
853     die $error if $error;
854
855   }
856
857   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
858   ''; #no error
859
860 }
861
862 =item generate_email OPTION => VALUE ...
863
864 Options:
865
866 =over 4
867
868 =item from
869
870 sender address, required
871
872 =item tempate
873
874 alternate template name, optional
875
876 =item print_text
877
878 text attachment arrayref, optional
879
880 =item subject
881
882 email subject, optional
883
884 =item notice_name
885
886 notice name instead of "Invoice", optional
887
888 =back
889
890 Returns an argument list to be passed to L<FS::Misc::send_email>.
891
892 =cut
893
894 use MIME::Entity;
895
896 sub generate_email {
897
898   my $self = shift;
899   my %args = @_;
900
901   my $me = '[FS::cust_bill::generate_email]';
902
903   my %return = (
904     'from'      => $args{'from'},
905     'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
906   );
907
908   my %opt = (
909     'unsquelch_cdr' => $conf->exists('voip-cdr_email'),
910     'template'      => $args{'template'},
911     'notice_name'   => ( $args{'notice_name'} || 'Invoice' ),
912   );
913
914   my $cust_main = $self->cust_main;
915
916   if (ref($args{'to'}) eq 'ARRAY') {
917     $return{'to'} = $args{'to'};
918   } else {
919     $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
920                            $cust_main->invoicing_list
921                     ];
922   }
923
924   if ( $conf->exists('invoice_html') ) {
925
926     warn "$me creating HTML/text multipart message"
927       if $DEBUG;
928
929     $return{'nobody'} = 1;
930
931     my $alternative = build MIME::Entity
932       'Type'        => 'multipart/alternative',
933       'Encoding'    => '7bit',
934       'Disposition' => 'inline'
935     ;
936
937     my $data;
938     if ( $conf->exists('invoice_email_pdf')
939          and scalar($conf->config('invoice_email_pdf_note')) ) {
940
941       warn "$me using 'invoice_email_pdf_note' in multipart message"
942         if $DEBUG;
943       $data = [ map { $_ . "\n" }
944                     $conf->config('invoice_email_pdf_note')
945               ];
946
947     } else {
948
949       warn "$me not using 'invoice_email_pdf_note' in multipart message"
950         if $DEBUG;
951       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
952         $data = $args{'print_text'};
953       } else {
954         $data = [ $self->print_text(\%opt) ];
955       }
956
957     }
958
959     $alternative->attach(
960       'Type'        => 'text/plain',
961       #'Encoding'    => 'quoted-printable',
962       'Encoding'    => '7bit',
963       'Data'        => $data,
964       'Disposition' => 'inline',
965     );
966
967     $args{'from'} =~ /\@([\w\.\-]+)/;
968     my $from = $1 || 'example.com';
969     my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
970
971     my $logo;
972     my $agentnum = $cust_main->agentnum;
973     if ( defined($args{'template'}) && length($args{'template'})
974          && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
975        )
976     {
977       $logo = 'logo_'. $args{'template'}. '.png';
978     } else {
979       $logo = "logo.png";
980     }
981     my $image_data = $conf->config_binary( $logo, $agentnum);
982
983     my $image = build MIME::Entity
984       'Type'       => 'image/png',
985       'Encoding'   => 'base64',
986       'Data'       => $image_data,
987       'Filename'   => 'logo.png',
988       'Content-ID' => "<$content_id>",
989     ;
990
991     $alternative->attach(
992       'Type'        => 'text/html',
993       'Encoding'    => 'quoted-printable',
994       'Data'        => [ '<html>',
995                          '  <head>',
996                          '    <title>',
997                          '      '. encode_entities($return{'subject'}), 
998                          '    </title>',
999                          '  </head>',
1000                          '  <body bgcolor="#e8e8e8">',
1001                          $self->print_html({ 'cid'=>$content_id, %opt }),
1002                          '  </body>',
1003                          '</html>',
1004                        ],
1005       'Disposition' => 'inline',
1006       #'Filename'    => 'invoice.pdf',
1007     );
1008
1009     my @otherparts = ();
1010     if ( $cust_main->email_csv_cdr ) {
1011
1012       push @otherparts, build MIME::Entity
1013         'Type'        => 'text/csv',
1014         'Encoding'    => '7bit',
1015         'Data'        => [ map { "$_\n" }
1016                              $self->call_details('prepend_billed_number' => 1)
1017                          ],
1018         'Disposition' => 'attachment',
1019         'Filename'    => 'usage-'. $self->invnum. '.csv',
1020       ;
1021
1022     }
1023
1024     if ( $conf->exists('invoice_email_pdf') ) {
1025
1026       #attaching pdf too:
1027       # multipart/mixed
1028       #   multipart/related
1029       #     multipart/alternative
1030       #       text/plain
1031       #       text/html
1032       #     image/png
1033       #   application/pdf
1034
1035       my $related = build MIME::Entity 'Type'     => 'multipart/related',
1036                                        'Encoding' => '7bit';
1037
1038       #false laziness w/Misc::send_email
1039       $related->head->replace('Content-type',
1040         $related->mime_type.
1041         '; boundary="'. $related->head->multipart_boundary. '"'.
1042         '; type=multipart/alternative'
1043       );
1044
1045       $related->add_part($alternative);
1046
1047       $related->add_part($image);
1048
1049       my $pdf = build MIME::Entity $self->mimebuild_pdf(\%opt);
1050
1051       $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
1052
1053     } else {
1054
1055       #no other attachment:
1056       # multipart/related
1057       #   multipart/alternative
1058       #     text/plain
1059       #     text/html
1060       #   image/png
1061
1062       $return{'content-type'} = 'multipart/related';
1063       $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
1064       $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
1065       #$return{'disposition'} = 'inline';
1066
1067     }
1068   
1069   } else {
1070
1071     if ( $conf->exists('invoice_email_pdf') ) {
1072       warn "$me creating PDF attachment"
1073         if $DEBUG;
1074
1075       #mime parts arguments a la MIME::Entity->build().
1076       $return{'mimeparts'} = [
1077         { $self->mimebuild_pdf(\%opt) }
1078       ];
1079     }
1080   
1081     if ( $conf->exists('invoice_email_pdf')
1082          and scalar($conf->config('invoice_email_pdf_note')) ) {
1083
1084       warn "$me using 'invoice_email_pdf_note'"
1085         if $DEBUG;
1086       $return{'body'} = [ map { $_ . "\n" }
1087                               $conf->config('invoice_email_pdf_note')
1088                         ];
1089
1090     } else {
1091
1092       warn "$me not using 'invoice_email_pdf_note'"
1093         if $DEBUG;
1094       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
1095         $return{'body'} = $args{'print_text'};
1096       } else {
1097         $return{'body'} = [ $self->print_text(\%opt) ];
1098       }
1099
1100     }
1101
1102   }
1103
1104   %return;
1105
1106 }
1107
1108 =item mimebuild_pdf
1109
1110 Returns a list suitable for passing to MIME::Entity->build(), representing
1111 this invoice as PDF attachment.
1112
1113 =cut
1114
1115 sub mimebuild_pdf {
1116   my $self = shift;
1117   (
1118     'Type'        => 'application/pdf',
1119     'Encoding'    => 'base64',
1120     'Data'        => [ $self->print_pdf(@_) ],
1121     'Disposition' => 'attachment',
1122     'Filename'    => 'invoice-'. $self->invnum. '.pdf',
1123   );
1124 }
1125
1126 =item send HASHREF | [ TEMPLATE [ , AGENTNUM [ , INVOICE_FROM [ , AMOUNT ] ] ] ]
1127
1128 Sends this invoice to the destinations configured for this customer: sends
1129 email, prints and/or faxes.  See L<FS::cust_main_invoice>.
1130
1131 Options can be passed as a hashref (recommended) or as a list of up to 
1132 four values for templatename, agentnum, invoice_from and amount.
1133
1134 I<template>, if specified, is the name of a suffix for alternate invoices.
1135
1136 I<agentnum>, if specified, means that this invoice will only be sent for customers
1137 of the specified agent or agent(s).  AGENTNUM can be a scalar agentnum (for a
1138 single agent) or an arrayref of agentnums.
1139
1140 I<invoice_from>, if specified, overrides the default email invoice From: address.
1141
1142 I<amount>, if specified, only sends the invoice if the total amount owed on this
1143 invoice and all older invoices is greater than the specified amount.
1144
1145 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1146
1147 =cut
1148
1149 sub queueable_send {
1150   my %opt = @_;
1151
1152   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1153     or die "invalid invoice number: " . $opt{invnum};
1154
1155   my @args = ( $opt{template}, $opt{agentnum} );
1156   push @args, $opt{invoice_from}
1157     if exists($opt{invoice_from}) && $opt{invoice_from};
1158
1159   my $error = $self->send( @args );
1160   die $error if $error;
1161
1162 }
1163
1164 sub send {
1165   my $self = shift;
1166
1167   my( $template, $invoice_from, $notice_name );
1168   my $agentnums = '';
1169   my $balance_over = 0;
1170
1171   if ( ref($_[0]) ) {
1172     my $opt = shift;
1173     $template = $opt->{'template'} || '';
1174     if ( $agentnums = $opt->{'agentnum'} ) {
1175       $agentnums = [ $agentnums ] unless ref($agentnums);
1176     }
1177     $invoice_from = $opt->{'invoice_from'};
1178     $balance_over = $opt->{'balance_over'} if $opt->{'balance_over'};
1179     $notice_name = $opt->{'notice_name'};
1180   } else {
1181     $template = scalar(@_) ? shift : '';
1182     if ( scalar(@_) && $_[0]  ) {
1183       $agentnums = ref($_[0]) ? shift : [ shift ];
1184     }
1185     $invoice_from = shift if scalar(@_);
1186     $balance_over = shift if scalar(@_) && $_[0] !~ /^\s*$/;
1187   }
1188
1189   return 'N/A' unless ! $agentnums
1190                    or grep { $_ == $self->cust_main->agentnum } @$agentnums;
1191
1192   return ''
1193     unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
1194
1195   $invoice_from ||= $self->_agent_invoice_from ||    #XXX should go away
1196                     $conf->config('invoice_from', $self->cust_main->agentnum );
1197
1198   my %opt = (
1199     'template'     => $template,
1200     'invoice_from' => $invoice_from,
1201     'notice_name'  => ( $notice_name || 'Invoice' ),
1202   );
1203
1204   my @invoicing_list = $self->cust_main->invoicing_list;
1205
1206   #$self->email_invoice(\%opt)
1207   $self->email(\%opt)
1208     if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
1209
1210   #$self->print_invoice(\%opt)
1211   $self->print(\%opt)
1212     if grep { $_ eq 'POST' } @invoicing_list; #postal
1213
1214   $self->fax_invoice(\%opt)
1215     if grep { $_ eq 'FAX' } @invoicing_list; #fax
1216
1217   '';
1218
1219 }
1220
1221 =item email HASHREF | [ TEMPLATE [ , INVOICE_FROM ] ] 
1222
1223 Emails this invoice.
1224
1225 Options can be passed as a hashref (recommended) or as a list of up to 
1226 two values for templatename and invoice_from.
1227
1228 I<template>, if specified, is the name of a suffix for alternate invoices.
1229
1230 I<invoice_from>, if specified, overrides the default email invoice From: address.
1231
1232 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1233
1234 =cut
1235
1236 sub queueable_email {
1237   my %opt = @_;
1238
1239   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1240     or die "invalid invoice number: " . $opt{invnum};
1241
1242   my @args = ( $opt{template} );
1243   push @args, $opt{invoice_from}
1244     if exists($opt{invoice_from}) && $opt{invoice_from};
1245
1246   my $error = $self->email( @args );
1247   die $error if $error;
1248
1249 }
1250
1251 #sub email_invoice {
1252 sub email {
1253   my $self = shift;
1254
1255   my( $template, $invoice_from, $notice_name );
1256   if ( ref($_[0]) ) {
1257     my $opt = shift;
1258     $template = $opt->{'template'} || '';
1259     $invoice_from = $opt->{'invoice_from'};
1260     $notice_name = $opt->{'notice_name'} || 'Invoice';
1261   } else {
1262     $template = scalar(@_) ? shift : '';
1263     $invoice_from = shift if scalar(@_);
1264     $notice_name = 'Invoice';
1265   }
1266
1267   $invoice_from ||= $self->_agent_invoice_from ||    #XXX should go away
1268                     $conf->config('invoice_from', $self->cust_main->agentnum );
1269
1270   my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
1271                             $self->cust_main->invoicing_list;
1272
1273   if ( ! @invoicing_list ) { #no recipients
1274     if ( $conf->exists('cust_bill-no_recipients-error') ) {
1275       die 'No recipients for customer #'. $self->custnum;
1276     } else {
1277       #default: better to notify this person than silence
1278       @invoicing_list = ($invoice_from);
1279     }
1280   }
1281
1282   my $subject = $self->email_subject($template);
1283
1284   my $error = send_email(
1285     $self->generate_email(
1286       'from'        => $invoice_from,
1287       'to'          => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1288       'subject'     => $subject,
1289       'template'    => $template,
1290       'notice_name' => $notice_name,
1291     )
1292   );
1293   die "can't email invoice: $error\n" if $error;
1294   #die "$error\n" if $error;
1295
1296 }
1297
1298 sub email_subject {
1299   my $self = shift;
1300
1301   #my $template = scalar(@_) ? shift : '';
1302   #per-template?
1303
1304   my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1305                 || 'Invoice';
1306
1307   my $cust_main = $self->cust_main;
1308   my $name = $cust_main->name;
1309   my $name_short = $cust_main->name_short;
1310   my $invoice_number = $self->invnum;
1311   my $invoice_date = $self->_date_pretty;
1312
1313   eval qq("$subject");
1314 }
1315
1316 =item lpr_data HASHREF | [ TEMPLATE ]
1317
1318 Returns the postscript or plaintext for this invoice as an arrayref.
1319
1320 Options can be passed as a hashref (recommended) or as a single optional value
1321 for template.
1322
1323 I<template>, if specified, is the name of a suffix for alternate invoices.
1324
1325 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1326
1327 =cut
1328
1329 sub lpr_data {
1330   my $self = shift;
1331   my( $template, $notice_name );
1332   if ( ref($_[0]) ) {
1333     my $opt = shift;
1334     $template = $opt->{'template'} || '';
1335     $notice_name = $opt->{'notice_name'} || 'Invoice';
1336   } else {
1337     $template = scalar(@_) ? shift : '';
1338     $notice_name = 'Invoice';
1339   }
1340
1341   my %opt = (
1342     'template'    => $template,
1343     'notice_name' => $notice_name,
1344   );
1345
1346   my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1347   [ $self->$method( \%opt ) ];
1348 }
1349
1350 =item print HASHREF | [ TEMPLATE ]
1351
1352 Prints this invoice.
1353
1354 Options can be passed as a hashref (recommended) or as a single optional
1355 value for template.
1356
1357 I<template>, if specified, is the name of a suffix for alternate invoices.
1358
1359 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1360
1361 =cut
1362
1363 #sub print_invoice {
1364 sub print {
1365   my $self = shift;
1366   my( $template, $notice_name );
1367   if ( ref($_[0]) ) {
1368     my $opt = shift;
1369     $template = $opt->{'template'} || '';
1370     $notice_name = $opt->{'notice_name'} || 'Invoice';
1371   } else {
1372     $template = scalar(@_) ? shift : '';
1373     $notice_name = 'Invoice';
1374   }
1375
1376   my %opt = (
1377     'template'    => $template,
1378     'notice_name' => $notice_name,
1379   );
1380
1381   if($conf->exists('invoice_print_pdf')) {
1382     # Add the invoice to the current batch.
1383     $self->batch_invoice(\%opt);
1384   }
1385   else {
1386     do_print $self->lpr_data(\%opt);
1387   }
1388 }
1389
1390 =item fax_invoice HASHREF | [ TEMPLATE ] 
1391
1392 Faxes this invoice.
1393
1394 Options can be passed as a hashref (recommended) or as a single optional
1395 value for template.
1396
1397 I<template>, if specified, is the name of a suffix for alternate invoices.
1398
1399 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1400
1401 =cut
1402
1403 sub fax_invoice {
1404   my $self = shift;
1405   my( $template, $notice_name );
1406   if ( ref($_[0]) ) {
1407     my $opt = shift;
1408     $template = $opt->{'template'} || '';
1409     $notice_name = $opt->{'notice_name'} || 'Invoice';
1410   } else {
1411     $template = scalar(@_) ? shift : '';
1412     $notice_name = 'Invoice';
1413   }
1414
1415   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1416     unless $conf->exists('invoice_latex');
1417
1418   my $dialstring = $self->cust_main->getfield('fax');
1419   #Check $dialstring?
1420
1421   my %opt = (
1422     'template'    => $template,
1423     'notice_name' => $notice_name,
1424   );
1425
1426   my $error = send_fax( 'docdata'    => $self->lpr_data(\%opt),
1427                         'dialstring' => $dialstring,
1428                       );
1429   die $error if $error;
1430
1431 }
1432
1433 =item batch_invoice [ HASHREF ]
1434
1435 Place this invoice into the open batch (see C<FS::bill_batch>).  If there 
1436 isn't an open batch, one will be created.
1437
1438 =cut
1439
1440 sub batch_invoice {
1441   my ($self, $opt) = @_;
1442   my $batch = FS::bill_batch->get_open_batch;
1443   my $cust_bill_batch = FS::cust_bill_batch->new({
1444       batchnum => $batch->batchnum,
1445       invnum   => $self->invnum,
1446   });
1447   return $cust_bill_batch->insert($opt);
1448 }
1449
1450 =item ftp_invoice [ TEMPLATENAME ] 
1451
1452 Sends this invoice data via FTP.
1453
1454 TEMPLATENAME is unused?
1455
1456 =cut
1457
1458 sub ftp_invoice {
1459   my $self = shift;
1460   my $template = scalar(@_) ? shift : '';
1461
1462   $self->send_csv(
1463     'protocol'   => 'ftp',
1464     'server'     => $conf->config('cust_bill-ftpserver'),
1465     'username'   => $conf->config('cust_bill-ftpusername'),
1466     'password'   => $conf->config('cust_bill-ftppassword'),
1467     'dir'        => $conf->config('cust_bill-ftpdir'),
1468     'format'     => $conf->config('cust_bill-ftpformat'),
1469   );
1470 }
1471
1472 =item spool_invoice [ TEMPLATENAME ] 
1473
1474 Spools this invoice data (see L<FS::spool_csv>)
1475
1476 TEMPLATENAME is unused?
1477
1478 =cut
1479
1480 sub spool_invoice {
1481   my $self = shift;
1482   my $template = scalar(@_) ? shift : '';
1483
1484   $self->spool_csv(
1485     'format'       => $conf->config('cust_bill-spoolformat'),
1486     'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1487   );
1488 }
1489
1490 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1491
1492 Like B<send>, but only sends the invoice if it is the newest open invoice for
1493 this customer.
1494
1495 =cut
1496
1497 sub send_if_newest {
1498   my $self = shift;
1499
1500   return ''
1501     if scalar(
1502                grep { $_->owed > 0 } 
1503                     qsearch('cust_bill', {
1504                       'custnum' => $self->custnum,
1505                       #'_date'   => { op=>'>', value=>$self->_date },
1506                       'invnum'  => { op=>'>', value=>$self->invnum },
1507                     } )
1508              );
1509     
1510   $self->send(@_);
1511 }
1512
1513 =item send_csv OPTION => VALUE, ...
1514
1515 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1516
1517 Options are:
1518
1519 protocol - currently only "ftp"
1520 server
1521 username
1522 password
1523 dir
1524
1525 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1526 and YYMMDDHHMMSS is a timestamp.
1527
1528 See L</print_csv> for a description of the output format.
1529
1530 =cut
1531
1532 sub send_csv {
1533   my($self, %opt) = @_;
1534
1535   #create file(s)
1536
1537   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1538   mkdir $spooldir, 0700 unless -d $spooldir;
1539
1540   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1541   my $file = "$spooldir/$tracctnum.csv";
1542   
1543   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1544
1545   open(CSV, ">$file") or die "can't open $file: $!";
1546   print CSV $header;
1547
1548   print CSV $detail;
1549
1550   close CSV;
1551
1552   my $net;
1553   if ( $opt{protocol} eq 'ftp' ) {
1554     eval "use Net::FTP;";
1555     die $@ if $@;
1556     $net = Net::FTP->new($opt{server}) or die @$;
1557   } else {
1558     die "unknown protocol: $opt{protocol}";
1559   }
1560
1561   $net->login( $opt{username}, $opt{password} )
1562     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1563
1564   $net->binary or die "can't set binary mode";
1565
1566   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1567
1568   $net->put($file) or die "can't put $file: $!";
1569
1570   $net->quit;
1571
1572   unlink $file;
1573
1574 }
1575
1576 =item spool_csv
1577
1578 Spools CSV invoice data.
1579
1580 Options are:
1581
1582 =over 4
1583
1584 =item format - 'default' or 'billco'
1585
1586 =item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the customer has the corresponding invoice destinations set (see L<FS::cust_main_invoice>).
1587
1588 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1589
1590 =item balanceover - if set, only spools the invoice if the total amount owed on this invoice and all older invoices is greater than the specified amount.
1591
1592 =back
1593
1594 =cut
1595
1596 sub spool_csv {
1597   my($self, %opt) = @_;
1598
1599   my $cust_main = $self->cust_main;
1600
1601   if ( $opt{'dest'} ) {
1602     my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1603                              $cust_main->invoicing_list;
1604     return 'N/A' unless $invoicing_list{$opt{'dest'}}
1605                      || ! keys %invoicing_list;
1606   }
1607
1608   if ( $opt{'balanceover'} ) {
1609     return 'N/A'
1610       if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1611   }
1612
1613   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1614   mkdir $spooldir, 0700 unless -d $spooldir;
1615
1616   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1617
1618   my $file =
1619     "$spooldir/".
1620     ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1621     ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1622     '.csv';
1623   
1624   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1625
1626   open(CSV, ">>$file") or die "can't open $file: $!";
1627   flock(CSV, LOCK_EX);
1628   seek(CSV, 0, 2);
1629
1630   print CSV $header;
1631
1632   if ( lc($opt{'format'}) eq 'billco' ) {
1633
1634     flock(CSV, LOCK_UN);
1635     close CSV;
1636
1637     $file =
1638       "$spooldir/".
1639       ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1640       '-detail.csv';
1641
1642     open(CSV,">>$file") or die "can't open $file: $!";
1643     flock(CSV, LOCK_EX);
1644     seek(CSV, 0, 2);
1645   }
1646
1647   print CSV $detail;
1648
1649   flock(CSV, LOCK_UN);
1650   close CSV;
1651
1652   return '';
1653
1654 }
1655
1656 =item print_csv OPTION => VALUE, ...
1657
1658 Returns CSV data for this invoice.
1659
1660 Options are:
1661
1662 format - 'default' or 'billco'
1663
1664 Returns a list consisting of two scalars.  The first is a single line of CSV
1665 header information for this invoice.  The second is one or more lines of CSV
1666 detail information for this invoice.
1667
1668 If I<format> is not specified or "default", the fields of the CSV file are as
1669 follows:
1670
1671 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1672
1673 =over 4
1674
1675 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1676
1677 B<record_type> is C<cust_bill> for the initial header line only.  The
1678 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1679 fields are filled in.
1680
1681 B<record_type> is C<cust_bill_pkg> for detail lines.  Only the first two fields
1682 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1683 are filled in.
1684
1685 =item invnum - invoice number
1686
1687 =item custnum - customer number
1688
1689 =item _date - invoice date
1690
1691 =item charged - total invoice amount
1692
1693 =item first - customer first name
1694
1695 =item last - customer first name
1696
1697 =item company - company name
1698
1699 =item address1 - address line 1
1700
1701 =item address2 - address line 1
1702
1703 =item city
1704
1705 =item state
1706
1707 =item zip
1708
1709 =item country
1710
1711 =item pkg - line item description
1712
1713 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1714
1715 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1716
1717 =item sdate - start date for recurring fee
1718
1719 =item edate - end date for recurring fee
1720
1721 =back
1722
1723 If I<format> is "billco", the fields of the header CSV file are as follows:
1724
1725   +-------------------------------------------------------------------+
1726   |                        FORMAT HEADER FILE                         |
1727   |-------------------------------------------------------------------|
1728   | Field | Description                   | Name       | Type | Width |
1729   | 1     | N/A-Leave Empty               | RC         | CHAR |     2 |
1730   | 2     | N/A-Leave Empty               | CUSTID     | CHAR |    15 |
1731   | 3     | Transaction Account No        | TRACCTNUM  | CHAR |    15 |
1732   | 4     | Transaction Invoice No        | TRINVOICE  | CHAR |    15 |
1733   | 5     | Transaction Zip Code          | TRZIP      | CHAR |     5 |
1734   | 6     | Transaction Company Bill To   | TRCOMPANY  | CHAR |    30 |
1735   | 7     | Transaction Contact Bill To   | TRNAME     | CHAR |    30 |
1736   | 8     | Additional Address Unit Info  | TRADDR1    | CHAR |    30 |
1737   | 9     | Bill To Street Address        | TRADDR2    | CHAR |    30 |
1738   | 10    | Ancillary Billing Information | TRADDR3    | CHAR |    30 |
1739   | 11    | Transaction City Bill To      | TRCITY     | CHAR |    20 |
1740   | 12    | Transaction State Bill To     | TRSTATE    | CHAR |     2 |
1741   | 13    | Bill Cycle Close Date         | CLOSEDATE  | CHAR |    10 |
1742   | 14    | Bill Due Date                 | DUEDATE    | CHAR |    10 |
1743   | 15    | Previous Balance              | BALFWD     | NUM* |     9 |
1744   | 16    | Pmt/CR Applied                | CREDAPPLY  | NUM* |     9 |
1745   | 17    | Total Current Charges         | CURRENTCHG | NUM* |     9 |
1746   | 18    | Total Amt Due                 | TOTALDUE   | NUM* |     9 |
1747   | 19    | Total Amt Due                 | AMTDUE     | NUM* |     9 |
1748   | 20    | 30 Day Aging                  | AMT30      | NUM* |     9 |
1749   | 21    | 60 Day Aging                  | AMT60      | NUM* |     9 |
1750   | 22    | 90 Day Aging                  | AMT90      | NUM* |     9 |
1751   | 23    | Y/N                           | AGESWITCH  | CHAR |     1 |
1752   | 24    | Remittance automation         | SCANLINE   | CHAR |   100 |
1753   | 25    | Total Taxes & Fees            | TAXTOT     | NUM* |     9 |
1754   | 26    | Customer Reference Number     | CUSTREF    | CHAR |    15 |
1755   | 27    | Federal Tax***                | FEDTAX     | NUM* |     9 |
1756   | 28    | State Tax***                  | STATETAX   | NUM* |     9 |
1757   | 29    | Other Taxes & Fees***         | OTHERTAX   | NUM* |     9 |
1758   +-------+-------------------------------+------------+------+-------+
1759
1760 If I<format> is "billco", the fields of the detail CSV file are as follows:
1761
1762                                   FORMAT FOR DETAIL FILE
1763         |                            |           |      |
1764   Field | Description                | Name      | Type | Width
1765   1     | N/A-Leave Empty            | RC        | CHAR |     2
1766   2     | N/A-Leave Empty            | CUSTID    | CHAR |    15
1767   3     | Account Number             | TRACCTNUM | CHAR |    15
1768   4     | Invoice Number             | TRINVOICE | CHAR |    15
1769   5     | Line Sequence (sort order) | LINESEQ   | NUM  |     6
1770   6     | Transaction Detail         | DETAILS   | CHAR |   100
1771   7     | Amount                     | AMT       | NUM* |     9
1772   8     | Line Format Control**      | LNCTRL    | CHAR |     2
1773   9     | Grouping Code              | GROUP     | CHAR |     2
1774   10    | User Defined               | ACCT CODE | CHAR |    15
1775
1776 =cut
1777
1778 sub print_csv {
1779   my($self, %opt) = @_;
1780   
1781   eval "use Text::CSV_XS";
1782   die $@ if $@;
1783
1784   my $cust_main = $self->cust_main;
1785
1786   my $csv = Text::CSV_XS->new({'always_quote'=>1});
1787
1788   if ( lc($opt{'format'}) eq 'billco' ) {
1789
1790     my $taxtotal = 0;
1791     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1792
1793     my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1794
1795     my( $previous_balance, @unused ) = $self->previous; #previous balance
1796
1797     my $pmt_cr_applied = 0;
1798     $pmt_cr_applied += $_->{'amount'}
1799       foreach ( $self->_items_payments, $self->_items_credits ) ;
1800
1801     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1802
1803     $csv->combine(
1804       '',                         #  1 | N/A-Leave Empty               CHAR   2
1805       '',                         #  2 | N/A-Leave Empty               CHAR  15
1806       $opt{'tracctnum'},          #  3 | Transaction Account No        CHAR  15
1807       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
1808       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
1809       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
1810       #$cust_main->payname,        #  7 | Transaction Contact Bill To   CHAR  30
1811       $cust_main->contact,        #  7 | Transaction Contact Bill To   CHAR  30
1812       $cust_main->address2,       #  8 | Additional Address Unit Info  CHAR  30
1813       $cust_main->address1,       #  9 | Bill To Street Address        CHAR  30
1814       '',                         # 10 | Ancillary Billing Information CHAR  30
1815       $cust_main->city,           # 11 | Transaction City Bill To      CHAR  20
1816       $cust_main->state,          # 12 | Transaction State Bill To     CHAR   2
1817
1818       # XXX ?
1819       time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR  10
1820
1821       # XXX ?
1822       $duedate,                   # 14 | Bill Due Date                 CHAR  10
1823
1824       $previous_balance,          # 15 | Previous Balance              NUM*   9
1825       $pmt_cr_applied,            # 16 | Pmt/CR Applied                NUM*   9
1826       sprintf("%.2f", $self->charged), # 17 | Total Current Charges    NUM*   9
1827       $totaldue,                  # 18 | Total Amt Due                 NUM*   9
1828       $totaldue,                  # 19 | Total Amt Due                 NUM*   9
1829       '',                         # 20 | 30 Day Aging                  NUM*   9
1830       '',                         # 21 | 60 Day Aging                  NUM*   9
1831       '',                         # 22 | 90 Day Aging                  NUM*   9
1832       'N',                        # 23 | Y/N                           CHAR   1
1833       '',                         # 24 | Remittance automation         CHAR 100
1834       $taxtotal,                  # 25 | Total Taxes & Fees            NUM*   9
1835       $self->custnum,             # 26 | Customer Reference Number     CHAR  15
1836       '0',                        # 27 | Federal Tax***                NUM*   9
1837       sprintf("%.2f", $taxtotal), # 28 | State Tax***                  NUM*   9
1838       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
1839     );
1840
1841   } else {
1842   
1843     $csv->combine(
1844       'cust_bill',
1845       $self->invnum,
1846       $self->custnum,
1847       time2str("%x", $self->_date),
1848       sprintf("%.2f", $self->charged),
1849       ( map { $cust_main->getfield($_) }
1850           qw( first last company address1 address2 city state zip country ) ),
1851       map { '' } (1..5),
1852     ) or die "can't create csv";
1853   }
1854
1855   my $header = $csv->string. "\n";
1856
1857   my $detail = '';
1858   if ( lc($opt{'format'}) eq 'billco' ) {
1859
1860     my $lineseq = 0;
1861     foreach my $item ( $self->_items_pkg ) {
1862
1863       $csv->combine(
1864         '',                     #  1 | N/A-Leave Empty            CHAR   2
1865         '',                     #  2 | N/A-Leave Empty            CHAR  15
1866         $opt{'tracctnum'},      #  3 | Account Number             CHAR  15
1867         $self->invnum,          #  4 | Invoice Number             CHAR  15
1868         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
1869         $item->{'description'}, #  6 | Transaction Detail         CHAR 100
1870         $item->{'amount'},      #  7 | Amount                     NUM*   9
1871         '',                     #  8 | Line Format Control**      CHAR   2
1872         '',                     #  9 | Grouping Code              CHAR   2
1873         '',                     # 10 | User Defined               CHAR  15
1874       );
1875
1876       $detail .= $csv->string. "\n";
1877
1878     }
1879
1880   } else {
1881
1882     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1883
1884       my($pkg, $setup, $recur, $sdate, $edate);
1885       if ( $cust_bill_pkg->pkgnum ) {
1886       
1887         ($pkg, $setup, $recur, $sdate, $edate) = (
1888           $cust_bill_pkg->part_pkg->pkg,
1889           ( $cust_bill_pkg->setup != 0
1890             ? sprintf("%.2f", $cust_bill_pkg->setup )
1891             : '' ),
1892           ( $cust_bill_pkg->recur != 0
1893             ? sprintf("%.2f", $cust_bill_pkg->recur )
1894             : '' ),
1895           ( $cust_bill_pkg->sdate 
1896             ? time2str("%x", $cust_bill_pkg->sdate)
1897             : '' ),
1898           ($cust_bill_pkg->edate 
1899             ?time2str("%x", $cust_bill_pkg->edate)
1900             : '' ),
1901         );
1902   
1903       } else { #pkgnum tax
1904         next unless $cust_bill_pkg->setup != 0;
1905         $pkg = $cust_bill_pkg->desc;
1906         $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1907         ( $sdate, $edate ) = ( '', '' );
1908       }
1909   
1910       $csv->combine(
1911         'cust_bill_pkg',
1912         $self->invnum,
1913         ( map { '' } (1..11) ),
1914         ($pkg, $setup, $recur, $sdate, $edate)
1915       ) or die "can't create csv";
1916
1917       $detail .= $csv->string. "\n";
1918
1919     }
1920
1921   }
1922
1923   ( $header, $detail );
1924
1925 }
1926
1927 =item comp
1928
1929 Pays this invoice with a compliemntary payment.  If there is an error,
1930 returns the error, otherwise returns false.
1931
1932 =cut
1933
1934 sub comp {
1935   my $self = shift;
1936   my $cust_pay = new FS::cust_pay ( {
1937     'invnum'   => $self->invnum,
1938     'paid'     => $self->owed,
1939     '_date'    => '',
1940     'payby'    => 'COMP',
1941     'payinfo'  => $self->cust_main->payinfo,
1942     'paybatch' => '',
1943   } );
1944   $cust_pay->insert;
1945 }
1946
1947 =item realtime_card
1948
1949 Attempts to pay this invoice with a credit card payment via a
1950 Business::OnlinePayment realtime gateway.  See
1951 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1952 for supported processors.
1953
1954 =cut
1955
1956 sub realtime_card {
1957   my $self = shift;
1958   $self->realtime_bop( 'CC', @_ );
1959 }
1960
1961 =item realtime_ach
1962
1963 Attempts to pay this invoice with an electronic check (ACH) payment via a
1964 Business::OnlinePayment realtime gateway.  See
1965 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1966 for supported processors.
1967
1968 =cut
1969
1970 sub realtime_ach {
1971   my $self = shift;
1972   $self->realtime_bop( 'ECHECK', @_ );
1973 }
1974
1975 =item realtime_lec
1976
1977 Attempts to pay this invoice with phone bill (LEC) payment via a
1978 Business::OnlinePayment realtime gateway.  See
1979 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1980 for supported processors.
1981
1982 =cut
1983
1984 sub realtime_lec {
1985   my $self = shift;
1986   $self->realtime_bop( 'LEC', @_ );
1987 }
1988
1989 sub realtime_bop {
1990   my( $self, $method ) = (shift,shift);
1991   my %opt = @_;
1992
1993   my $cust_main = $self->cust_main;
1994   my $balance = $cust_main->balance;
1995   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1996   $amount = sprintf("%.2f", $amount);
1997   return "not run (balance $balance)" unless $amount > 0;
1998
1999   my $description = 'Internet Services';
2000   if ( $conf->exists('business-onlinepayment-description') ) {
2001     my $dtempl = $conf->config('business-onlinepayment-description');
2002
2003     my $agent_obj = $cust_main->agent
2004       or die "can't retreive agent for $cust_main (agentnum ".
2005              $cust_main->agentnum. ")";
2006     my $agent = $agent_obj->agent;
2007     my $pkgs = join(', ',
2008       map { $_->part_pkg->pkg }
2009         grep { $_->pkgnum } $self->cust_bill_pkg
2010     );
2011     $description = eval qq("$dtempl");
2012   }
2013
2014   $cust_main->realtime_bop($method, $amount,
2015     'description' => $description,
2016     'invnum'      => $self->invnum,
2017 #this didn't do what we want, it just calls apply_payments_and_credits
2018 #    'apply'       => 1,
2019     'apply_to_invoice' => 1,
2020     %opt,
2021  #what we want:
2022  #this changes application behavior: auto payments
2023                         #triggered against a specific invoice are now applied
2024                         #to that invoice instead of oldest open.
2025                         #seem okay to me...
2026   );
2027
2028 }
2029
2030 =item batch_card OPTION => VALUE...
2031
2032 Adds a payment for this invoice to the pending credit card batch (see
2033 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2034 runs the payment using a realtime gateway.
2035
2036 =cut
2037
2038 sub batch_card {
2039   my ($self, %options) = @_;
2040   my $cust_main = $self->cust_main;
2041
2042   $options{invnum} = $self->invnum;
2043   
2044   $cust_main->batch_card(%options);
2045 }
2046
2047 sub _agent_template {
2048   my $self = shift;
2049   $self->cust_main->agent_template;
2050 }
2051
2052 sub _agent_invoice_from {
2053   my $self = shift;
2054   $self->cust_main->agent_invoice_from;
2055 }
2056
2057 =item print_text HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2058
2059 Returns an text invoice, as a list of lines.
2060
2061 Options can be passed as a hashref (recommended) or as a list of time, template
2062 and then any key/value pairs for any other options.
2063
2064 I<time>, if specified, is used to control the printing of overdue messages.  The
2065 default is now.  It isn't the date of the invoice; that's the `_date' field.
2066 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2067 L<Time::Local> and L<Date::Parse> for conversion functions.
2068
2069 I<template>, if specified, is the name of a suffix for alternate invoices.
2070
2071 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2072
2073 =cut
2074
2075 sub print_text {
2076   my $self = shift;
2077   my( $today, $template, %opt );
2078   if ( ref($_[0]) ) {
2079     %opt = %{ shift() };
2080     $today = delete($opt{'time'}) || '';
2081     $template = delete($opt{template}) || '';
2082   } else {
2083     ( $today, $template, %opt ) = @_;
2084   }
2085
2086   my %params = ( 'format' => 'template' );
2087   $params{'time'} = $today if $today;
2088   $params{'template'} = $template if $template;
2089   $params{$_} = $opt{$_} 
2090     foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2091
2092   $self->print_generic( %params );
2093 }
2094
2095 =item print_latex HASHREF | [ TIME [ , TEMPLATE [ , OPTION => VALUE ... ] ] ]
2096
2097 Internal method - returns a filename of a filled-in LaTeX template for this
2098 invoice (Note: add ".tex" to get the actual filename), and a filename of
2099 an associated logo (with the .eps extension included).
2100
2101 See print_ps and print_pdf for methods that return PostScript and PDF output.
2102
2103 Options can be passed as a hashref (recommended) or as a list of time, template
2104 and then any key/value pairs for any other options.
2105
2106 I<time>, if specified, is used to control the printing of overdue messages.  The
2107 default is now.  It isn't the date of the invoice; that's the `_date' field.
2108 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2109 L<Time::Local> and L<Date::Parse> for conversion functions.
2110
2111 I<template>, if specified, is the name of a suffix for alternate invoices.
2112
2113 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2114
2115 =cut
2116
2117 sub print_latex {
2118   my $self = shift;
2119   my( $today, $template, %opt );
2120   if ( ref($_[0]) ) {
2121     %opt = %{ shift() };
2122     $today = delete($opt{'time'}) || '';
2123     $template = delete($opt{template}) || '';
2124   } else {
2125     ( $today, $template, %opt ) = @_;
2126   }
2127
2128   my %params = ( 'format' => 'latex' );
2129   $params{'time'} = $today if $today;
2130   $params{'template'} = $template if $template;
2131   $params{$_} = $opt{$_} 
2132     foreach grep $opt{$_}, qw( unsquealch_cdr notice_name );
2133
2134   $template ||= $self->_agent_template;
2135
2136   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
2137   my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2138                            DIR      => $dir,
2139                            SUFFIX   => '.eps',
2140                            UNLINK   => 0,
2141                          ) or die "can't open temp file: $!\n";
2142
2143   my $agentnum = $self->cust_main->agentnum;
2144
2145   if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
2146     print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
2147       or die "can't write temp file: $!\n";
2148   } else {
2149     print $lh $conf->config_binary('logo.eps', $agentnum)
2150       or die "can't write temp file: $!\n";
2151   }
2152   close $lh;
2153   $params{'logo_file'} = $lh->filename;
2154
2155   my @filled_in = $self->print_generic( %params );
2156   
2157   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
2158                            DIR      => $dir,
2159                            SUFFIX   => '.tex',
2160                            UNLINK   => 0,
2161                          ) or die "can't open temp file: $!\n";
2162   print $fh join('', @filled_in );
2163   close $fh;
2164
2165   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
2166   return ($1, $params{'logo_file'});
2167
2168 }
2169
2170 =item print_generic OPTION => VALUE ...
2171
2172 Internal method - returns a filled-in template for this invoice as a scalar.
2173
2174 See print_ps and print_pdf for methods that return PostScript and PDF output.
2175
2176 Non optional options include 
2177   format - latex, html, template
2178
2179 Optional options include
2180
2181 template - a value used as a suffix for a configuration template
2182
2183 time - a value used to control the printing of overdue messages.  The
2184 default is now.  It isn't the date of the invoice; that's the `_date' field.
2185 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2186 L<Time::Local> and L<Date::Parse> for conversion functions.
2187
2188 cid - 
2189
2190 unsquelch_cdr - overrides any per customer cdr squelching when true
2191
2192 notice_name - overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
2193
2194 =cut
2195
2196 #what's with all the sprintf('%10.2f')'s in here?  will it cause any
2197 # (alignment in text invoice?) problems to change them all to '%.2f' ?
2198 # yes: fixed width (dot matrix) text printing will be borked
2199 sub print_generic {
2200
2201   my( $self, %params ) = @_;
2202   my $today = $params{today} ? $params{today} : time;
2203   warn "$me print_generic called on $self with suffix $params{template}\n"
2204     if $DEBUG;
2205
2206   my $format = $params{format};
2207   die "Unknown format: $format"
2208     unless $format =~ /^(latex|html|template)$/;
2209
2210   my $cust_main = $self->cust_main;
2211   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
2212     unless $cust_main->payname
2213         && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
2214
2215   my %delimiters = ( 'latex'    => [ '[@--', '--@]' ],
2216                      'html'     => [ '<%=', '%>' ],
2217                      'template' => [ '{', '}' ],
2218                    );
2219
2220   warn "$me print_generic creating template\n"
2221     if $DEBUG > 1;
2222
2223   #create the template
2224   my $template = $params{template} ? $params{template} : $self->_agent_template;
2225   my $templatefile = "invoice_$format";
2226   $templatefile .= "_$template"
2227     if length($template);
2228   my @invoice_template = map "$_\n", $conf->config($templatefile)
2229     or die "cannot load config data $templatefile";
2230
2231   my $old_latex = '';
2232   if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
2233     #change this to a die when the old code is removed
2234     warn "old-style invoice template $templatefile; ".
2235          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
2236     $old_latex = 'true';
2237     @invoice_template = _translate_old_latex_format(@invoice_template);
2238   } 
2239
2240   warn "$me print_generic creating T:T object\n"
2241     if $DEBUG > 1;
2242
2243   my $text_template = new Text::Template(
2244     TYPE => 'ARRAY',
2245     SOURCE => \@invoice_template,
2246     DELIMITERS => $delimiters{$format},
2247   );
2248
2249   warn "$me print_generic compiling T:T object\n"
2250     if $DEBUG > 1;
2251
2252   $text_template->compile()
2253     or die "Can't compile $templatefile: $Text::Template::ERROR\n";
2254
2255
2256   # additional substitution could possibly cause breakage in existing templates
2257   my %convert_maps = ( 
2258     'latex' => {
2259                  'notes'         => sub { map "$_", @_ },
2260                  'footer'        => sub { map "$_", @_ },
2261                  'smallfooter'   => sub { map "$_", @_ },
2262                  'returnaddress' => sub { map "$_", @_ },
2263                  'coupon'        => sub { map "$_", @_ },
2264                  'summary'       => sub { map "$_", @_ },
2265                },
2266     'html'  => {
2267                  'notes' =>
2268                    sub {
2269                      map { 
2270                        s/%%(.*)$/<!-- $1 -->/g;
2271                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
2272                        s/\\begin\{enumerate\}/<ol>/g;
2273                        s/\\item /  <li>/g;
2274                        s/\\end\{enumerate\}/<\/ol>/g;
2275                        s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
2276                        s/\\\\\*/<br>/g;
2277                        s/\\dollar ?/\$/g;
2278                        s/\\#/#/g;
2279                        s/~/&nbsp;/g;
2280                        $_;
2281                      }  @_
2282                    },
2283                  'footer' =>
2284                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2285                  'smallfooter' =>
2286                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
2287                  'returnaddress' =>
2288                    sub {
2289                      map { 
2290                        s/~/&nbsp;/g;
2291                        s/\\\\\*?\s*$/<BR>/;
2292                        s/\\hyphenation\{[\w\s\-]+}//;
2293                        s/\\([&])/$1/g;
2294                        $_;
2295                      }  @_
2296                    },
2297                  'coupon'        => sub { "" },
2298                  'summary'       => sub { "" },
2299                },
2300     'template' => {
2301                  'notes' =>
2302                    sub {
2303                      map { 
2304                        s/%%.*$//g;
2305                        s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
2306                        s/\\begin\{enumerate\}//g;
2307                        s/\\item /  * /g;
2308                        s/\\end\{enumerate\}//g;
2309                        s/\\textbf\{(.*)\}/$1/g;
2310                        s/\\\\\*/ /;
2311                        s/\\dollar ?/\$/g;
2312                        $_;
2313                      }  @_
2314                    },
2315                  'footer' =>
2316                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2317                  'smallfooter' =>
2318                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
2319                  'returnaddress' =>
2320                    sub {
2321                      map { 
2322                        s/~/ /g;
2323                        s/\\\\\*?\s*$/\n/;             # dubious
2324                        s/\\hyphenation\{[\w\s\-]+}//;
2325                        $_;
2326                      }  @_
2327                    },
2328                  'coupon'        => sub { "" },
2329                  'summary'       => sub { "" },
2330                },
2331   );
2332
2333
2334   # hashes for differing output formats
2335   my %nbsps = ( 'latex'    => '~',
2336                 'html'     => '',    # '&nbps;' would be nice
2337                 'template' => '',    # not used
2338               );
2339   my $nbsp = $nbsps{$format};
2340
2341   my %escape_functions = ( 'latex'    => \&_latex_escape,
2342                            'html'     => \&_html_escape_nbsp,#\&encode_entities,
2343                            'template' => sub { shift },
2344                          );
2345   my $escape_function = $escape_functions{$format};
2346   my $escape_function_nonbsp = ($format eq 'html')
2347                                  ? \&_html_escape : $escape_function;
2348
2349   my %date_formats = ( 'latex'    => $date_format_long,
2350                        'html'     => $date_format_long,
2351                        'template' => '%s',
2352                      );
2353   $date_formats{'html'} =~ s/ /&nbsp;/g;
2354
2355   my $date_format = $date_formats{$format};
2356
2357   my %embolden_functions = ( 'latex'    => sub { return '\textbf{'. shift(). '}'
2358                                                },
2359                              'html'     => sub { return '<b>'. shift(). '</b>'
2360                                                },
2361                              'template' => sub { shift },
2362                            );
2363   my $embolden_function = $embolden_functions{$format};
2364
2365   warn "$me generating template variables\n"
2366     if $DEBUG > 1;
2367
2368   # generate template variables
2369   my $returnaddress;
2370   if (
2371          defined( $conf->config_orbase( "invoice_${format}returnaddress",
2372                                         $template
2373                                       )
2374                 )
2375        && length( $conf->config_orbase( "invoice_${format}returnaddress",
2376                                         $template
2377                                       )
2378                 )
2379   ) {
2380
2381     $returnaddress = join("\n",
2382       $conf->config_orbase("invoice_${format}returnaddress", $template)
2383     );
2384
2385   } elsif ( grep /\S/,
2386             $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
2387
2388     my $convert_map = $convert_maps{$format}{'returnaddress'};
2389     $returnaddress =
2390       join( "\n",
2391             &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
2392                                                  $template
2393                                                )
2394                          )
2395           );
2396   } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
2397
2398     my $convert_map = $convert_maps{$format}{'returnaddress'};
2399     $returnaddress = join( "\n", &$convert_map(
2400                                    map { s/( {2,})/'~' x length($1)/eg;
2401                                          s/$/\\\\\*/;
2402                                          $_
2403                                        }
2404                                      ( $conf->config('company_name', $self->cust_main->agentnum),
2405                                        $conf->config('company_address', $self->cust_main->agentnum),
2406                                      )
2407                                  )
2408                      );
2409
2410   } else {
2411
2412     my $warning = "Couldn't find a return address; ".
2413                   "do you need to set the company_address configuration value?";
2414     warn "$warning\n";
2415     $returnaddress = $nbsp;
2416     #$returnaddress = $warning;
2417
2418   }
2419
2420   warn "$me generating invoice data\n"
2421     if $DEBUG > 1;
2422
2423   my $agentnum = $self->cust_main->agentnum;
2424
2425   my %invoice_data = (
2426
2427     #invoice from info
2428     'company_name'    => scalar( $conf->config('company_name', $agentnum) ),
2429     'company_address' => join("\n", $conf->config('company_address', $agentnum) ). "\n",
2430     'returnaddress'   => $returnaddress,
2431     'agent'           => &$escape_function($cust_main->agent->agent),
2432
2433     #invoice info
2434     'invnum'          => $self->invnum,
2435     'date'            => time2str($date_format, $self->_date),
2436     'today'           => time2str($date_format_long, $today),
2437     'terms'           => $self->terms,
2438     'template'        => $template, #params{'template'},
2439     'notice_name'     => ($params{'notice_name'} || 'Invoice'),#escape_function?
2440     'current_charges' => sprintf("%.2f", $self->charged),
2441     'duedate'         => $self->due_date2str($rdate_format), #date_format?
2442
2443     #customer info
2444     'custnum'         => $cust_main->display_custnum,
2445     'agent_custid'    => &$escape_function($cust_main->agent_custid),
2446     ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2447       payname company address1 address2 city state zip fax
2448     )),
2449
2450     #global config
2451     'ship_enable'     => $conf->exists('invoice-ship_address'),
2452     'unitprices'      => $conf->exists('invoice-unitprice'),
2453     'smallernotes'    => $conf->exists('invoice-smallernotes'),
2454     'smallerfooter'   => $conf->exists('invoice-smallerfooter'),
2455     'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2456    
2457     #layout info -- would be fancy to calc some of this and bury the template
2458     #               here in the code
2459     'topmargin'             => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2460     'headsep'               => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2461     'textheight'            => scalar($conf->config('invoice_latextextheight', $agentnum)),
2462     'extracouponspace'      => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2463     'couponfootsep'         => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2464     'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2465     'addresssep'            => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2466     'amountenclosedsep'     => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2467     'coupontoaddresssep'    => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2468     'addcompanytoaddress'   => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2469
2470     # better hang on to conf_dir for a while (for old templates)
2471     'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2472
2473     #these are only used when doing paged plaintext
2474     'page'            => 1,
2475     'total_pages'     => 1,
2476
2477   );
2478
2479   $invoice_data{finance_section} = '';
2480   if ( $conf->config('finance_pkgclass') ) {
2481     my $pkg_class =
2482       qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2483     $invoice_data{finance_section} = $pkg_class->categoryname;
2484   } 
2485   $invoice_data{finance_amount} = '0.00';
2486   $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2487
2488   my $countrydefault = $conf->config('countrydefault') || 'US';
2489   my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2490   foreach ( qw( contact company address1 address2 city state zip country fax) ){
2491     my $method = $prefix.$_;
2492     $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2493   }
2494   $invoice_data{'ship_country'} = ''
2495     if ( $invoice_data{'ship_country'} eq $countrydefault );
2496   
2497   $invoice_data{'cid'} = $params{'cid'}
2498     if $params{'cid'};
2499
2500   if ( $cust_main->country eq $countrydefault ) {
2501     $invoice_data{'country'} = '';
2502   } else {
2503     $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2504   }
2505
2506   my @address = ();
2507   $invoice_data{'address'} = \@address;
2508   push @address,
2509     $cust_main->payname.
2510       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2511         ? " (P.O. #". $cust_main->payinfo. ")"
2512         : ''
2513       )
2514   ;
2515   push @address, $cust_main->company
2516     if $cust_main->company;
2517   push @address, $cust_main->address1;
2518   push @address, $cust_main->address2
2519     if $cust_main->address2;
2520   push @address,
2521     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
2522   push @address, $invoice_data{'country'}
2523     if $invoice_data{'country'};
2524   push @address, ''
2525     while (scalar(@address) < 5);
2526
2527   $invoice_data{'logo_file'} = $params{'logo_file'}
2528     if $params{'logo_file'};
2529
2530   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2531 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2532   #my $balance_due = $self->owed + $pr_total - $cr_total;
2533   my $balance_due = $self->owed + $pr_total;
2534   $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2535   $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2536   $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2537   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2538
2539   my $summarypage = '';
2540   if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2541     $summarypage = 1;
2542   }
2543   $invoice_data{'summarypage'} = $summarypage;
2544
2545   warn "$me substituting variables in notes, footer, smallfooter\n"
2546     if $DEBUG > 1;
2547
2548   foreach my $include (qw( notes footer smallfooter coupon )) {
2549
2550     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2551     my @inc_src;
2552
2553     if ( $conf->exists($inc_file, $agentnum)
2554          && length( $conf->config($inc_file, $agentnum) ) ) {
2555
2556       @inc_src = $conf->config($inc_file, $agentnum);
2557
2558     } else {
2559
2560       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2561
2562       my $convert_map = $convert_maps{$format}{$include};
2563
2564       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2565                        s/--\@\]/$delimiters{$format}[1]/g;
2566                        $_;
2567                      } 
2568                  &$convert_map( $conf->config($inc_file, $agentnum) );
2569
2570     }
2571
2572     my $inc_tt = new Text::Template (
2573       TYPE       => 'ARRAY',
2574       SOURCE     => [ map "$_\n", @inc_src ],
2575       DELIMITERS => $delimiters{$format},
2576     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2577
2578     unless ( $inc_tt->compile() ) {
2579       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2580       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2581       die $error;
2582     }
2583
2584     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2585
2586     $invoice_data{$include} =~ s/\n+$//
2587       if ($format eq 'latex');
2588   }
2589
2590   $invoice_data{'po_line'} =
2591     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2592       ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2593       : $nbsp;
2594
2595   my %money_chars = ( 'latex'    => '',
2596                       'html'     => $conf->config('money_char') || '$',
2597                       'template' => '',
2598                     );
2599   my $money_char = $money_chars{$format};
2600
2601   my %other_money_chars = ( 'latex'    => '\dollar ',#XXX should be a config too
2602                             'html'     => $conf->config('money_char') || '$',
2603                             'template' => '',
2604                           );
2605   my $other_money_char = $other_money_chars{$format};
2606   $invoice_data{'dollar'} = $other_money_char;
2607
2608   my @detail_items = ();
2609   my @total_items = ();
2610   my @buf = ();
2611   my @sections = ();
2612
2613   $invoice_data{'detail_items'} = \@detail_items;
2614   $invoice_data{'total_items'} = \@total_items;
2615   $invoice_data{'buf'} = \@buf;
2616   $invoice_data{'sections'} = \@sections;
2617
2618   warn "$me generating sections\n"
2619     if $DEBUG > 1;
2620
2621   my $previous_section = { 'description' => 'Previous Charges',
2622                            'subtotal'    => $other_money_char.
2623                                             sprintf('%.2f', $pr_total),
2624                            'summarized'  => $summarypage ? 'Y' : '',
2625                          };
2626   $previous_section->{posttotal} = '0 / 30 / 60/ 90 days overdue '. 
2627     join(' / ', map { $cust_main->balance_date_range(@$_) }
2628                 $self->_prior_month30s
2629         )
2630     if $conf->exists('invoice_include_aging');
2631
2632   my $taxtotal = 0;
2633   my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2634                       'subtotal'    => $taxtotal,   # adjusted below
2635                       'summarized'  => $summarypage ? 'Y' : '',
2636                     };
2637   my $tax_weight = _pkg_category($tax_section->{description})
2638                         ? _pkg_category($tax_section->{description})->weight
2639                         : 0;
2640   $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2641   $tax_section->{'sort_weight'} = $tax_weight;
2642
2643
2644   my $adjusttotal = 0;
2645   my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2646                          'subtotal'    => 0,   # adjusted below
2647                          'summarized'  => $summarypage ? 'Y' : '',
2648                        };
2649   my $adjust_weight = _pkg_category($adjust_section->{description})
2650                         ? _pkg_category($adjust_section->{description})->weight
2651                         : 0;
2652   $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2653   $adjust_section->{'sort_weight'} = $adjust_weight;
2654
2655   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2656   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2657   $invoice_data{'multisection'} = $multisection;
2658   my $late_sections = [];
2659   my $extra_sections = [];
2660   my $extra_lines = ();
2661   if ( $multisection ) {
2662     ($extra_sections, $extra_lines) =
2663       $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2664       if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2665
2666     push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2667
2668     push @detail_items, @$extra_lines if $extra_lines;
2669     push @sections,
2670       $self->_items_sections( $late_sections,      # this could stand a refactor
2671                               $summarypage,
2672                               $escape_function_nonbsp,
2673                               $extra_sections,
2674                               $format,             #bah
2675                             );
2676     if ($conf->exists('svc_phone_sections')) {
2677       my ($phone_sections, $phone_lines) =
2678         $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2679       push @{$late_sections}, @$phone_sections;
2680       push @detail_items, @$phone_lines;
2681     }
2682   }else{
2683     push @sections, { 'description' => '', 'subtotal' => '' };
2684   }
2685
2686   unless (    $conf->exists('disable_previous_balance')
2687            || $conf->exists('previous_balance-summary_only')
2688          )
2689   {
2690
2691     warn "$me adding previous balances\n"
2692       if $DEBUG > 1;
2693
2694     foreach my $line_item ( $self->_items_previous ) {
2695
2696       my $detail = {
2697         ext_description => [],
2698       };
2699       $detail->{'ref'} = $line_item->{'pkgnum'};
2700       $detail->{'quantity'} = 1;
2701       $detail->{'section'} = $previous_section;
2702       $detail->{'description'} = &$escape_function($line_item->{'description'});
2703       if ( exists $line_item->{'ext_description'} ) {
2704         @{$detail->{'ext_description'}} = map {
2705           &$escape_function($_);
2706         } @{$line_item->{'ext_description'}};
2707       }
2708       $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2709                             $line_item->{'amount'};
2710       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2711
2712       push @detail_items, $detail;
2713       push @buf, [ $detail->{'description'},
2714                    $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2715                  ];
2716     }
2717
2718   }
2719
2720   if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2721     push @buf, ['','-----------'];
2722     push @buf, [ 'Total Previous Balance',
2723                  $money_char. sprintf("%10.2f", $pr_total) ];
2724     push @buf, ['',''];
2725   }
2726  
2727   if ( $conf->exists('svc_phone-did-summary') ) {
2728       warn "$me adding DID summary\n"
2729         if $DEBUG > 1;
2730
2731       my ($didsummary,$minutes) = $self->_did_summary;
2732       my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
2733       push @detail_items, 
2734         { 'description' => $didsummary_desc,
2735             'ext_description' => [ $didsummary, $minutes ],
2736         }
2737         if !$multisection;
2738   }
2739
2740   foreach my $section (@sections, @$late_sections) {
2741
2742     warn "$me adding $section section\n"
2743       if $DEBUG > 1;
2744
2745     # begin some normalization
2746     $section->{'subtotal'} = $section->{'amount'}
2747       if $multisection
2748          && !exists($section->{subtotal})
2749          && exists($section->{amount});
2750
2751     $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2752       if ( $invoice_data{finance_section} &&
2753            $section->{'description'} eq $invoice_data{finance_section} );
2754
2755     $section->{'subtotal'} = $other_money_char.
2756                              sprintf('%.2f', $section->{'subtotal'})
2757       if $multisection;
2758
2759     # continue some normalization
2760     $section->{'amount'}   = $section->{'subtotal'}
2761       if $multisection;
2762
2763
2764     if ( $section->{'description'} ) {
2765       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2766                    [ '', '' ],
2767                  );
2768     }
2769
2770     my $multilocation = scalar($cust_main->cust_location); #too expensive?
2771     my %options = ();
2772     $options{'section'} = $section if $multisection;
2773     $options{'format'} = $format;
2774     $options{'escape_function'} = $escape_function;
2775     $options{'format_function'} = sub { () } unless $unsquelched;
2776     $options{'unsquelched'} = $unsquelched;
2777     $options{'summary_page'} = $summarypage;
2778     $options{'skip_usage'} =
2779       scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2780     $options{'multilocation'} = $multilocation;
2781     $options{'multisection'} = $multisection;
2782
2783     foreach my $line_item ( $self->_items_pkg(%options) ) {
2784       my $detail = {
2785         ext_description => [],
2786       };
2787       $detail->{'ref'} = $line_item->{'pkgnum'};
2788       $detail->{'quantity'} = $line_item->{'quantity'};
2789       $detail->{'section'} = $section;
2790       $detail->{'description'} = &$escape_function($line_item->{'description'});
2791       if ( exists $line_item->{'ext_description'} ) {
2792         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2793       }
2794       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2795                               $line_item->{'amount'};
2796       $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2797                                  $line_item->{'unit_amount'};
2798       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2799   
2800       push @detail_items, $detail;
2801       push @buf, ( [ $detail->{'description'},
2802                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2803                    ],
2804                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2805                  );
2806     }
2807
2808     if ( $section->{'description'} ) {
2809       push @buf, ( ['','-----------'],
2810                    [ $section->{'description'}. ' sub-total',
2811                       $money_char. sprintf("%10.2f", $section->{'subtotal'})
2812                    ],
2813                    [ '', '' ],
2814                    [ '', '' ],
2815                  );
2816     }
2817   
2818   }
2819   
2820   $invoice_data{current_less_finance} =
2821     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2822
2823   if ( $multisection && !$conf->exists('disable_previous_balance')
2824     || $conf->exists('previous_balance-summary_only') )
2825   {
2826     unshift @sections, $previous_section if $pr_total;
2827   }
2828
2829   warn "$me adding taxes\n"
2830     if $DEBUG > 1;
2831
2832   foreach my $tax ( $self->_items_tax ) {
2833
2834     $taxtotal += $tax->{'amount'};
2835
2836     my $description = &$escape_function( $tax->{'description'} );
2837     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
2838
2839     if ( $multisection ) {
2840
2841       my $money = $old_latex ? '' : $money_char;
2842       push @detail_items, {
2843         ext_description => [],
2844         ref          => '',
2845         quantity     => '',
2846         description  => $description,
2847         amount       => $money. $amount,
2848         product_code => '',
2849         section      => $tax_section,
2850       };
2851
2852     } else {
2853
2854       push @total_items, {
2855         'total_item'   => $description,
2856         'total_amount' => $other_money_char. $amount,
2857       };
2858
2859     }
2860
2861     push @buf,[ $description,
2862                 $money_char. $amount,
2863               ];
2864
2865   }
2866   
2867   if ( $taxtotal ) {
2868     my $total = {};
2869     $total->{'total_item'} = 'Sub-total';
2870     $total->{'total_amount'} =
2871       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2872
2873     if ( $multisection ) {
2874       $tax_section->{'subtotal'} = $other_money_char.
2875                                    sprintf('%.2f', $taxtotal);
2876       $tax_section->{'pretotal'} = 'New charges sub-total '.
2877                                    $total->{'total_amount'};
2878       push @sections, $tax_section if $taxtotal;
2879     }else{
2880       unshift @total_items, $total;
2881     }
2882   }
2883   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2884
2885   push @buf,['','-----------'];
2886   push @buf,[( $conf->exists('disable_previous_balance') 
2887                ? 'Total Charges'
2888                : 'Total New Charges'
2889              ),
2890              $money_char. sprintf("%10.2f",$self->charged) ];
2891   push @buf,['',''];
2892
2893   {
2894     my $total = {};
2895     my $item = 'Total';
2896     $item = $conf->config('previous_balance-exclude_from_total')
2897          || 'Total New Charges'
2898       if $conf->exists('previous_balance-exclude_from_total');
2899     my $amount = $self->charged +
2900                    ( $conf->exists('disable_previous_balance') ||
2901                      $conf->exists('previous_balance-exclude_from_total')
2902                      ? 0
2903                      : $pr_total
2904                    );
2905     $total->{'total_item'} = &$embolden_function($item);
2906     $total->{'total_amount'} =
2907       &$embolden_function( $other_money_char.  sprintf( '%.2f', $amount ) );
2908     if ( $multisection ) {
2909       if ( $adjust_section->{'sort_weight'} ) {
2910         $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
2911           sprintf("%.2f", ($self->billing_balance || 0) );
2912       } else {
2913         $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2914                                         sprintf('%.2f', $self->charged );
2915       } 
2916     }else{
2917       push @total_items, $total;
2918     }
2919     push @buf,['','-----------'];
2920     push @buf,[$item,
2921                $money_char.
2922                sprintf( '%10.2f', $amount )
2923               ];
2924     push @buf,['',''];
2925   }
2926   
2927   unless ( $conf->exists('disable_previous_balance') ) {
2928     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2929   
2930     # credits
2931     my $credittotal = 0;
2932     foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2933
2934       my $total;
2935       $total->{'total_item'} = &$escape_function($credit->{'description'});
2936       $credittotal += $credit->{'amount'};
2937       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2938       $adjusttotal += $credit->{'amount'};
2939       if ( $multisection ) {
2940         my $money = $old_latex ? '' : $money_char;
2941         push @detail_items, {
2942           ext_description => [],
2943           ref          => '',
2944           quantity     => '',
2945           description  => &$escape_function($credit->{'description'}),
2946           amount       => $money. $credit->{'amount'},
2947           product_code => '',
2948           section      => $adjust_section,
2949         };
2950       } else {
2951         push @total_items, $total;
2952       }
2953
2954     }
2955     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2956
2957     #credits (again)
2958     foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2959       push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2960     }
2961
2962     # payments
2963     my $paymenttotal = 0;
2964     foreach my $payment ( $self->_items_payments ) {
2965       my $total = {};
2966       $total->{'total_item'} = &$escape_function($payment->{'description'});
2967       $paymenttotal += $payment->{'amount'};
2968       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2969       $adjusttotal += $payment->{'amount'};
2970       if ( $multisection ) {
2971         my $money = $old_latex ? '' : $money_char;
2972         push @detail_items, {
2973           ext_description => [],
2974           ref          => '',
2975           quantity     => '',
2976           description  => &$escape_function($payment->{'description'}),
2977           amount       => $money. $payment->{'amount'},
2978           product_code => '',
2979           section      => $adjust_section,
2980         };
2981       }else{
2982         push @total_items, $total;
2983       }
2984       push @buf, [ $payment->{'description'},
2985                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
2986                  ];
2987     }
2988     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2989   
2990     if ( $multisection ) {
2991       $adjust_section->{'subtotal'} = $other_money_char.
2992                                       sprintf('%.2f', $adjusttotal);
2993       push @sections, $adjust_section
2994         unless $adjust_section->{sort_weight};
2995     }
2996
2997     { 
2998       my $total;
2999       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3000       $total->{'total_amount'} =
3001         &$embolden_function(
3002           $other_money_char. sprintf('%.2f', $summarypage 
3003                                                ? $self->charged +
3004                                                  $self->billing_balance
3005                                                : $self->owed + $pr_total
3006                                     )
3007         );
3008       if ( $multisection && !$adjust_section->{sort_weight} ) {
3009         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3010                                          $total->{'total_amount'};
3011       }else{
3012         push @total_items, $total;
3013       }
3014       push @buf,['','-----------'];
3015       push @buf,[$self->balance_due_msg, $money_char. 
3016         sprintf("%10.2f", $balance_due ) ];
3017     }
3018   }
3019
3020   if ( $multisection ) {
3021     if ($conf->exists('svc_phone_sections')) {
3022       my $total;
3023       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3024       $total->{'total_amount'} =
3025         &$embolden_function(
3026           $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3027         );
3028       my $last_section = pop @sections;
3029       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3030                                      $total->{'total_amount'};
3031       push @sections, $last_section;
3032     }
3033     push @sections, @$late_sections
3034       if $unsquelched;
3035   }
3036
3037   my @includelist = ();
3038   push @includelist, 'summary' if $summarypage;
3039   foreach my $include ( @includelist ) {
3040
3041     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3042     my @inc_src;
3043
3044     if ( length( $conf->config($inc_file, $agentnum) ) ) {
3045
3046       @inc_src = $conf->config($inc_file, $agentnum);
3047
3048     } else {
3049
3050       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3051
3052       my $convert_map = $convert_maps{$format}{$include};
3053
3054       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3055                        s/--\@\]/$delimiters{$format}[1]/g;
3056                        $_;
3057                      } 
3058                  &$convert_map( $conf->config($inc_file, $agentnum) );
3059
3060     }
3061
3062     my $inc_tt = new Text::Template (
3063       TYPE       => 'ARRAY',
3064       SOURCE     => [ map "$_\n", @inc_src ],
3065       DELIMITERS => $delimiters{$format},
3066     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3067
3068     unless ( $inc_tt->compile() ) {
3069       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3070       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3071       die $error;
3072     }
3073
3074     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3075
3076     $invoice_data{$include} =~ s/\n+$//
3077       if ($format eq 'latex');
3078   }
3079
3080   $invoice_lines = 0;
3081   my $wasfunc = 0;
3082   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3083     /invoice_lines\((\d*)\)/;
3084     $invoice_lines += $1 || scalar(@buf);
3085     $wasfunc=1;
3086   }
3087   die "no invoice_lines() functions in template?"
3088     if ( $format eq 'template' && !$wasfunc );
3089
3090   if ($format eq 'template') {
3091
3092     if ( $invoice_lines ) {
3093       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3094       $invoice_data{'total_pages'}++
3095         if scalar(@buf) % $invoice_lines;
3096     }
3097
3098     #setup subroutine for the template
3099     sub FS::cust_bill::_template::invoice_lines {
3100       my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3101       map { 
3102         scalar(@FS::cust_bill::_template::buf)
3103           ? shift @FS::cust_bill::_template::buf
3104           : [ '', '' ];
3105       }
3106       ( 1 .. $lines );
3107     }
3108
3109     my $lines;
3110     my @collect;
3111     while (@buf) {
3112       push @collect, split("\n",
3113         $text_template->fill_in( HASH => \%invoice_data,
3114                                  PACKAGE => 'FS::cust_bill::_template'
3115                                )
3116       );
3117       $FS::cust_bill::_template::page++;
3118     }
3119     map "$_\n", @collect;
3120   }else{
3121     warn "filling in template for invoice ". $self->invnum. "\n"
3122       if $DEBUG;
3123     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3124       if $DEBUG > 1;
3125
3126     $text_template->fill_in(HASH => \%invoice_data);
3127   }
3128 }
3129
3130 # helper routine for generating date ranges
3131 sub _prior_month30s {
3132   my $self = shift;
3133   my @ranges = (
3134    [ 1,       2592000 ], # 0-30 days ago
3135    [ 2592000, 5184000 ], # 30-60 days ago
3136    [ 5184000, 7776000 ], # 60-90 days ago
3137    [ 7776000, 0       ], # 90+   days ago
3138   );
3139
3140   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3141           $_->[1] ? $self->_date - $_->[1] - 1 : '',
3142       ] }
3143   @ranges;
3144 }
3145
3146 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3147
3148 Returns an postscript invoice, as a scalar.
3149
3150 Options can be passed as a hashref (recommended) or as a list of time, template
3151 and then any key/value pairs for any other options.
3152
3153 I<time> an optional value used to control the printing of overdue messages.  The
3154 default is now.  It isn't the date of the invoice; that's the `_date' field.
3155 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3156 L<Time::Local> and L<Date::Parse> for conversion functions.
3157
3158 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3159
3160 =cut
3161
3162 sub print_ps {
3163   my $self = shift;
3164
3165   my ($file, $lfile) = $self->print_latex(@_);
3166   my $ps = generate_ps($file);
3167   unlink($lfile);
3168
3169   $ps;
3170 }
3171
3172 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3173
3174 Returns an PDF invoice, as a scalar.
3175
3176 Options can be passed as a hashref (recommended) or as a list of time, template
3177 and then any key/value pairs for any other options.
3178
3179 I<time> an optional value used to control the printing of overdue messages.  The
3180 default is now.  It isn't the date of the invoice; that's the `_date' field.
3181 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3182 L<Time::Local> and L<Date::Parse> for conversion functions.
3183
3184 I<template>, if specified, is the name of a suffix for alternate invoices.
3185
3186 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3187
3188 =cut
3189
3190 sub print_pdf {
3191   my $self = shift;
3192
3193   my ($file, $lfile) = $self->print_latex(@_);
3194   my $pdf = generate_pdf($file);
3195   unlink($lfile);
3196
3197   $pdf;
3198 }
3199
3200 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3201
3202 Returns an HTML invoice, as a scalar.
3203
3204 I<time> an optional value used to control the printing of overdue messages.  The
3205 default is now.  It isn't the date of the invoice; that's the `_date' field.
3206 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3207 L<Time::Local> and L<Date::Parse> for conversion functions.
3208
3209 I<template>, if specified, is the name of a suffix for alternate invoices.
3210
3211 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3212
3213 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3214 when emailing the invoice as part of a multipart/related MIME email.
3215
3216 =cut
3217
3218 sub print_html {
3219   my $self = shift;
3220   my %params;
3221   if ( ref($_[0]) ) {
3222     %params = %{ shift() }; 
3223   }else{
3224     $params{'time'} = shift;
3225     $params{'template'} = shift;
3226     $params{'cid'} = shift;
3227   }
3228
3229   $params{'format'} = 'html';
3230
3231   $self->print_generic( %params );
3232 }
3233
3234 # quick subroutine for print_latex
3235 #
3236 # There are ten characters that LaTeX treats as special characters, which
3237 # means that they do not simply typeset themselves: 
3238 #      # $ % & ~ _ ^ \ { }
3239 #
3240 # TeX ignores blanks following an escaped character; if you want a blank (as
3241 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
3242
3243 sub _latex_escape {
3244   my $value = shift;
3245   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3246   $value =~ s/([<>])/\$$1\$/g;
3247   $value;
3248 }
3249
3250 sub _html_escape {
3251   my $value = shift;
3252   encode_entities($value);
3253   $value;
3254 }
3255
3256 sub _html_escape_nbsp {
3257   my $value = _html_escape(shift);
3258   $value =~ s/ +/&nbsp;/g;
3259   $value;
3260 }
3261
3262 #utility methods for print_*
3263
3264 sub _translate_old_latex_format {
3265   warn "_translate_old_latex_format called\n"
3266     if $DEBUG; 
3267
3268   my @template = ();
3269   while ( @_ ) {
3270     my $line = shift;
3271   
3272     if ( $line =~ /^%%Detail\s*$/ ) {
3273   
3274       push @template, q![@--!,
3275                       q!  foreach my $_tr_line (@detail_items) {!,
3276                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3277                       q!      $_tr_line->{'description'} .= !, 
3278                       q!        "\\tabularnewline\n~~".!,
3279                       q!        join( "\\tabularnewline\n~~",!,
3280                       q!          @{$_tr_line->{'ext_description'}}!,
3281                       q!        );!,
3282                       q!    }!;
3283
3284       while ( ( my $line_item_line = shift )
3285               !~ /^%%EndDetail\s*$/                            ) {
3286         $line_item_line =~ s/'/\\'/g;    # nice LTS
3287         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3288         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3289         push @template, "    \$OUT .= '$line_item_line';";
3290       }
3291
3292       push @template, '}',
3293                       '--@]';
3294       #' doh, gvim
3295     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3296
3297       push @template, '[@--',
3298                       '  foreach my $_tr_line (@total_items) {';
3299
3300       while ( ( my $total_item_line = shift )
3301               !~ /^%%EndTotalDetails\s*$/                      ) {
3302         $total_item_line =~ s/'/\\'/g;    # nice LTS
3303         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3304         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3305         push @template, "    \$OUT .= '$total_item_line';";
3306       }
3307
3308       push @template, '}',
3309                       '--@]';
3310
3311     } else {
3312       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3313       push @template, $line;  
3314     }
3315   
3316   }
3317
3318   if ($DEBUG) {
3319     warn "$_\n" foreach @template;
3320   }
3321
3322   (@template);
3323 }
3324
3325 sub terms {
3326   my $self = shift;
3327
3328   #check for an invoice-specific override
3329   return $self->invoice_terms if $self->invoice_terms;
3330   
3331   #check for a customer- specific override
3332   my $cust_main = $self->cust_main;
3333   return $cust_main->invoice_terms if $cust_main->invoice_terms;
3334
3335   #use configured default
3336   $conf->config('invoice_default_terms') || '';
3337 }
3338
3339 sub due_date {
3340   my $self = shift;
3341   my $duedate = '';
3342   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3343     $duedate = $self->_date() + ( $1 * 86400 );
3344   }
3345   $duedate;
3346 }
3347
3348 sub due_date2str {
3349   my $self = shift;
3350   $self->due_date ? time2str(shift, $self->due_date) : '';
3351 }
3352
3353 sub balance_due_msg {
3354   my $self = shift;
3355   my $msg = 'Balance Due';
3356   return $msg unless $self->terms;
3357   if ( $self->due_date ) {
3358     $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3359   } elsif ( $self->terms ) {
3360     $msg .= ' - '. $self->terms;
3361   }
3362   $msg;
3363 }
3364
3365 sub balance_due_date {
3366   my $self = shift;
3367   my $duedate = '';
3368   if (    $conf->exists('invoice_default_terms') 
3369        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3370     $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3371   }
3372   $duedate;
3373 }
3374
3375 =item invnum_date_pretty
3376
3377 Returns a string with the invoice number and date, for example:
3378 "Invoice #54 (3/20/2008)"
3379
3380 =cut
3381
3382 sub invnum_date_pretty {
3383   my $self = shift;
3384   'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3385 }
3386
3387 =item _date_pretty
3388
3389 Returns a string with the date, for example: "3/20/2008"
3390
3391 =cut
3392
3393 sub _date_pretty {
3394   my $self = shift;
3395   time2str($date_format, $self->_date);
3396 }
3397
3398 use vars qw(%pkg_category_cache);
3399 sub _items_sections {
3400   my $self = shift;
3401   my $late = shift;
3402   my $summarypage = shift;
3403   my $escape = shift;
3404   my $extra_sections = shift;
3405   my $format = shift;
3406
3407   my %subtotal = ();
3408   my %late_subtotal = ();
3409   my %not_tax = ();
3410
3411   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3412   {
3413
3414       my $usage = $cust_bill_pkg->usage;
3415
3416       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3417         next if ( $display->summary && $summarypage );
3418
3419         my $section = $display->section;
3420         my $type    = $display->type;
3421
3422         $not_tax{$section} = 1
3423           unless $cust_bill_pkg->pkgnum == 0;
3424
3425         if ( $display->post_total && !$summarypage ) {
3426           if (! $type || $type eq 'S') {
3427             $late_subtotal{$section} += $cust_bill_pkg->setup
3428               if $cust_bill_pkg->setup != 0;
3429           }
3430
3431           if (! $type) {
3432             $late_subtotal{$section} += $cust_bill_pkg->recur
3433               if $cust_bill_pkg->recur != 0;
3434           }
3435
3436           if ($type && $type eq 'R') {
3437             $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3438               if $cust_bill_pkg->recur != 0;
3439           }
3440           
3441           if ($type && $type eq 'U') {
3442             $late_subtotal{$section} += $usage
3443               unless scalar(@$extra_sections);
3444           }
3445
3446         } else {
3447
3448           next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3449
3450           if (! $type || $type eq 'S') {
3451             $subtotal{$section} += $cust_bill_pkg->setup
3452               if $cust_bill_pkg->setup != 0;
3453           }
3454
3455           if (! $type) {
3456             $subtotal{$section} += $cust_bill_pkg->recur
3457               if $cust_bill_pkg->recur != 0;
3458           }
3459
3460           if ($type && $type eq 'R') {
3461             $subtotal{$section} += $cust_bill_pkg->recur - $usage
3462               if $cust_bill_pkg->recur != 0;
3463           }
3464           
3465           if ($type && $type eq 'U') {
3466             $subtotal{$section} += $usage
3467               unless scalar(@$extra_sections);
3468           }
3469
3470         }
3471
3472       }
3473
3474   }
3475
3476   %pkg_category_cache = ();
3477
3478   push @$late, map { { 'description' => &{$escape}($_),
3479                        'subtotal'    => $late_subtotal{$_},
3480                        'post_total'  => 1,
3481                        'sort_weight' => ( _pkg_category($_)
3482                                             ? _pkg_category($_)->weight
3483                                             : 0
3484                                        ),
3485                        ((_pkg_category($_) && _pkg_category($_)->condense)
3486                                            ? $self->_condense_section($format)
3487                                            : ()
3488                        ),
3489                    } }
3490                  sort _sectionsort keys %late_subtotal;
3491
3492   my @sections;
3493   if ( $summarypage ) {
3494     @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3495                 map { $_->categoryname } qsearch('pkg_category', {});
3496     push @sections, '' if exists($subtotal{''});
3497   } else {
3498     @sections = keys %subtotal;
3499   }
3500
3501   my @early = map { { 'description' => &{$escape}($_),
3502                       'subtotal'    => $subtotal{$_},
3503                       'summarized'  => $not_tax{$_} ? '' : 'Y',
3504                       'tax_section' => $not_tax{$_} ? '' : 'Y',
3505                       'sort_weight' => ( _pkg_category($_)
3506                                            ? _pkg_category($_)->weight
3507                                            : 0
3508                                        ),
3509                        ((_pkg_category($_) && _pkg_category($_)->condense)
3510                                            ? $self->_condense_section($format)
3511                                            : ()
3512                        ),
3513                     }
3514                   } @sections;
3515   push @early, @$extra_sections if $extra_sections;
3516  
3517   sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3518
3519 }
3520
3521 #helper subs for above
3522
3523 sub _sectionsort {
3524   _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3525 }
3526
3527 sub _pkg_category {
3528   my $categoryname = shift;
3529   $pkg_category_cache{$categoryname} ||=
3530     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3531 }
3532
3533 my %condensed_format = (
3534   'label' => [ qw( Description Qty Amount ) ],
3535   'fields' => [
3536                 sub { shift->{description} },
3537                 sub { shift->{quantity} },
3538                 sub { my($href, %opt) = @_;
3539                       ($opt{dollar} || ''). $href->{amount};
3540                     },
3541               ],
3542   'align'  => [ qw( l r r ) ],
3543   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
3544   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
3545 );
3546
3547 sub _condense_section {
3548   my ( $self, $format ) = ( shift, shift );
3549   ( 'condensed' => 1,
3550     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3551       qw( description_generator
3552           header_generator
3553           total_generator
3554           total_line_generator
3555         )
3556   );
3557 }
3558
3559 sub _condensed_generator_defaults {
3560   my ( $self, $format ) = ( shift, shift );
3561   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3562 }
3563
3564 my %html_align = (
3565   'c' => 'center',
3566   'l' => 'left',
3567   'r' => 'right',
3568 );
3569
3570 sub _condensed_header_generator {
3571   my ( $self, $format ) = ( shift, shift );
3572
3573   my ( $f, $prefix, $suffix, $separator, $column ) =
3574     _condensed_generator_defaults($format);
3575
3576   if ($format eq 'latex') {
3577     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3578     $suffix = "\\\\\n\\hline";
3579     $separator = "&\n";
3580     $column =
3581       sub { my ($d,$a,$s,$w) = @_;
3582             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3583           };
3584   } elsif ( $format eq 'html' ) {
3585     $prefix = '<th></th>';
3586     $suffix = '';
3587     $separator = '';
3588     $column =
3589       sub { my ($d,$a,$s,$w) = @_;
3590             return qq!<th align="$html_align{$a}">$d</th>!;
3591       };
3592   }
3593
3594   sub {
3595     my @args = @_;
3596     my @result = ();
3597
3598     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3599       push @result,
3600         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3601     }
3602
3603     $prefix. join($separator, @result). $suffix;
3604   };
3605
3606 }
3607
3608 sub _condensed_description_generator {
3609   my ( $self, $format ) = ( shift, shift );
3610
3611   my ( $f, $prefix, $suffix, $separator, $column ) =
3612     _condensed_generator_defaults($format);
3613
3614   my $money_char = '$';
3615   if ($format eq 'latex') {
3616     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3617     $suffix = '\\\\';
3618     $separator = " & \n";
3619     $column =
3620       sub { my ($d,$a,$s,$w) = @_;
3621             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3622           };
3623     $money_char = '\\dollar';
3624   }elsif ( $format eq 'html' ) {
3625     $prefix = '"><td align="center"></td>';
3626     $suffix = '';
3627     $separator = '';
3628     $column =
3629       sub { my ($d,$a,$s,$w) = @_;
3630             return qq!<td align="$html_align{$a}">$d</td>!;
3631       };
3632     #$money_char = $conf->config('money_char') || '$';
3633     $money_char = '';  # this is madness
3634   }
3635
3636   sub {
3637     #my @args = @_;
3638     my $href = shift;
3639     my @result = ();
3640
3641     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3642       my $dollar = '';
3643       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3644       push @result,
3645         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3646                     map { $f->{$_}->[$i] } qw(align span width)
3647                   );
3648     }
3649
3650     $prefix. join( $separator, @result ). $suffix;
3651   };
3652
3653 }
3654
3655 sub _condensed_total_generator {
3656   my ( $self, $format ) = ( shift, shift );
3657
3658   my ( $f, $prefix, $suffix, $separator, $column ) =
3659     _condensed_generator_defaults($format);
3660   my $style = '';
3661
3662   if ($format eq 'latex') {
3663     $prefix = "& ";
3664     $suffix = "\\\\\n";
3665     $separator = " & \n";
3666     $column =
3667       sub { my ($d,$a,$s,$w) = @_;
3668             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3669           };
3670   }elsif ( $format eq 'html' ) {
3671     $prefix = '';
3672     $suffix = '';
3673     $separator = '';
3674     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3675     $column =
3676       sub { my ($d,$a,$s,$w) = @_;
3677             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3678       };
3679   }
3680
3681
3682   sub {
3683     my @args = @_;
3684     my @result = ();
3685
3686     #  my $r = &{$f->{fields}->[$i]}(@args);
3687     #  $r .= ' Total' unless $i;
3688
3689     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3690       push @result,
3691         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3692                     map { $f->{$_}->[$i] } qw(align span width)
3693                   );
3694     }
3695
3696     $prefix. join( $separator, @result ). $suffix;
3697   };
3698
3699 }
3700
3701 =item total_line_generator FORMAT
3702
3703 Returns a coderef used for generation of invoice total line items for this
3704 usage_class.  FORMAT is either html or latex
3705
3706 =cut
3707
3708 # should not be used: will have issues with hash element names (description vs
3709 # total_item and amount vs total_amount -- another array of functions?
3710
3711 sub _condensed_total_line_generator {
3712   my ( $self, $format ) = ( shift, shift );
3713
3714   my ( $f, $prefix, $suffix, $separator, $column ) =
3715     _condensed_generator_defaults($format);
3716   my $style = '';
3717
3718   if ($format eq 'latex') {
3719     $prefix = "& ";
3720     $suffix = "\\\\\n";
3721     $separator = " & \n";
3722     $column =
3723       sub { my ($d,$a,$s,$w) = @_;
3724             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3725           };
3726   }elsif ( $format eq 'html' ) {
3727     $prefix = '';
3728     $suffix = '';
3729     $separator = '';
3730     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3731     $column =
3732       sub { my ($d,$a,$s,$w) = @_;
3733             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3734       };
3735   }
3736
3737
3738   sub {
3739     my @args = @_;
3740     my @result = ();
3741
3742     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3743       push @result,
3744         &{$column}( &{$f->{fields}->[$i]}(@args),
3745                     map { $f->{$_}->[$i] } qw(align span width)
3746                   );
3747     }
3748
3749     $prefix. join( $separator, @result ). $suffix;
3750   };
3751
3752 }
3753
3754 #sub _items_extra_usage_sections {
3755 #  my $self = shift;
3756 #  my $escape = shift;
3757 #
3758 #  my %sections = ();
3759 #
3760 #  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
3761 #  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3762 #  {
3763 #    next unless $cust_bill_pkg->pkgnum > 0;
3764 #
3765 #    foreach my $section ( keys %usage_class ) {
3766 #
3767 #      my $usage = $cust_bill_pkg->usage($section);
3768 #
3769 #      next unless $usage && $usage > 0;
3770 #
3771 #      $sections{$section} ||= 0;
3772 #      $sections{$section} += $usage;
3773 #
3774 #    }
3775 #
3776 #  }
3777 #
3778 #  map { { 'description' => &{$escape}($_),
3779 #          'subtotal'    => $sections{$_},
3780 #          'summarized'  => '',
3781 #          'tax_section' => '',
3782 #        }
3783 #      }
3784 #    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3785 #
3786 #}
3787
3788 sub _items_extra_usage_sections {
3789   my $self = shift;
3790   my $escape = shift;
3791   my $format = shift;
3792
3793   my %sections = ();
3794   my %classnums = ();
3795   my %lines = ();
3796
3797   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3798   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3799     next unless $cust_bill_pkg->pkgnum > 0;
3800
3801     foreach my $classnum ( keys %usage_class ) {
3802       my $section = $usage_class{$classnum}->classname;
3803       $classnums{$section} = $classnum;
3804
3805       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3806         my $amount = $detail->amount;
3807         next unless $amount && $amount > 0;
3808  
3809         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3810         $sections{$section}{amount} += $amount;  #subtotal
3811         $sections{$section}{calls}++;
3812         $sections{$section}{duration} += $detail->duration;
3813
3814         my $desc = $detail->regionname; 
3815         my $description = $desc;
3816         $description = substr($desc, 0, 50). '...'
3817           if $format eq 'latex' && length($desc) > 50;
3818
3819         $lines{$section}{$desc} ||= {
3820           description     => &{$escape}($description),
3821           #pkgpart         => $part_pkg->pkgpart,
3822           pkgnum          => $cust_bill_pkg->pkgnum,
3823           ref             => '',
3824           amount          => 0,
3825           calls           => 0,
3826           duration        => 0,
3827           #unit_amount     => $cust_bill_pkg->unitrecur,
3828           quantity        => $cust_bill_pkg->quantity,
3829           product_code    => 'N/A',
3830           ext_description => [],
3831         };
3832
3833         $lines{$section}{$desc}{amount} += $amount;
3834         $lines{$section}{$desc}{calls}++;
3835         $lines{$section}{$desc}{duration} += $detail->duration;
3836
3837       }
3838     }
3839   }
3840
3841   my %sectionmap = ();
3842   foreach (keys %sections) {
3843     my $usage_class = $usage_class{$classnums{$_}};
3844     $sectionmap{$_} = { 'description' => &{$escape}($_),
3845                         'amount'    => $sections{$_}{amount},    #subtotal
3846                         'calls'       => $sections{$_}{calls},
3847                         'duration'    => $sections{$_}{duration},
3848                         'summarized'  => '',
3849                         'tax_section' => '',
3850                         'sort_weight' => $usage_class->weight,
3851                         ( $usage_class->format
3852                           ? ( map { $_ => $usage_class->$_($format) }
3853                               qw( description_generator header_generator total_generator total_line_generator )
3854                             )
3855                           : ()
3856                         ), 
3857                       };
3858   }
3859
3860   my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
3861                  values %sectionmap;
3862
3863   my @lines = ();
3864   foreach my $section ( keys %lines ) {
3865     foreach my $line ( keys %{$lines{$section}} ) {
3866       my $l = $lines{$section}{$line};
3867       $l->{section}     = $sectionmap{$section};
3868       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
3869       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
3870       push @lines, $l;
3871     }
3872   }
3873
3874   return(\@sections, \@lines);
3875
3876 }
3877
3878 sub _did_summary {
3879     my $self = shift;
3880     my $end = $self->_date;
3881     my $start = $end - 2592000; # 30 days
3882     my $cust_main = $self->cust_main;
3883     my @pkgs = $cust_main->all_pkgs;
3884     my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
3885         = (0,0,0,0,0);
3886     my @seen = ();
3887     foreach my $pkg ( @pkgs ) {
3888         my @h_cust_svc = $pkg->h_cust_svc($end);
3889         foreach my $h_cust_svc ( @h_cust_svc ) {
3890             next if grep {$_ eq $h_cust_svc->svcnum} @seen;
3891             next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
3892
3893             my $inserted = $h_cust_svc->date_inserted;
3894             my $deleted = $h_cust_svc->date_deleted;
3895             my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
3896             my $phone_deleted;
3897             $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
3898             
3899 # DID either activated or ported in; cannot be both for same DID simultaneously
3900             if ($inserted >= $start && $inserted <= $end && $phone_inserted
3901                 && (!$phone_inserted->lnp_status 
3902                     || $phone_inserted->lnp_status eq ''
3903                     || $phone_inserted->lnp_status eq 'native')) {
3904                 $num_activated++;
3905             }
3906             else { # this one not so clean, should probably move to (h_)svc_phone
3907                  my $phone_portedin = qsearchs( 'h_svc_phone',
3908                       { 'svcnum' => $h_cust_svc->svcnum, 
3909                         'lnp_status' => 'portedin' },  
3910                       FS::h_svc_phone->sql_h_searchs($end),  
3911                     );
3912                  $num_portedin++ if $phone_portedin;
3913             }
3914
3915 # DID either deactivated or ported out; cannot be both for same DID simultaneously
3916             if($deleted >= $start && $deleted <= $end && $phone_deleted
3917                 && (!$phone_deleted->lnp_status 
3918                     || $phone_deleted->lnp_status ne 'portingout')) {
3919                 $num_deactivated++;
3920             } 
3921             elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
3922                 && $phone_deleted->lnp_status 
3923                 && $phone_deleted->lnp_status eq 'portingout') {
3924                 $num_portedout++;
3925             }
3926
3927             # increment usage minutes
3928             my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
3929             foreach my $cdr ( @cdrs ) {
3930                 $minutes += $cdr->billsec/60;
3931             }
3932
3933             # don't look at this service again
3934             push @seen, $h_cust_svc->svcnum;
3935         }
3936     }
3937
3938     $minutes = sprintf("%d", $minutes);
3939     ("Activated: $num_activated  Ported-In: $num_portedin  Deactivated: "
3940         . "$num_deactivated  Ported-Out: $num_portedout ",
3941             "Total Minutes: $minutes");
3942 }
3943
3944 sub _items_svc_phone_sections {
3945   my $self = shift;
3946   my $escape = shift;
3947   my $format = shift;
3948
3949   my %sections = ();
3950   my %classnums = ();
3951   my %lines = ();
3952
3953   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3954   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
3955
3956   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3957     next unless $cust_bill_pkg->pkgnum > 0;
3958
3959     my @header = $cust_bill_pkg->details_header;
3960     next unless scalar(@header);
3961
3962     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
3963
3964       my $phonenum = $detail->phonenum;
3965       next unless $phonenum;
3966
3967       my $amount = $detail->amount;
3968       next unless $amount && $amount > 0;
3969
3970       $sections{$phonenum} ||= { 'amount'      => 0,
3971                                  'calls'       => 0,
3972                                  'duration'    => 0,
3973                                  'sort_weight' => -1,
3974                                  'phonenum'    => $phonenum,
3975                                 };
3976       $sections{$phonenum}{amount} += $amount;  #subtotal
3977       $sections{$phonenum}{calls}++;
3978       $sections{$phonenum}{duration} += $detail->duration;
3979
3980       my $desc = $detail->regionname; 
3981       my $description = $desc;
3982       $description = substr($desc, 0, 50). '...'
3983         if $format eq 'latex' && length($desc) > 50;
3984
3985       $lines{$phonenum}{$desc} ||= {
3986         description     => &{$escape}($description),
3987         #pkgpart         => $part_pkg->pkgpart,
3988         pkgnum          => '',
3989         ref             => '',
3990         amount          => 0,
3991         calls           => 0,
3992         duration        => 0,
3993         #unit_amount     => '',
3994         quantity        => '',
3995         product_code    => 'N/A',
3996         ext_description => [],
3997       };
3998
3999       $lines{$phonenum}{$desc}{amount} += $amount;
4000       $lines{$phonenum}{$desc}{calls}++;
4001       $lines{$phonenum}{$desc}{duration} += $detail->duration;
4002
4003       my $line = $usage_class{$detail->classnum}->classname;
4004       $sections{"$phonenum $line"} ||=
4005         { 'amount' => 0,
4006           'calls' => 0,
4007           'duration' => 0,
4008           'sort_weight' => $usage_class{$detail->classnum}->weight,
4009           'phonenum' => $phonenum,
4010           'header'  => [ @header ],
4011         };
4012       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
4013       $sections{"$phonenum $line"}{calls}++;
4014       $sections{"$phonenum $line"}{duration} += $detail->duration;
4015
4016       $lines{"$phonenum $line"}{$desc} ||= {
4017         description     => &{$escape}($description),
4018         #pkgpart         => $part_pkg->pkgpart,
4019         pkgnum          => '',
4020         ref             => '',
4021         amount          => 0,
4022         calls           => 0,
4023         duration        => 0,
4024         #unit_amount     => '',
4025         quantity        => '',
4026         product_code    => 'N/A',
4027         ext_description => [],
4028       };
4029
4030       $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4031       $lines{"$phonenum $line"}{$desc}{calls}++;
4032       $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4033       push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4034            $detail->formatted('format' => $format);
4035
4036     }
4037   }
4038
4039   my %sectionmap = ();
4040   my $simple = new FS::usage_class { format => 'simple' }; #bleh
4041   foreach ( keys %sections ) {
4042     my @header = @{ $sections{$_}{header} || [] };
4043     my $usage_simple =
4044       new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4045     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4046     my $usage_class = $summary ? $simple : $usage_simple;
4047     my $ending = $summary ? ' usage charges' : '';
4048     my %gen_opt = ();
4049     unless ($summary) {
4050       $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4051     }
4052     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4053                         'amount'    => $sections{$_}{amount},    #subtotal
4054                         'calls'       => $sections{$_}{calls},
4055                         'duration'    => $sections{$_}{duration},
4056                         'summarized'  => '',
4057                         'tax_section' => '',
4058                         'phonenum'    => $sections{$_}{phonenum},
4059                         'sort_weight' => $sections{$_}{sort_weight},
4060                         'post_total'  => $summary, #inspire pagebreak
4061                         (
4062                           ( map { $_ => $usage_class->$_($format, %gen_opt) }
4063                             qw( description_generator
4064                                 header_generator
4065                                 total_generator
4066                                 total_line_generator
4067                               )
4068                           )
4069                         ), 
4070                       };
4071   }
4072
4073   my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4074                         $a->{sort_weight} <=> $b->{sort_weight}
4075                       }
4076                  values %sectionmap;
4077
4078   my @lines = ();
4079   foreach my $section ( keys %lines ) {
4080     foreach my $line ( keys %{$lines{$section}} ) {
4081       my $l = $lines{$section}{$line};
4082       $l->{section}     = $sectionmap{$section};
4083       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
4084       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4085       push @lines, $l;
4086     }
4087   }
4088
4089   return(\@sections, \@lines);
4090
4091 }
4092
4093 sub _items {
4094   my $self = shift;
4095
4096   #my @display = scalar(@_)
4097   #              ? @_
4098   #              : qw( _items_previous _items_pkg );
4099   #              #: qw( _items_pkg );
4100   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4101   my @display = qw( _items_previous _items_pkg );
4102
4103   my @b = ();
4104   foreach my $display ( @display ) {
4105     push @b, $self->$display(@_);
4106   }
4107   @b;
4108 }
4109
4110 sub _items_previous {
4111   my $self = shift;
4112   my $cust_main = $self->cust_main;
4113   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4114   my @b = ();
4115   foreach ( @pr_cust_bill ) {
4116     my $date = $conf->exists('invoice_show_prior_due_date')
4117                ? 'due '. $_->due_date2str($date_format)
4118                : time2str($date_format, $_->_date);
4119     push @b, {
4120       'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4121       #'pkgpart'     => 'N/A',
4122       'pkgnum'      => 'N/A',
4123       'amount'      => sprintf("%.2f", $_->owed),
4124     };
4125   }
4126   @b;
4127
4128   #{
4129   #    'description'     => 'Previous Balance',
4130   #    #'pkgpart'         => 'N/A',
4131   #    'pkgnum'          => 'N/A',
4132   #    'amount'          => sprintf("%10.2f", $pr_total ),
4133   #    'ext_description' => [ map {
4134   #                                 "Invoice ". $_->invnum.
4135   #                                 " (". time2str("%x",$_->_date). ") ".
4136   #                                 sprintf("%10.2f", $_->owed)
4137   #                         } @pr_cust_bill ],
4138
4139   #};
4140 }
4141
4142 sub _items_pkg {
4143   my $self = shift;
4144   my %options = @_;
4145   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4146   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4147   if ($options{section} && $options{section}->{condensed}) {
4148     my %itemshash = ();
4149     local $Storable::canonical = 1;
4150     foreach ( @items ) {
4151       my $item = { %$_ };
4152       delete $item->{ref};
4153       delete $item->{ext_description};
4154       my $key = freeze($item);
4155       $itemshash{$key} ||= 0;
4156       $itemshash{$key} ++; # += $item->{quantity};
4157     }
4158     @items = sort { $a->{description} cmp $b->{description} }
4159              map { my $i = thaw($_);
4160                    $i->{quantity} = $itemshash{$_};
4161                    $i->{amount} =
4162                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4163                    $i;
4164                  }
4165              keys %itemshash;
4166   }
4167   @items;
4168 }
4169
4170 sub _taxsort {
4171   return 0 unless $a->itemdesc cmp $b->itemdesc;
4172   return -1 if $b->itemdesc eq 'Tax';
4173   return 1 if $a->itemdesc eq 'Tax';
4174   return -1 if $b->itemdesc eq 'Other surcharges';
4175   return 1 if $a->itemdesc eq 'Other surcharges';
4176   $a->itemdesc cmp $b->itemdesc;
4177 }
4178
4179 sub _items_tax {
4180   my $self = shift;
4181   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4182   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4183 }
4184
4185 sub _items_cust_bill_pkg {
4186   my $self = shift;
4187   my $cust_bill_pkg = shift;
4188   my %opt = @_;
4189
4190   my $format = $opt{format} || '';
4191   my $escape_function = $opt{escape_function} || sub { shift };
4192   my $format_function = $opt{format_function} || '';
4193   my $unsquelched = $opt{unsquelched} || '';
4194   my $section = $opt{section}->{description} if $opt{section};
4195   my $summary_page = $opt{summary_page} || '';
4196   my $multilocation = $opt{multilocation} || '';
4197   my $multisection = $opt{multisection} || '';
4198   my $discount_show_always = 0;
4199
4200   my @b = ();
4201   my ($s, $r, $u) = ( undef, undef, undef );
4202   foreach my $cust_bill_pkg ( @$cust_bill_pkg )
4203   {
4204
4205     $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4206                                 && $conf->exists('discount-show-always'));
4207
4208     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4209       if ( $_ && !$cust_bill_pkg->hidden ) {
4210         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4211         $_->{amount}      =~ s/^\-0\.00$/0.00/;
4212         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4213         push @b, { %$_ }
4214           unless ( $_->{amount} == 0 && !$discount_show_always );
4215         $_ = undef;
4216       }
4217     }
4218
4219     foreach my $display ( grep { defined($section)
4220                                  ? $_->section eq $section
4221                                  : 1
4222                                }
4223                           #grep { !$_->summary || !$summary_page } # bunk!
4224                           grep { !$_->summary || $multisection }
4225                           $cust_bill_pkg->cust_bill_pkg_display
4226                         )
4227     {
4228
4229       my $type = $display->type;
4230
4231       my $desc = $cust_bill_pkg->desc;
4232       $desc = substr($desc, 0, 50). '...'
4233         if $format eq 'latex' && length($desc) > 50;
4234
4235       my %details_opt = ( 'format'          => $format,
4236                           'escape_function' => $escape_function,
4237                           'format_function' => $format_function,
4238                         );
4239
4240       if ( $cust_bill_pkg->pkgnum > 0 ) {
4241
4242         my $cust_pkg = $cust_bill_pkg->cust_pkg;
4243
4244         if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4245
4246           my $description = $desc;
4247           $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4248
4249           my @d = ();
4250           unless ( $cust_pkg->part_pkg->hide_svc_detail
4251                 || $cust_bill_pkg->hidden )
4252           {
4253
4254             push @d, map &{$escape_function}($_),
4255                          $cust_pkg->h_labels_short($self->_date, undef, 'I')
4256               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4257
4258             if ( $multilocation ) {
4259               my $loc = $cust_pkg->location_label;
4260               $loc = substr($loc, 0, 50). '...'
4261                 if $format eq 'latex' && length($loc) > 50;
4262               push @d, &{$escape_function}($loc);
4263             }
4264
4265           }
4266
4267           push @d, $cust_bill_pkg->details(%details_opt)
4268             if $cust_bill_pkg->recur == 0;
4269
4270           if ( $cust_bill_pkg->hidden ) {
4271             $s->{amount}      += $cust_bill_pkg->setup;
4272             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4273             push @{ $s->{ext_description} }, @d;
4274           } else {
4275             $s = {
4276               description     => $description,
4277               #pkgpart         => $part_pkg->pkgpart,
4278               pkgnum          => $cust_bill_pkg->pkgnum,
4279               amount          => $cust_bill_pkg->setup,
4280               unit_amount     => $cust_bill_pkg->unitsetup,
4281               quantity        => $cust_bill_pkg->quantity,
4282               ext_description => \@d,
4283             };
4284           };
4285
4286         }
4287
4288         if ( ( $cust_bill_pkg->recur != 0  || $cust_bill_pkg->setup == 0 || 
4289                 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4290              ( !$type || $type eq 'R' || $type eq 'U' )
4291            )
4292         {
4293
4294           my $is_summary = $display->summary;
4295           my $description = ($is_summary && $type && $type eq 'U')
4296                             ? "Usage charges" : $desc;
4297
4298           unless ( $conf->exists('disable_line_item_date_ranges') ) {
4299             $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4300                             " - ". time2str($date_format, $cust_bill_pkg->edate). ")";
4301           }
4302
4303           my @d = ();
4304
4305           #at least until cust_bill_pkg has "past" ranges in addition to
4306           #the "future" sdate/edate ones... see #3032
4307           my @dates = ( $self->_date );
4308           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4309           push @dates, $prev->sdate if $prev;
4310           push @dates, undef if !$prev;
4311
4312           unless ( $cust_pkg->part_pkg->hide_svc_detail
4313                 || $cust_bill_pkg->itemdesc
4314                 || $cust_bill_pkg->hidden
4315                 || $is_summary && $type && $type eq 'U' )
4316           {
4317
4318             push @d, map &{$escape_function}($_),
4319                          $cust_pkg->h_labels_short(@dates, 'I')
4320                                                    #$cust_bill_pkg->edate,
4321                                                    #$cust_bill_pkg->sdate)
4322               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4323
4324             if ( $multilocation ) {
4325               my $loc = $cust_pkg->location_label;
4326               $loc = substr($loc, 0, 50). '...'
4327                 if $format eq 'latex' && length($loc) > 50;
4328               push @d, &{$escape_function}($loc);
4329             }
4330
4331           }
4332
4333           push @d, $cust_bill_pkg->details(%details_opt)
4334             unless ($is_summary || $type && $type eq 'R');
4335   
4336           my $amount = 0;
4337           if (!$type) {
4338             $amount = $cust_bill_pkg->recur;
4339           }elsif($type eq 'R') {
4340             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4341           }elsif($type eq 'U') {
4342             $amount = $cust_bill_pkg->usage;
4343           }
4344   
4345           if ( !$type || $type eq 'R' ) {
4346
4347             if ( $cust_bill_pkg->hidden ) {
4348               $r->{amount}      += $amount;
4349               $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4350               push @{ $r->{ext_description} }, @d;
4351             } else {
4352               $r = {
4353                 description     => $description,
4354                 #pkgpart         => $part_pkg->pkgpart,
4355                 pkgnum          => $cust_bill_pkg->pkgnum,
4356                 amount          => $amount,
4357                 unit_amount     => $cust_bill_pkg->unitrecur,
4358                 quantity        => $cust_bill_pkg->quantity,
4359                 ext_description => \@d,
4360               };
4361             }
4362
4363           } else {  # $type eq 'U'
4364
4365             if ( $cust_bill_pkg->hidden ) {
4366               $u->{amount}      += $amount;
4367               $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4368               push @{ $u->{ext_description} }, @d;
4369             } else {
4370               $u = {
4371                 description     => $description,
4372                 #pkgpart         => $part_pkg->pkgpart,
4373                 pkgnum          => $cust_bill_pkg->pkgnum,
4374                 amount          => $amount,
4375                 unit_amount     => $cust_bill_pkg->unitrecur,
4376                 quantity        => $cust_bill_pkg->quantity,
4377                 ext_description => \@d,
4378               };
4379             }
4380
4381           }
4382
4383         } # recurring or usage with recurring charge
4384
4385       } else { #pkgnum tax or one-shot line item (??)
4386
4387         if ( $cust_bill_pkg->setup != 0 ) {
4388           push @b, {
4389             'description' => $desc,
4390             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
4391           };
4392         }
4393         if ( $cust_bill_pkg->recur != 0 ) {
4394           push @b, {
4395             'description' => "$desc (".
4396                              time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4397                              time2str($date_format, $cust_bill_pkg->edate). ')',
4398             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
4399           };
4400         }
4401
4402       }
4403
4404     }
4405
4406   }
4407
4408   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4409     if ( $_  ) {
4410       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4411       $_->{amount}      =~ s/^\-0\.00$/0.00/;
4412       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4413       push @b, { %$_ }
4414             unless ( $_->{amount} == 0 && !$discount_show_always );
4415     }
4416   }
4417
4418   @b;
4419
4420 }
4421
4422 sub _items_credits {
4423   my( $self, %opt ) = @_;
4424   my $trim_len = $opt{'trim_len'} || 60;
4425
4426   my @b;
4427   #credits
4428   foreach ( $self->cust_credited ) {
4429
4430     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4431
4432     my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4433     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4434     $reason = " ($reason) " if $reason;
4435
4436     push @b, {
4437       #'description' => 'Credit ref\#'. $_->crednum.
4438       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
4439       #                 $reason,
4440       'description' => 'Credit applied '.
4441                        time2str($date_format,$_->cust_credit->_date). $reason,
4442       'amount'      => sprintf("%.2f",$_->amount),
4443     };
4444   }
4445
4446   @b;
4447
4448 }
4449
4450 sub _items_payments {
4451   my $self = shift;
4452
4453   my @b;
4454   #get & print payments
4455   foreach ( $self->cust_bill_pay ) {
4456
4457     #something more elaborate if $_->amount ne ->cust_pay->paid ?
4458
4459     push @b, {
4460       'description' => "Payment received ".
4461                        time2str($date_format,$_->cust_pay->_date ),
4462       'amount'      => sprintf("%.2f", $_->amount )
4463     };
4464   }
4465
4466   @b;
4467
4468 }
4469
4470 =item call_details [ OPTION => VALUE ... ]
4471
4472 Returns an array of CSV strings representing the call details for this invoice
4473 The only option available is the boolean prepend_billed_number
4474
4475 =cut
4476
4477 sub call_details {
4478   my ($self, %opt) = @_;
4479
4480   my $format_function = sub { shift };
4481
4482   if ($opt{prepend_billed_number}) {
4483     $format_function = sub {
4484       my $detail = shift;
4485       my $row = shift;
4486
4487       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4488       
4489     };
4490   }
4491
4492   my @details = map { $_->details( 'format_function' => $format_function,
4493                                    'escape_function' => sub{ return() },
4494                                  )
4495                     }
4496                   grep { $_->pkgnum }
4497                   $self->cust_bill_pkg;
4498   my $header = $details[0];
4499   ( $header, grep { $_ ne $header } @details );
4500 }
4501
4502
4503 =back
4504
4505 =head1 SUBROUTINES
4506
4507 =over 4
4508
4509 =item process_reprint
4510
4511 =cut
4512
4513 sub process_reprint {
4514   process_re_X('print', @_);
4515 }
4516
4517 =item process_reemail
4518
4519 =cut
4520
4521 sub process_reemail {
4522   process_re_X('email', @_);
4523 }
4524
4525 =item process_refax
4526
4527 =cut
4528
4529 sub process_refax {
4530   process_re_X('fax', @_);
4531 }
4532
4533 =item process_reftp
4534
4535 =cut
4536
4537 sub process_reftp {
4538   process_re_X('ftp', @_);
4539 }
4540
4541 =item respool
4542
4543 =cut
4544
4545 sub process_respool {
4546   process_re_X('spool', @_);
4547 }
4548
4549 use Storable qw(thaw);
4550 use Data::Dumper;
4551 use MIME::Base64;
4552 sub process_re_X {
4553   my( $method, $job ) = ( shift, shift );
4554   warn "$me process_re_X $method for job $job\n" if $DEBUG;
4555
4556   my $param = thaw(decode_base64(shift));
4557   warn Dumper($param) if $DEBUG;
4558
4559   re_X(
4560     $method,
4561     $job,
4562     %$param,
4563   );
4564
4565 }
4566
4567 sub re_X {
4568   my($method, $job, %param ) = @_;
4569   if ( $DEBUG ) {
4570     warn "re_X $method for job $job with param:\n".
4571          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
4572   }
4573
4574   #some false laziness w/search/cust_bill.html
4575   my $distinct = '';
4576   my $orderby = 'ORDER BY cust_bill._date';
4577
4578   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4579
4580   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4581      
4582   my @cust_bill = qsearch( {
4583     #'select'    => "cust_bill.*",
4584     'table'     => 'cust_bill',
4585     'addl_from' => $addl_from,
4586     'hashref'   => {},
4587     'extra_sql' => $extra_sql,
4588     'order_by'  => $orderby,
4589     'debug' => 1,
4590   } );
4591
4592   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4593
4594   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4595     if $DEBUG;
4596
4597   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4598   foreach my $cust_bill ( @cust_bill ) {
4599     $cust_bill->$method();
4600
4601     if ( $job ) { #progressbar foo
4602       $num++;
4603       if ( time - $min_sec > $last ) {
4604         my $error = $job->update_statustext(
4605           int( 100 * $num / scalar(@cust_bill) )
4606         );
4607         die $error if $error;
4608         $last = time;
4609       }
4610     }
4611
4612   }
4613
4614 }
4615
4616 =back
4617
4618 =head1 CLASS METHODS
4619
4620 =over 4
4621
4622 =item owed_sql
4623
4624 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4625
4626 =cut
4627
4628 sub owed_sql {
4629   my ($class, $start, $end) = @_;
4630   'charged - '. 
4631     $class->paid_sql($start, $end). ' - '. 
4632     $class->credited_sql($start, $end);
4633 }
4634
4635 =item net_sql
4636
4637 Returns an SQL fragment to retreive the net amount (charged minus credited).
4638
4639 =cut
4640
4641 sub net_sql {
4642   my ($class, $start, $end) = @_;
4643   'charged - '. $class->credited_sql($start, $end);
4644 }
4645
4646 =item paid_sql
4647
4648 Returns an SQL fragment to retreive the amount paid against this invoice.
4649
4650 =cut
4651
4652 sub paid_sql {
4653   my ($class, $start, $end) = @_;
4654   $start &&= "AND cust_bill_pay._date <= $start";
4655   $end   &&= "AND cust_bill_pay._date > $end";
4656   $start = '' unless defined($start);
4657   $end   = '' unless defined($end);
4658   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4659        WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
4660 }
4661
4662 =item credited_sql
4663
4664 Returns an SQL fragment to retreive the amount credited against this invoice.
4665
4666 =cut
4667
4668 sub credited_sql {
4669   my ($class, $start, $end) = @_;
4670   $start &&= "AND cust_credit_bill._date <= $start";
4671   $end   &&= "AND cust_credit_bill._date >  $end";
4672   $start = '' unless defined($start);
4673   $end   = '' unless defined($end);
4674   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4675        WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
4676 }
4677
4678 =item due_date_sql
4679
4680 Returns an SQL fragment to retrieve the due date of an invoice.
4681 Currently only supported on PostgreSQL.
4682
4683 =cut
4684
4685 sub due_date_sql {
4686 'COALESCE(
4687   SUBSTRING(
4688     COALESCE(
4689       cust_bill.invoice_terms,
4690       cust_main.invoice_terms,
4691       \''.($conf->config('invoice_default_terms') || '').'\'
4692     ), E\'Net (\\\\d+)\'
4693   )::INTEGER, 0
4694 ) * 86400 + cust_bill._date'
4695 }
4696
4697 =item search_sql_where HASHREF
4698
4699 Class method which returns an SQL WHERE fragment to search for parameters
4700 specified in HASHREF.  Valid parameters are
4701
4702 =over 4
4703
4704 =item _date
4705
4706 List reference of start date, end date, as UNIX timestamps.
4707
4708 =item invnum_min
4709
4710 =item invnum_max
4711
4712 =item agentnum
4713
4714 =item charged
4715
4716 List reference of charged limits (exclusive).
4717
4718 =item owed
4719
4720 List reference of charged limits (exclusive).
4721
4722 =item open
4723
4724 flag, return open invoices only
4725
4726 =item net
4727
4728 flag, return net invoices only
4729
4730 =item days
4731
4732 =item newest_percust
4733
4734 =back
4735
4736 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
4737
4738 =cut
4739
4740 sub search_sql_where {
4741   my($class, $param) = @_;
4742   if ( $DEBUG ) {
4743     warn "$me search_sql_where called with params: \n".
4744          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
4745   }
4746
4747   my @search = ();
4748
4749   #agentnum
4750   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
4751     push @search, "cust_main.agentnum = $1";
4752   }
4753
4754   #_date
4755   if ( $param->{_date} ) {
4756     my($beginning, $ending) = @{$param->{_date}};
4757
4758     push @search, "cust_bill._date >= $beginning",
4759                   "cust_bill._date <  $ending";
4760   }
4761
4762   #invnum
4763   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
4764     push @search, "cust_bill.invnum >= $1";
4765   }
4766   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
4767     push @search, "cust_bill.invnum <= $1";
4768   }
4769
4770   #charged
4771   if ( $param->{charged} ) {
4772     my @charged = ref($param->{charged})
4773                     ? @{ $param->{charged} }
4774                     : ($param->{charged});
4775
4776     push @search, map { s/^charged/cust_bill.charged/; $_; }
4777                       @charged;
4778   }
4779
4780   my $owed_sql = FS::cust_bill->owed_sql;
4781
4782   #owed
4783   if ( $param->{owed} ) {
4784     my @owed = ref($param->{owed})
4785                  ? @{ $param->{owed} }
4786                  : ($param->{owed});
4787     push @search, map { s/^owed/$owed_sql/; $_; }
4788                       @owed;
4789   }
4790
4791   #open/net flags
4792   push @search, "0 != $owed_sql"
4793     if $param->{'open'};
4794   push @search, '0 != '. FS::cust_bill->net_sql
4795     if $param->{'net'};
4796
4797   #days
4798   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
4799     if $param->{'days'};
4800
4801   #newest_percust
4802   if ( $param->{'newest_percust'} ) {
4803
4804     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
4805     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
4806
4807     my @newest_where = map { my $x = $_;
4808                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
4809                              $x;
4810                            }
4811                            grep ! /^cust_main./, @search;
4812     my $newest_where = scalar(@newest_where)
4813                          ? ' AND '. join(' AND ', @newest_where)
4814                          : '';
4815
4816
4817     push @search, "cust_bill._date = (
4818       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
4819         WHERE newest_cust_bill.custnum = cust_bill.custnum
4820           $newest_where
4821     )";
4822
4823   }
4824
4825   #agent virtualization
4826   my $curuser = $FS::CurrentUser::CurrentUser;
4827   if ( $curuser->username eq 'fs_queue'
4828        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
4829     my $username = $1;
4830     my $newuser = qsearchs('access_user', {
4831       'username' => $username,
4832       'disabled' => '',
4833     } );
4834     if ( $newuser ) {
4835       $curuser = $newuser;
4836     } else {
4837       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
4838     }
4839   }
4840   push @search, $curuser->agentnums_sql;
4841
4842   join(' AND ', @search );
4843
4844 }
4845
4846 =back
4847
4848 =head1 BUGS
4849
4850 The delete method.
4851
4852 =head1 SEE ALSO
4853
4854 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
4855 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
4856 documentation.
4857
4858 =cut
4859
4860 1;
4861