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