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