fix package balance conditions, RT#11834
[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     'returnaddress'   => $returnaddress,
2521     'agent'           => &$escape_function($cust_main->agent->agent),
2522
2523     #invoice info
2524     'invnum'          => $self->invnum,
2525     'date'            => time2str($date_format, $self->_date),
2526     'today'           => time2str($date_format_long, $today),
2527     'terms'           => $self->terms,
2528     'template'        => $template, #params{'template'},
2529     'notice_name'     => ($params{'notice_name'} || 'Invoice'),#escape_function?
2530     'current_charges' => sprintf("%.2f", $self->charged),
2531     'duedate'         => $self->due_date2str($rdate_format), #date_format?
2532
2533     #customer info
2534     'custnum'         => $cust_main->display_custnum,
2535     'agent_custid'    => &$escape_function($cust_main->agent_custid),
2536     ( map { $_ => &$escape_function($cust_main->$_()) } qw(
2537       payname company address1 address2 city state zip fax
2538     )),
2539
2540     #global config
2541     'ship_enable'     => $conf->exists('invoice-ship_address'),
2542     'unitprices'      => $conf->exists('invoice-unitprice'),
2543     'smallernotes'    => $conf->exists('invoice-smallernotes'),
2544     'smallerfooter'   => $conf->exists('invoice-smallerfooter'),
2545     'balance_due_below_line' => $conf->exists('balance_due_below_line'),
2546    
2547     #layout info -- would be fancy to calc some of this and bury the template
2548     #               here in the code
2549     'topmargin'             => scalar($conf->config('invoice_latextopmargin', $agentnum)),
2550     'headsep'               => scalar($conf->config('invoice_latexheadsep', $agentnum)),
2551     'textheight'            => scalar($conf->config('invoice_latextextheight', $agentnum)),
2552     'extracouponspace'      => scalar($conf->config('invoice_latexextracouponspace', $agentnum)),
2553     'couponfootsep'         => scalar($conf->config('invoice_latexcouponfootsep', $agentnum)),
2554     'verticalreturnaddress' => $conf->exists('invoice_latexverticalreturnaddress', $agentnum),
2555     'addresssep'            => scalar($conf->config('invoice_latexaddresssep', $agentnum)),
2556     'amountenclosedsep'     => scalar($conf->config('invoice_latexcouponamountenclosedsep', $agentnum)),
2557     'coupontoaddresssep'    => scalar($conf->config('invoice_latexcoupontoaddresssep', $agentnum)),
2558     'addcompanytoaddress'   => $conf->exists('invoice_latexcouponaddcompanytoaddress', $agentnum),
2559
2560     # better hang on to conf_dir for a while (for old templates)
2561     'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2562
2563     #these are only used when doing paged plaintext
2564     'page'            => 1,
2565     'total_pages'     => 1,
2566
2567   );
2568   
2569   my $min_sdate = 999999999999;
2570   my $max_edate = 0;
2571   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2572     next unless $cust_bill_pkg->pkgnum > 0;
2573     $min_sdate = $cust_bill_pkg->sdate
2574       if length($cust_bill_pkg->sdate) && $cust_bill_pkg->sdate < $min_sdate;
2575     $max_edate = $cust_bill_pkg->edate
2576       if length($cust_bill_pkg->edate) && $cust_bill_pkg->edate > $max_edate;
2577   }
2578
2579   $invoice_data{'bill_period'} = '';
2580   $invoice_data{'bill_period'} = time2str('%e %h', $min_sdate) 
2581     . " to " . time2str('%e %h', $max_edate)
2582     if ($max_edate != 0 && $min_sdate != 999999999999);
2583
2584   $invoice_data{finance_section} = '';
2585   if ( $conf->config('finance_pkgclass') ) {
2586     my $pkg_class =
2587       qsearchs('pkg_class', { classnum => $conf->config('finance_pkgclass') });
2588     $invoice_data{finance_section} = $pkg_class->categoryname;
2589   } 
2590   $invoice_data{finance_amount} = '0.00';
2591   $invoice_data{finance_section} ||= 'Finance Charges'; #avoid config confusion
2592
2593   my $countrydefault = $conf->config('countrydefault') || 'US';
2594   my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2595   foreach ( qw( contact company address1 address2 city state zip country fax) ){
2596     my $method = $prefix.$_;
2597     $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2598   }
2599   $invoice_data{'ship_country'} = ''
2600     if ( $invoice_data{'ship_country'} eq $countrydefault );
2601   
2602   $invoice_data{'cid'} = $params{'cid'}
2603     if $params{'cid'};
2604
2605   if ( $cust_main->country eq $countrydefault ) {
2606     $invoice_data{'country'} = '';
2607   } else {
2608     $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2609   }
2610
2611   my @address = ();
2612   $invoice_data{'address'} = \@address;
2613   push @address,
2614     $cust_main->payname.
2615       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2616         ? " (P.O. #". $cust_main->payinfo. ")"
2617         : ''
2618       )
2619   ;
2620   push @address, $cust_main->company
2621     if $cust_main->company;
2622   push @address, $cust_main->address1;
2623   push @address, $cust_main->address2
2624     if $cust_main->address2;
2625   push @address,
2626     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
2627   push @address, $invoice_data{'country'}
2628     if $invoice_data{'country'};
2629   push @address, ''
2630     while (scalar(@address) < 5);
2631
2632   $invoice_data{'logo_file'} = $params{'logo_file'}
2633     if $params{'logo_file'};
2634   $invoice_data{'barcode_file'} = $params{'barcode_file'}
2635     if $params{'barcode_file'};
2636   $invoice_data{'barcode_img'} = $params{'barcode_img'}
2637     if $params{'barcode_img'};
2638   $invoice_data{'barcode_cid'} = $params{'barcode_cid'}
2639     if $params{'barcode_cid'};
2640
2641   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2642 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2643   #my $balance_due = $self->owed + $pr_total - $cr_total;
2644   my $balance_due = $self->owed + $pr_total;
2645   $invoice_data{'true_previous_balance'} = sprintf("%.2f", ($self->previous_balance || 0) );
2646   $invoice_data{'balance_adjustments'} = sprintf("%.2f", ($self->previous_balance || 0) - ($self->billing_balance || 0) );
2647   $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2648   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2649
2650   my $summarypage = '';
2651   if ( $conf->exists('invoice_usesummary', $agentnum) ) {
2652     $summarypage = 1;
2653   }
2654   $invoice_data{'summarypage'} = $summarypage;
2655
2656   warn "$me substituting variables in notes, footer, smallfooter\n"
2657     if $DEBUG > 1;
2658
2659   foreach my $include (qw( notes footer smallfooter coupon )) {
2660
2661     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2662     my @inc_src;
2663
2664     if ( $conf->exists($inc_file, $agentnum)
2665          && length( $conf->config($inc_file, $agentnum) ) ) {
2666
2667       @inc_src = $conf->config($inc_file, $agentnum);
2668
2669     } else {
2670
2671       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2672
2673       my $convert_map = $convert_maps{$format}{$include};
2674
2675       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2676                        s/--\@\]/$delimiters{$format}[1]/g;
2677                        $_;
2678                      } 
2679                  &$convert_map( $conf->config($inc_file, $agentnum) );
2680
2681     }
2682
2683     my $inc_tt = new Text::Template (
2684       TYPE       => 'ARRAY',
2685       SOURCE     => [ map "$_\n", @inc_src ],
2686       DELIMITERS => $delimiters{$format},
2687     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2688
2689     unless ( $inc_tt->compile() ) {
2690       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2691       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2692       die $error;
2693     }
2694
2695     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2696
2697     $invoice_data{$include} =~ s/\n+$//
2698       if ($format eq 'latex');
2699   }
2700
2701   $invoice_data{'po_line'} =
2702     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2703       ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2704       : $nbsp;
2705
2706   my %money_chars = ( 'latex'    => '',
2707                       'html'     => $conf->config('money_char') || '$',
2708                       'template' => '',
2709                     );
2710   my $money_char = $money_chars{$format};
2711
2712   my %other_money_chars = ( 'latex'    => '\dollar ',#XXX should be a config too
2713                             'html'     => $conf->config('money_char') || '$',
2714                             'template' => '',
2715                           );
2716   my $other_money_char = $other_money_chars{$format};
2717   $invoice_data{'dollar'} = $other_money_char;
2718
2719   my @detail_items = ();
2720   my @total_items = ();
2721   my @buf = ();
2722   my @sections = ();
2723
2724   $invoice_data{'detail_items'} = \@detail_items;
2725   $invoice_data{'total_items'} = \@total_items;
2726   $invoice_data{'buf'} = \@buf;
2727   $invoice_data{'sections'} = \@sections;
2728
2729   warn "$me generating sections\n"
2730     if $DEBUG > 1;
2731
2732   my $previous_section = { 'description' => 'Previous Charges',
2733                            'subtotal'    => $other_money_char.
2734                                             sprintf('%.2f', $pr_total),
2735                            'summarized'  => $summarypage ? 'Y' : '',
2736                          };
2737   $previous_section->{posttotal} = '0 / 30 / 60 / 90 days overdue '. 
2738     join(' / ', map { $cust_main->balance_date_range(@$_) }
2739                 $self->_prior_month30s
2740         )
2741     if $conf->exists('invoice_include_aging');
2742
2743   my $taxtotal = 0;
2744   my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2745                       'subtotal'    => $taxtotal,   # adjusted below
2746                       'summarized'  => $summarypage ? 'Y' : '',
2747                     };
2748   my $tax_weight = _pkg_category($tax_section->{description})
2749                         ? _pkg_category($tax_section->{description})->weight
2750                         : 0;
2751   $tax_section->{'summarized'} = $summarypage && !$tax_weight ? 'Y' : '';
2752   $tax_section->{'sort_weight'} = $tax_weight;
2753
2754
2755   my $adjusttotal = 0;
2756   my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2757                          'subtotal'    => 0,   # adjusted below
2758                          'summarized'  => $summarypage ? 'Y' : '',
2759                        };
2760   my $adjust_weight = _pkg_category($adjust_section->{description})
2761                         ? _pkg_category($adjust_section->{description})->weight
2762                         : 0;
2763   $adjust_section->{'summarized'} = $summarypage && !$adjust_weight ? 'Y' : '';
2764   $adjust_section->{'sort_weight'} = $adjust_weight;
2765
2766   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2767   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2768   $invoice_data{'multisection'} = $multisection;
2769   my $late_sections = [];
2770   my $extra_sections = [];
2771   my $extra_lines = ();
2772   if ( $multisection ) {
2773     ($extra_sections, $extra_lines) =
2774       $self->_items_extra_usage_sections($escape_function_nonbsp, $format)
2775       if $conf->exists('usage_class_as_a_section', $cust_main->agentnum);
2776
2777     push @$extra_sections, $adjust_section if $adjust_section->{sort_weight};
2778
2779     push @detail_items, @$extra_lines if $extra_lines;
2780     push @sections,
2781       $self->_items_sections( $late_sections,      # this could stand a refactor
2782                               $summarypage,
2783                               $escape_function_nonbsp,
2784                               $extra_sections,
2785                               $format,             #bah
2786                             );
2787     if ($conf->exists('svc_phone_sections')) {
2788       my ($phone_sections, $phone_lines) =
2789         $self->_items_svc_phone_sections($escape_function_nonbsp, $format);
2790       push @{$late_sections}, @$phone_sections;
2791       push @detail_items, @$phone_lines;
2792     }
2793   }else{
2794     push @sections, { 'description' => '', 'subtotal' => '' };
2795   }
2796
2797   unless (    $conf->exists('disable_previous_balance')
2798            || $conf->exists('previous_balance-summary_only')
2799          )
2800   {
2801
2802     warn "$me adding previous balances\n"
2803       if $DEBUG > 1;
2804
2805     foreach my $line_item ( $self->_items_previous ) {
2806
2807       my $detail = {
2808         ext_description => [],
2809       };
2810       $detail->{'ref'} = $line_item->{'pkgnum'};
2811       $detail->{'quantity'} = 1;
2812       $detail->{'section'} = $previous_section;
2813       $detail->{'description'} = &$escape_function($line_item->{'description'});
2814       if ( exists $line_item->{'ext_description'} ) {
2815         @{$detail->{'ext_description'}} = map {
2816           &$escape_function($_);
2817         } @{$line_item->{'ext_description'}};
2818       }
2819       $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2820                             $line_item->{'amount'};
2821       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2822
2823       push @detail_items, $detail;
2824       push @buf, [ $detail->{'description'},
2825                    $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2826                  ];
2827     }
2828
2829   }
2830
2831   if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2832     push @buf, ['','-----------'];
2833     push @buf, [ 'Total Previous Balance',
2834                  $money_char. sprintf("%10.2f", $pr_total) ];
2835     push @buf, ['',''];
2836   }
2837  
2838   if ( $conf->exists('svc_phone-did-summary') ) {
2839       warn "$me adding DID summary\n"
2840         if $DEBUG > 1;
2841
2842       my ($didsummary,$minutes) = $self->_did_summary;
2843       my $didsummary_desc = 'DID Activity Summary (Past 30 days)';
2844       push @detail_items, 
2845         { 'description' => $didsummary_desc,
2846             'ext_description' => [ $didsummary, $minutes ],
2847         }
2848         if !$multisection;
2849   }
2850
2851   foreach my $section (@sections, @$late_sections) {
2852
2853     warn "$me adding section \n". Dumper($section)
2854       if $DEBUG > 1;
2855
2856     # begin some normalization
2857     $section->{'subtotal'} = $section->{'amount'}
2858       if $multisection
2859          && !exists($section->{subtotal})
2860          && exists($section->{amount});
2861
2862     $invoice_data{finance_amount} = sprintf('%.2f', $section->{'subtotal'} )
2863       if ( $invoice_data{finance_section} &&
2864            $section->{'description'} eq $invoice_data{finance_section} );
2865
2866     $section->{'subtotal'} = $other_money_char.
2867                              sprintf('%.2f', $section->{'subtotal'})
2868       if $multisection;
2869
2870     # continue some normalization
2871     $section->{'amount'}   = $section->{'subtotal'}
2872       if $multisection;
2873
2874
2875     if ( $section->{'description'} ) {
2876       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2877                    [ '', '' ],
2878                  );
2879     }
2880
2881     warn "$me   setting options\n"
2882       if $DEBUG > 1;
2883
2884     my $multilocation = scalar($cust_main->cust_location); #too expensive?
2885     my %options = ();
2886     $options{'section'} = $section if $multisection;
2887     $options{'format'} = $format;
2888     $options{'escape_function'} = $escape_function;
2889     $options{'format_function'} = sub { () } unless $unsquelched;
2890     $options{'unsquelched'} = $unsquelched;
2891     $options{'summary_page'} = $summarypage;
2892     $options{'skip_usage'} =
2893       scalar(@$extra_sections) && !grep{$section == $_} @$extra_sections;
2894     $options{'multilocation'} = $multilocation;
2895     $options{'multisection'} = $multisection;
2896
2897     warn "$me   searching for line items\n"
2898       if $DEBUG > 1;
2899
2900     foreach my $line_item ( $self->_items_pkg(%options) ) {
2901
2902       warn "$me     adding line item $line_item\n"
2903         if $DEBUG > 1;
2904
2905       my $detail = {
2906         ext_description => [],
2907       };
2908       $detail->{'ref'} = $line_item->{'pkgnum'};
2909       $detail->{'quantity'} = $line_item->{'quantity'};
2910       $detail->{'section'} = $section;
2911       $detail->{'description'} = &$escape_function($line_item->{'description'});
2912       if ( exists $line_item->{'ext_description'} ) {
2913         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2914       }
2915       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2916                               $line_item->{'amount'};
2917       $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2918                                  $line_item->{'unit_amount'};
2919       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2920   
2921       push @detail_items, $detail;
2922       push @buf, ( [ $detail->{'description'},
2923                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2924                    ],
2925                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2926                  );
2927     }
2928
2929     if ( $section->{'description'} ) {
2930       push @buf, ( ['','-----------'],
2931                    [ $section->{'description'}. ' sub-total',
2932                       $money_char. sprintf("%10.2f", $section->{'subtotal'})
2933                    ],
2934                    [ '', '' ],
2935                    [ '', '' ],
2936                  );
2937     }
2938   
2939   }
2940   
2941   $invoice_data{current_less_finance} =
2942     sprintf('%.2f', $self->charged - $invoice_data{finance_amount} );
2943
2944   if ( $multisection && !$conf->exists('disable_previous_balance')
2945     || $conf->exists('previous_balance-summary_only') )
2946   {
2947     unshift @sections, $previous_section if $pr_total;
2948   }
2949
2950   warn "$me adding taxes\n"
2951     if $DEBUG > 1;
2952
2953   foreach my $tax ( $self->_items_tax ) {
2954
2955     $taxtotal += $tax->{'amount'};
2956
2957     my $description = &$escape_function( $tax->{'description'} );
2958     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
2959
2960     if ( $multisection ) {
2961
2962       my $money = $old_latex ? '' : $money_char;
2963       push @detail_items, {
2964         ext_description => [],
2965         ref          => '',
2966         quantity     => '',
2967         description  => $description,
2968         amount       => $money. $amount,
2969         product_code => '',
2970         section      => $tax_section,
2971       };
2972
2973     } else {
2974
2975       push @total_items, {
2976         'total_item'   => $description,
2977         'total_amount' => $other_money_char. $amount,
2978       };
2979
2980     }
2981
2982     push @buf,[ $description,
2983                 $money_char. $amount,
2984               ];
2985
2986   }
2987   
2988   if ( $taxtotal ) {
2989     my $total = {};
2990     $total->{'total_item'} = 'Sub-total';
2991     $total->{'total_amount'} =
2992       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2993
2994     if ( $multisection ) {
2995       $tax_section->{'subtotal'} = $other_money_char.
2996                                    sprintf('%.2f', $taxtotal);
2997       $tax_section->{'pretotal'} = 'New charges sub-total '.
2998                                    $total->{'total_amount'};
2999       push @sections, $tax_section if $taxtotal;
3000     }else{
3001       unshift @total_items, $total;
3002     }
3003   }
3004   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
3005
3006   push @buf,['','-----------'];
3007   push @buf,[( $conf->exists('disable_previous_balance') 
3008                ? 'Total Charges'
3009                : 'Total New Charges'
3010              ),
3011              $money_char. sprintf("%10.2f",$self->charged) ];
3012   push @buf,['',''];
3013
3014   {
3015     my $total = {};
3016     my $item = 'Total';
3017     $item = $conf->config('previous_balance-exclude_from_total')
3018          || 'Total New Charges'
3019       if $conf->exists('previous_balance-exclude_from_total');
3020     my $amount = $self->charged +
3021                    ( $conf->exists('disable_previous_balance') ||
3022                      $conf->exists('previous_balance-exclude_from_total')
3023                      ? 0
3024                      : $pr_total
3025                    );
3026     $total->{'total_item'} = &$embolden_function($item);
3027     $total->{'total_amount'} =
3028       &$embolden_function( $other_money_char.  sprintf( '%.2f', $amount ) );
3029     if ( $multisection ) {
3030       if ( $adjust_section->{'sort_weight'} ) {
3031         $adjust_section->{'posttotal'} = 'Balance Forward '. $other_money_char.
3032           sprintf("%.2f", ($self->billing_balance || 0) );
3033       } else {
3034         $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
3035                                         sprintf('%.2f', $self->charged );
3036       } 
3037     }else{
3038       push @total_items, $total;
3039     }
3040     push @buf,['','-----------'];
3041     push @buf,[$item,
3042                $money_char.
3043                sprintf( '%10.2f', $amount )
3044               ];
3045     push @buf,['',''];
3046   }
3047   
3048   unless ( $conf->exists('disable_previous_balance') ) {
3049     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
3050   
3051     # credits
3052     my $credittotal = 0;
3053     foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
3054
3055       my $total;
3056       $total->{'total_item'} = &$escape_function($credit->{'description'});
3057       $credittotal += $credit->{'amount'};
3058       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
3059       $adjusttotal += $credit->{'amount'};
3060       if ( $multisection ) {
3061         my $money = $old_latex ? '' : $money_char;
3062         push @detail_items, {
3063           ext_description => [],
3064           ref          => '',
3065           quantity     => '',
3066           description  => &$escape_function($credit->{'description'}),
3067           amount       => $money. $credit->{'amount'},
3068           product_code => '',
3069           section      => $adjust_section,
3070         };
3071       } else {
3072         push @total_items, $total;
3073       }
3074
3075     }
3076     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
3077
3078     #credits (again)
3079     foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
3080       push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
3081     }
3082
3083     # payments
3084     my $paymenttotal = 0;
3085     foreach my $payment ( $self->_items_payments ) {
3086       my $total = {};
3087       $total->{'total_item'} = &$escape_function($payment->{'description'});
3088       $paymenttotal += $payment->{'amount'};
3089       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
3090       $adjusttotal += $payment->{'amount'};
3091       if ( $multisection ) {
3092         my $money = $old_latex ? '' : $money_char;
3093         push @detail_items, {
3094           ext_description => [],
3095           ref          => '',
3096           quantity     => '',
3097           description  => &$escape_function($payment->{'description'}),
3098           amount       => $money. $payment->{'amount'},
3099           product_code => '',
3100           section      => $adjust_section,
3101         };
3102       }else{
3103         push @total_items, $total;
3104       }
3105       push @buf, [ $payment->{'description'},
3106                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
3107                  ];
3108     }
3109     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
3110   
3111     if ( $multisection ) {
3112       $adjust_section->{'subtotal'} = $other_money_char.
3113                                       sprintf('%.2f', $adjusttotal);
3114       push @sections, $adjust_section
3115         unless $adjust_section->{sort_weight};
3116     }
3117
3118     { 
3119       my $total;
3120       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3121       $total->{'total_amount'} =
3122         &$embolden_function(
3123           $other_money_char. sprintf('%.2f', $summarypage 
3124                                                ? $self->charged +
3125                                                  $self->billing_balance
3126                                                : $self->owed + $pr_total
3127                                     )
3128         );
3129       if ( $multisection && !$adjust_section->{sort_weight} ) {
3130         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
3131                                          $total->{'total_amount'};
3132       }else{
3133         push @total_items, $total;
3134       }
3135       push @buf,['','-----------'];
3136       push @buf,[$self->balance_due_msg, $money_char. 
3137         sprintf("%10.2f", $balance_due ) ];
3138     }
3139
3140     if ( $conf->exists('previous_balance-show_credit')
3141         and $cust_main->balance < 0 ) {
3142       my $credit_total = {
3143         'total_item'    => &$embolden_function($self->credit_balance_msg),
3144         'total_amount'  => &$embolden_function(
3145           $other_money_char. sprintf('%.2f', -$cust_main->balance)
3146         ),
3147       };
3148       if ( $multisection ) {
3149         $adjust_section->{'posttotal'} .= $newline_token .
3150           $credit_total->{'total_item'} . ' ' . $credit_total->{'total_amount'};
3151       }
3152       else {
3153         push @total_items, $credit_total;
3154       }
3155       push @buf,['','-----------'];
3156       push @buf,[$self->credit_balance_msg, $money_char. 
3157         sprintf("%10.2f", -$cust_main->balance ) ];
3158     }
3159   }
3160
3161   if ( $multisection ) {
3162     if ($conf->exists('svc_phone_sections')) {
3163       my $total;
3164       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
3165       $total->{'total_amount'} =
3166         &$embolden_function(
3167           $other_money_char. sprintf('%.2f', $self->owed + $pr_total)
3168         );
3169       my $last_section = pop @sections;
3170       $last_section->{'posttotal'} = $total->{'total_item'}. ' '.
3171                                      $total->{'total_amount'};
3172       push @sections, $last_section;
3173     }
3174     push @sections, @$late_sections
3175       if $unsquelched;
3176   }
3177
3178   my @includelist = ();
3179   push @includelist, 'summary' if $summarypage;
3180   foreach my $include ( @includelist ) {
3181
3182     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
3183     my @inc_src;
3184
3185     if ( length( $conf->config($inc_file, $agentnum) ) ) {
3186
3187       @inc_src = $conf->config($inc_file, $agentnum);
3188
3189     } else {
3190
3191       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
3192
3193       my $convert_map = $convert_maps{$format}{$include};
3194
3195       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
3196                        s/--\@\]/$delimiters{$format}[1]/g;
3197                        $_;
3198                      } 
3199                  &$convert_map( $conf->config($inc_file, $agentnum) );
3200
3201     }
3202
3203     my $inc_tt = new Text::Template (
3204       TYPE       => 'ARRAY',
3205       SOURCE     => [ map "$_\n", @inc_src ],
3206       DELIMITERS => $delimiters{$format},
3207     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
3208
3209     unless ( $inc_tt->compile() ) {
3210       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
3211       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
3212       die $error;
3213     }
3214
3215     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
3216
3217     $invoice_data{$include} =~ s/\n+$//
3218       if ($format eq 'latex');
3219   }
3220
3221   $invoice_lines = 0;
3222   my $wasfunc = 0;
3223   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
3224     /invoice_lines\((\d*)\)/;
3225     $invoice_lines += $1 || scalar(@buf);
3226     $wasfunc=1;
3227   }
3228   die "no invoice_lines() functions in template?"
3229     if ( $format eq 'template' && !$wasfunc );
3230
3231   if ($format eq 'template') {
3232
3233     if ( $invoice_lines ) {
3234       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
3235       $invoice_data{'total_pages'}++
3236         if scalar(@buf) % $invoice_lines;
3237     }
3238
3239     #setup subroutine for the template
3240     sub FS::cust_bill::_template::invoice_lines {
3241       my $lines = shift || scalar(@FS::cust_bill::_template::buf);
3242       map { 
3243         scalar(@FS::cust_bill::_template::buf)
3244           ? shift @FS::cust_bill::_template::buf
3245           : [ '', '' ];
3246       }
3247       ( 1 .. $lines );
3248     }
3249
3250     my $lines;
3251     my @collect;
3252     while (@buf) {
3253       push @collect, split("\n",
3254         $text_template->fill_in( HASH => \%invoice_data,
3255                                  PACKAGE => 'FS::cust_bill::_template'
3256                                )
3257       );
3258       $FS::cust_bill::_template::page++;
3259     }
3260     map "$_\n", @collect;
3261   }else{
3262     warn "filling in template for invoice ". $self->invnum. "\n"
3263       if $DEBUG;
3264     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
3265       if $DEBUG > 1;
3266
3267     $text_template->fill_in(HASH => \%invoice_data);
3268   }
3269 }
3270
3271 # helper routine for generating date ranges
3272 sub _prior_month30s {
3273   my $self = shift;
3274   my @ranges = (
3275    [ 1,       2592000 ], # 0-30 days ago
3276    [ 2592000, 5184000 ], # 30-60 days ago
3277    [ 5184000, 7776000 ], # 60-90 days ago
3278    [ 7776000, 0       ], # 90+   days ago
3279   );
3280
3281   map { [ $_->[0] ? $self->_date - $_->[0] - 1 : '',
3282           $_->[1] ? $self->_date - $_->[1] - 1 : '',
3283       ] }
3284   @ranges;
3285 }
3286
3287 =item print_ps HASHREF | [ TIME [ , TEMPLATE ] ]
3288
3289 Returns an postscript invoice, as a scalar.
3290
3291 Options can be passed as a hashref (recommended) or as a list of time, template
3292 and then any key/value pairs for any other options.
3293
3294 I<time> an optional value used to control the printing of overdue messages.  The
3295 default is now.  It isn't the date of the invoice; that's the `_date' field.
3296 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3297 L<Time::Local> and L<Date::Parse> for conversion functions.
3298
3299 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3300
3301 =cut
3302
3303 sub print_ps {
3304   my $self = shift;
3305
3306   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3307   my $ps = generate_ps($file);
3308   unlink($logofile);
3309   unlink($barcodefile);
3310
3311   $ps;
3312 }
3313
3314 =item print_pdf HASHREF | [ TIME [ , TEMPLATE ] ]
3315
3316 Returns an PDF invoice, as a scalar.
3317
3318 Options can be passed as a hashref (recommended) or as a list of time, template
3319 and then any key/value pairs for any other options.
3320
3321 I<time> an optional value used to control the printing of overdue messages.  The
3322 default is now.  It isn't the date of the invoice; that's the `_date' field.
3323 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3324 L<Time::Local> and L<Date::Parse> for conversion functions.
3325
3326 I<template>, if specified, is the name of a suffix for alternate invoices.
3327
3328 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3329
3330 =cut
3331
3332 sub print_pdf {
3333   my $self = shift;
3334
3335   my ($file, $logofile, $barcodefile) = $self->print_latex(@_);
3336   my $pdf = generate_pdf($file);
3337   unlink($logofile);
3338   unlink($barcodefile);
3339
3340   $pdf;
3341 }
3342
3343 =item print_html HASHREF | [ TIME [ , TEMPLATE [ , CID ] ] ]
3344
3345 Returns an HTML invoice, as a scalar.
3346
3347 I<time> an optional value used to control the printing of overdue messages.  The
3348 default is now.  It isn't the date of the invoice; that's the `_date' field.
3349 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
3350 L<Time::Local> and L<Date::Parse> for conversion functions.
3351
3352 I<template>, if specified, is the name of a suffix for alternate invoices.
3353
3354 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
3355
3356 I<cid> is a MIME Content-ID used to create a "cid:" URL for the logo image, used
3357 when emailing the invoice as part of a multipart/related MIME email.
3358
3359 =cut
3360
3361 sub print_html {
3362   my $self = shift;
3363   my %params;
3364   if ( ref($_[0]) ) {
3365     %params = %{ shift() }; 
3366   }else{
3367     $params{'time'} = shift;
3368     $params{'template'} = shift;
3369     $params{'cid'} = shift;
3370   }
3371
3372   $params{'format'} = 'html';
3373   
3374   $self->print_generic( %params );
3375 }
3376
3377 # quick subroutine for print_latex
3378 #
3379 # There are ten characters that LaTeX treats as special characters, which
3380 # means that they do not simply typeset themselves: 
3381 #      # $ % & ~ _ ^ \ { }
3382 #
3383 # TeX ignores blanks following an escaped character; if you want a blank (as
3384 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
3385
3386 sub _latex_escape {
3387   my $value = shift;
3388   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
3389   $value =~ s/([<>])/\$$1\$/g;
3390   $value;
3391 }
3392
3393 sub _html_escape {
3394   my $value = shift;
3395   encode_entities($value);
3396   $value;
3397 }
3398
3399 sub _html_escape_nbsp {
3400   my $value = _html_escape(shift);
3401   $value =~ s/ +/&nbsp;/g;
3402   $value;
3403 }
3404
3405 #utility methods for print_*
3406
3407 sub _translate_old_latex_format {
3408   warn "_translate_old_latex_format called\n"
3409     if $DEBUG; 
3410
3411   my @template = ();
3412   while ( @_ ) {
3413     my $line = shift;
3414   
3415     if ( $line =~ /^%%Detail\s*$/ ) {
3416   
3417       push @template, q![@--!,
3418                       q!  foreach my $_tr_line (@detail_items) {!,
3419                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
3420                       q!      $_tr_line->{'description'} .= !, 
3421                       q!        "\\tabularnewline\n~~".!,
3422                       q!        join( "\\tabularnewline\n~~",!,
3423                       q!          @{$_tr_line->{'ext_description'}}!,
3424                       q!        );!,
3425                       q!    }!;
3426
3427       while ( ( my $line_item_line = shift )
3428               !~ /^%%EndDetail\s*$/                            ) {
3429         $line_item_line =~ s/'/\\'/g;    # nice LTS
3430         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3431         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3432         push @template, "    \$OUT .= '$line_item_line';";
3433       }
3434
3435       push @template, '}',
3436                       '--@]';
3437       #' doh, gvim
3438     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
3439
3440       push @template, '[@--',
3441                       '  foreach my $_tr_line (@total_items) {';
3442
3443       while ( ( my $total_item_line = shift )
3444               !~ /^%%EndTotalDetails\s*$/                      ) {
3445         $total_item_line =~ s/'/\\'/g;    # nice LTS
3446         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
3447         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
3448         push @template, "    \$OUT .= '$total_item_line';";
3449       }
3450
3451       push @template, '}',
3452                       '--@]';
3453
3454     } else {
3455       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
3456       push @template, $line;  
3457     }
3458   
3459   }
3460
3461   if ($DEBUG) {
3462     warn "$_\n" foreach @template;
3463   }
3464
3465   (@template);
3466 }
3467
3468 sub terms {
3469   my $self = shift;
3470
3471   #check for an invoice-specific override
3472   return $self->invoice_terms if $self->invoice_terms;
3473   
3474   #check for a customer- specific override
3475   my $cust_main = $self->cust_main;
3476   return $cust_main->invoice_terms if $cust_main->invoice_terms;
3477
3478   #use configured default
3479   $conf->config('invoice_default_terms') || '';
3480 }
3481
3482 sub due_date {
3483   my $self = shift;
3484   my $duedate = '';
3485   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
3486     $duedate = $self->_date() + ( $1 * 86400 );
3487   }
3488   $duedate;
3489 }
3490
3491 sub due_date2str {
3492   my $self = shift;
3493   $self->due_date ? time2str(shift, $self->due_date) : '';
3494 }
3495
3496 sub balance_due_msg {
3497   my $self = shift;
3498   my $msg = 'Balance Due';
3499   return $msg unless $self->terms;
3500   if ( $self->due_date ) {
3501     $msg .= ' - Please pay by '. $self->due_date2str($date_format);
3502   } elsif ( $self->terms ) {
3503     $msg .= ' - '. $self->terms;
3504   }
3505   $msg;
3506 }
3507
3508 sub balance_due_date {
3509   my $self = shift;
3510   my $duedate = '';
3511   if (    $conf->exists('invoice_default_terms') 
3512        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
3513     $duedate = time2str($rdate_format, $self->_date + ($1*86400) );
3514   }
3515   $duedate;
3516 }
3517
3518 sub credit_balance_msg { 'Credit Balance Remaining' }
3519
3520 =item invnum_date_pretty
3521
3522 Returns a string with the invoice number and date, for example:
3523 "Invoice #54 (3/20/2008)"
3524
3525 =cut
3526
3527 sub invnum_date_pretty {
3528   my $self = shift;
3529   'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
3530 }
3531
3532 =item _date_pretty
3533
3534 Returns a string with the date, for example: "3/20/2008"
3535
3536 =cut
3537
3538 sub _date_pretty {
3539   my $self = shift;
3540   time2str($date_format, $self->_date);
3541 }
3542
3543 use vars qw(%pkg_category_cache);
3544 sub _items_sections {
3545   my $self = shift;
3546   my $late = shift;
3547   my $summarypage = shift;
3548   my $escape = shift;
3549   my $extra_sections = shift;
3550   my $format = shift;
3551
3552   my %subtotal = ();
3553   my %late_subtotal = ();
3554   my %not_tax = ();
3555
3556   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3557   {
3558
3559       my $usage = $cust_bill_pkg->usage;
3560
3561       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
3562         next if ( $display->summary && $summarypage );
3563
3564         my $section = $display->section;
3565         my $type    = $display->type;
3566
3567         $not_tax{$section} = 1
3568           unless $cust_bill_pkg->pkgnum == 0;
3569
3570         if ( $display->post_total && !$summarypage ) {
3571           if (! $type || $type eq 'S') {
3572             $late_subtotal{$section} += $cust_bill_pkg->setup
3573               if $cust_bill_pkg->setup != 0;
3574           }
3575
3576           if (! $type) {
3577             $late_subtotal{$section} += $cust_bill_pkg->recur
3578               if $cust_bill_pkg->recur != 0;
3579           }
3580
3581           if ($type && $type eq 'R') {
3582             $late_subtotal{$section} += $cust_bill_pkg->recur - $usage
3583               if $cust_bill_pkg->recur != 0;
3584           }
3585           
3586           if ($type && $type eq 'U') {
3587             $late_subtotal{$section} += $usage
3588               unless scalar(@$extra_sections);
3589           }
3590
3591         } else {
3592
3593           next if $cust_bill_pkg->pkgnum == 0 && ! $section;
3594
3595           if (! $type || $type eq 'S') {
3596             $subtotal{$section} += $cust_bill_pkg->setup
3597               if $cust_bill_pkg->setup != 0;
3598           }
3599
3600           if (! $type) {
3601             $subtotal{$section} += $cust_bill_pkg->recur
3602               if $cust_bill_pkg->recur != 0;
3603           }
3604
3605           if ($type && $type eq 'R') {
3606             $subtotal{$section} += $cust_bill_pkg->recur - $usage
3607               if $cust_bill_pkg->recur != 0;
3608           }
3609           
3610           if ($type && $type eq 'U') {
3611             $subtotal{$section} += $usage
3612               unless scalar(@$extra_sections);
3613           }
3614
3615         }
3616
3617       }
3618
3619   }
3620
3621   %pkg_category_cache = ();
3622
3623   push @$late, map { { 'description' => &{$escape}($_),
3624                        'subtotal'    => $late_subtotal{$_},
3625                        'post_total'  => 1,
3626                        'sort_weight' => ( _pkg_category($_)
3627                                             ? _pkg_category($_)->weight
3628                                             : 0
3629                                        ),
3630                        ((_pkg_category($_) && _pkg_category($_)->condense)
3631                                            ? $self->_condense_section($format)
3632                                            : ()
3633                        ),
3634                    } }
3635                  sort _sectionsort keys %late_subtotal;
3636
3637   my @sections;
3638   if ( $summarypage ) {
3639     @sections = grep { exists($subtotal{$_}) || ! _pkg_category($_)->disabled }
3640                 map { $_->categoryname } qsearch('pkg_category', {});
3641     push @sections, '' if exists($subtotal{''});
3642   } else {
3643     @sections = keys %subtotal;
3644   }
3645
3646   my @early = map { { 'description' => &{$escape}($_),
3647                       'subtotal'    => $subtotal{$_},
3648                       'summarized'  => $not_tax{$_} ? '' : 'Y',
3649                       'tax_section' => $not_tax{$_} ? '' : 'Y',
3650                       'sort_weight' => ( _pkg_category($_)
3651                                            ? _pkg_category($_)->weight
3652                                            : 0
3653                                        ),
3654                        ((_pkg_category($_) && _pkg_category($_)->condense)
3655                                            ? $self->_condense_section($format)
3656                                            : ()
3657                        ),
3658                     }
3659                   } @sections;
3660   push @early, @$extra_sections if $extra_sections;
3661  
3662   sort { $a->{sort_weight} <=> $b->{sort_weight} } @early;
3663
3664 }
3665
3666 #helper subs for above
3667
3668 sub _sectionsort {
3669   _pkg_category($a)->weight <=> _pkg_category($b)->weight;
3670 }
3671
3672 sub _pkg_category {
3673   my $categoryname = shift;
3674   $pkg_category_cache{$categoryname} ||=
3675     qsearchs( 'pkg_category', { 'categoryname' => $categoryname } );
3676 }
3677
3678 my %condensed_format = (
3679   'label' => [ qw( Description Qty Amount ) ],
3680   'fields' => [
3681                 sub { shift->{description} },
3682                 sub { shift->{quantity} },
3683                 sub { my($href, %opt) = @_;
3684                       ($opt{dollar} || ''). $href->{amount};
3685                     },
3686               ],
3687   'align'  => [ qw( l r r ) ],
3688   'span'   => [ qw( 5 1 1 ) ],            # unitprices?
3689   'width'  => [ qw( 10.7cm 1.4cm 1.6cm ) ],   # don't like this
3690 );
3691
3692 sub _condense_section {
3693   my ( $self, $format ) = ( shift, shift );
3694   ( 'condensed' => 1,
3695     map { my $method = "_condensed_$_"; $_ => $self->$method($format) }
3696       qw( description_generator
3697           header_generator
3698           total_generator
3699           total_line_generator
3700         )
3701   );
3702 }
3703
3704 sub _condensed_generator_defaults {
3705   my ( $self, $format ) = ( shift, shift );
3706   return ( \%condensed_format, ' ', ' ', ' ', sub { shift } );
3707 }
3708
3709 my %html_align = (
3710   'c' => 'center',
3711   'l' => 'left',
3712   'r' => 'right',
3713 );
3714
3715 sub _condensed_header_generator {
3716   my ( $self, $format ) = ( shift, shift );
3717
3718   my ( $f, $prefix, $suffix, $separator, $column ) =
3719     _condensed_generator_defaults($format);
3720
3721   if ($format eq 'latex') {
3722     $prefix = "\\hline\n\\rule{0pt}{2.5ex}\n\\makebox[1.4cm]{}&\n";
3723     $suffix = "\\\\\n\\hline";
3724     $separator = "&\n";
3725     $column =
3726       sub { my ($d,$a,$s,$w) = @_;
3727             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3728           };
3729   } elsif ( $format eq 'html' ) {
3730     $prefix = '<th></th>';
3731     $suffix = '';
3732     $separator = '';
3733     $column =
3734       sub { my ($d,$a,$s,$w) = @_;
3735             return qq!<th align="$html_align{$a}">$d</th>!;
3736       };
3737   }
3738
3739   sub {
3740     my @args = @_;
3741     my @result = ();
3742
3743     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3744       push @result,
3745         &{$column}( map { $f->{$_}->[$i] } qw(label align span width) );
3746     }
3747
3748     $prefix. join($separator, @result). $suffix;
3749   };
3750
3751 }
3752
3753 sub _condensed_description_generator {
3754   my ( $self, $format ) = ( shift, shift );
3755
3756   my ( $f, $prefix, $suffix, $separator, $column ) =
3757     _condensed_generator_defaults($format);
3758
3759   my $money_char = '$';
3760   if ($format eq 'latex') {
3761     $prefix = "\\hline\n\\multicolumn{1}{c}{\\rule{0pt}{2.5ex}~} &\n";
3762     $suffix = '\\\\';
3763     $separator = " & \n";
3764     $column =
3765       sub { my ($d,$a,$s,$w) = @_;
3766             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{\\textbf{$d}}}";
3767           };
3768     $money_char = '\\dollar';
3769   }elsif ( $format eq 'html' ) {
3770     $prefix = '"><td align="center"></td>';
3771     $suffix = '';
3772     $separator = '';
3773     $column =
3774       sub { my ($d,$a,$s,$w) = @_;
3775             return qq!<td align="$html_align{$a}">$d</td>!;
3776       };
3777     #$money_char = $conf->config('money_char') || '$';
3778     $money_char = '';  # this is madness
3779   }
3780
3781   sub {
3782     #my @args = @_;
3783     my $href = shift;
3784     my @result = ();
3785
3786     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3787       my $dollar = '';
3788       $dollar = $money_char if $i == scalar(@{$f->{label}})-1;
3789       push @result,
3790         &{$column}( &{$f->{fields}->[$i]}($href, 'dollar' => $dollar),
3791                     map { $f->{$_}->[$i] } qw(align span width)
3792                   );
3793     }
3794
3795     $prefix. join( $separator, @result ). $suffix;
3796   };
3797
3798 }
3799
3800 sub _condensed_total_generator {
3801   my ( $self, $format ) = ( shift, shift );
3802
3803   my ( $f, $prefix, $suffix, $separator, $column ) =
3804     _condensed_generator_defaults($format);
3805   my $style = '';
3806
3807   if ($format eq 'latex') {
3808     $prefix = "& ";
3809     $suffix = "\\\\\n";
3810     $separator = " & \n";
3811     $column =
3812       sub { my ($d,$a,$s,$w) = @_;
3813             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3814           };
3815   }elsif ( $format eq 'html' ) {
3816     $prefix = '';
3817     $suffix = '';
3818     $separator = '';
3819     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3820     $column =
3821       sub { my ($d,$a,$s,$w) = @_;
3822             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3823       };
3824   }
3825
3826
3827   sub {
3828     my @args = @_;
3829     my @result = ();
3830
3831     #  my $r = &{$f->{fields}->[$i]}(@args);
3832     #  $r .= ' Total' unless $i;
3833
3834     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3835       push @result,
3836         &{$column}( &{$f->{fields}->[$i]}(@args). ($i ? '' : ' Total'),
3837                     map { $f->{$_}->[$i] } qw(align span width)
3838                   );
3839     }
3840
3841     $prefix. join( $separator, @result ). $suffix;
3842   };
3843
3844 }
3845
3846 =item total_line_generator FORMAT
3847
3848 Returns a coderef used for generation of invoice total line items for this
3849 usage_class.  FORMAT is either html or latex
3850
3851 =cut
3852
3853 # should not be used: will have issues with hash element names (description vs
3854 # total_item and amount vs total_amount -- another array of functions?
3855
3856 sub _condensed_total_line_generator {
3857   my ( $self, $format ) = ( shift, shift );
3858
3859   my ( $f, $prefix, $suffix, $separator, $column ) =
3860     _condensed_generator_defaults($format);
3861   my $style = '';
3862
3863   if ($format eq 'latex') {
3864     $prefix = "& ";
3865     $suffix = "\\\\\n";
3866     $separator = " & \n";
3867     $column =
3868       sub { my ($d,$a,$s,$w) = @_;
3869             return "\\multicolumn{$s}{$a}{\\makebox[$w][$a]{$d}}";
3870           };
3871   }elsif ( $format eq 'html' ) {
3872     $prefix = '';
3873     $suffix = '';
3874     $separator = '';
3875     $style = 'border-top: 3px solid #000000;border-bottom: 3px solid #000000;';
3876     $column =
3877       sub { my ($d,$a,$s,$w) = @_;
3878             return qq!<td align="$html_align{$a}" style="$style">$d</td>!;
3879       };
3880   }
3881
3882
3883   sub {
3884     my @args = @_;
3885     my @result = ();
3886
3887     foreach  (my $i = 0; $f->{label}->[$i]; $i++) {
3888       push @result,
3889         &{$column}( &{$f->{fields}->[$i]}(@args),
3890                     map { $f->{$_}->[$i] } qw(align span width)
3891                   );
3892     }
3893
3894     $prefix. join( $separator, @result ). $suffix;
3895   };
3896
3897 }
3898
3899 #sub _items_extra_usage_sections {
3900 #  my $self = shift;
3901 #  my $escape = shift;
3902 #
3903 #  my %sections = ();
3904 #
3905 #  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
3906 #  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
3907 #  {
3908 #    next unless $cust_bill_pkg->pkgnum > 0;
3909 #
3910 #    foreach my $section ( keys %usage_class ) {
3911 #
3912 #      my $usage = $cust_bill_pkg->usage($section);
3913 #
3914 #      next unless $usage && $usage > 0;
3915 #
3916 #      $sections{$section} ||= 0;
3917 #      $sections{$section} += $usage;
3918 #
3919 #    }
3920 #
3921 #  }
3922 #
3923 #  map { { 'description' => &{$escape}($_),
3924 #          'subtotal'    => $sections{$_},
3925 #          'summarized'  => '',
3926 #          'tax_section' => '',
3927 #        }
3928 #      }
3929 #    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
3930 #
3931 #}
3932
3933 sub _items_extra_usage_sections {
3934   my $self = shift;
3935   my $escape = shift;
3936   my $format = shift;
3937
3938   my %sections = ();
3939   my %classnums = ();
3940   my %lines = ();
3941
3942   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
3943   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
3944     next unless $cust_bill_pkg->pkgnum > 0;
3945
3946     foreach my $classnum ( keys %usage_class ) {
3947       my $section = $usage_class{$classnum}->classname;
3948       $classnums{$section} = $classnum;
3949
3950       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
3951         my $amount = $detail->amount;
3952         next unless $amount && $amount > 0;
3953  
3954         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
3955         $sections{$section}{amount} += $amount;  #subtotal
3956         $sections{$section}{calls}++;
3957         $sections{$section}{duration} += $detail->duration;
3958
3959         my $desc = $detail->regionname; 
3960         my $description = $desc;
3961         $description = substr($desc, 0, 50). '...'
3962           if $format eq 'latex' && length($desc) > 50;
3963
3964         $lines{$section}{$desc} ||= {
3965           description     => &{$escape}($description),
3966           #pkgpart         => $part_pkg->pkgpart,
3967           pkgnum          => $cust_bill_pkg->pkgnum,
3968           ref             => '',
3969           amount          => 0,
3970           calls           => 0,
3971           duration        => 0,
3972           #unit_amount     => $cust_bill_pkg->unitrecur,
3973           quantity        => $cust_bill_pkg->quantity,
3974           product_code    => 'N/A',
3975           ext_description => [],
3976         };
3977
3978         $lines{$section}{$desc}{amount} += $amount;
3979         $lines{$section}{$desc}{calls}++;
3980         $lines{$section}{$desc}{duration} += $detail->duration;
3981
3982       }
3983     }
3984   }
3985
3986   my %sectionmap = ();
3987   foreach (keys %sections) {
3988     my $usage_class = $usage_class{$classnums{$_}};
3989     $sectionmap{$_} = { 'description' => &{$escape}($_),
3990                         'amount'    => $sections{$_}{amount},    #subtotal
3991                         'calls'       => $sections{$_}{calls},
3992                         'duration'    => $sections{$_}{duration},
3993                         'summarized'  => '',
3994                         'tax_section' => '',
3995                         'sort_weight' => $usage_class->weight,
3996                         ( $usage_class->format
3997                           ? ( map { $_ => $usage_class->$_($format) }
3998                               qw( description_generator header_generator total_generator total_line_generator )
3999                             )
4000                           : ()
4001                         ), 
4002                       };
4003   }
4004
4005   my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
4006                  values %sectionmap;
4007
4008   my @lines = ();
4009   foreach my $section ( keys %lines ) {
4010     foreach my $line ( keys %{$lines{$section}} ) {
4011       my $l = $lines{$section}{$line};
4012       $l->{section}     = $sectionmap{$section};
4013       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
4014       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4015       push @lines, $l;
4016     }
4017   }
4018
4019   return(\@sections, \@lines);
4020
4021 }
4022
4023 sub _did_summary {
4024     my $self = shift;
4025     my $end = $self->_date;
4026     my $start = $end - 2592000; # 30 days
4027     my $cust_main = $self->cust_main;
4028     my @pkgs = $cust_main->all_pkgs;
4029     my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
4030         = (0,0,0,0,0);
4031     my @seen = ();
4032     foreach my $pkg ( @pkgs ) {
4033         my @h_cust_svc = $pkg->h_cust_svc($end);
4034         foreach my $h_cust_svc ( @h_cust_svc ) {
4035             next if grep {$_ eq $h_cust_svc->svcnum} @seen;
4036             next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
4037
4038             my $inserted = $h_cust_svc->date_inserted;
4039             my $deleted = $h_cust_svc->date_deleted;
4040             my $phone_inserted = $h_cust_svc->h_svc_x($inserted);
4041             my $phone_deleted;
4042             $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
4043             
4044 # DID either activated or ported in; cannot be both for same DID simultaneously
4045             if ($inserted >= $start && $inserted <= $end && $phone_inserted
4046                 && (!$phone_inserted->lnp_status 
4047                     || $phone_inserted->lnp_status eq ''
4048                     || $phone_inserted->lnp_status eq 'native')) {
4049                 $num_activated++;
4050             }
4051             else { # this one not so clean, should probably move to (h_)svc_phone
4052                  my $phone_portedin = qsearchs( 'h_svc_phone',
4053                       { 'svcnum' => $h_cust_svc->svcnum, 
4054                         'lnp_status' => 'portedin' },  
4055                       FS::h_svc_phone->sql_h_searchs($end),  
4056                     );
4057                  $num_portedin++ if $phone_portedin;
4058             }
4059
4060 # DID either deactivated or ported out; cannot be both for same DID simultaneously
4061             if($deleted >= $start && $deleted <= $end && $phone_deleted
4062                 && (!$phone_deleted->lnp_status 
4063                     || $phone_deleted->lnp_status ne 'portingout')) {
4064                 $num_deactivated++;
4065             } 
4066             elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
4067                 && $phone_deleted->lnp_status 
4068                 && $phone_deleted->lnp_status eq 'portingout') {
4069                 $num_portedout++;
4070             }
4071
4072             # increment usage minutes
4073             my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end);
4074             foreach my $cdr ( @cdrs ) {
4075                 $minutes += $cdr->billsec/60;
4076             }
4077
4078             # don't look at this service again
4079             push @seen, $h_cust_svc->svcnum;
4080         }
4081     }
4082
4083     $minutes = sprintf("%d", $minutes);
4084     ("Activated: $num_activated  Ported-In: $num_portedin  Deactivated: "
4085         . "$num_deactivated  Ported-Out: $num_portedout ",
4086             "Total Minutes: $minutes");
4087 }
4088
4089 sub _items_svc_phone_sections {
4090   my $self = shift;
4091   my $escape = shift;
4092   my $format = shift;
4093
4094   my %sections = ();
4095   my %classnums = ();
4096   my %lines = ();
4097
4098   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
4099   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
4100
4101   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
4102     next unless $cust_bill_pkg->pkgnum > 0;
4103
4104     my @header = $cust_bill_pkg->details_header;
4105     next unless scalar(@header);
4106
4107     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
4108
4109       my $phonenum = $detail->phonenum;
4110       next unless $phonenum;
4111
4112       my $amount = $detail->amount;
4113       next unless $amount && $amount > 0;
4114
4115       $sections{$phonenum} ||= { 'amount'      => 0,
4116                                  'calls'       => 0,
4117                                  'duration'    => 0,
4118                                  'sort_weight' => -1,
4119                                  'phonenum'    => $phonenum,
4120                                 };
4121       $sections{$phonenum}{amount} += $amount;  #subtotal
4122       $sections{$phonenum}{calls}++;
4123       $sections{$phonenum}{duration} += $detail->duration;
4124
4125       my $desc = $detail->regionname; 
4126       my $description = $desc;
4127       $description = substr($desc, 0, 50). '...'
4128         if $format eq 'latex' && length($desc) > 50;
4129
4130       $lines{$phonenum}{$desc} ||= {
4131         description     => &{$escape}($description),
4132         #pkgpart         => $part_pkg->pkgpart,
4133         pkgnum          => '',
4134         ref             => '',
4135         amount          => 0,
4136         calls           => 0,
4137         duration        => 0,
4138         #unit_amount     => '',
4139         quantity        => '',
4140         product_code    => 'N/A',
4141         ext_description => [],
4142       };
4143
4144       $lines{$phonenum}{$desc}{amount} += $amount;
4145       $lines{$phonenum}{$desc}{calls}++;
4146       $lines{$phonenum}{$desc}{duration} += $detail->duration;
4147
4148       my $line = $usage_class{$detail->classnum}->classname;
4149       $sections{"$phonenum $line"} ||=
4150         { 'amount' => 0,
4151           'calls' => 0,
4152           'duration' => 0,
4153           'sort_weight' => $usage_class{$detail->classnum}->weight,
4154           'phonenum' => $phonenum,
4155           'header'  => [ @header ],
4156         };
4157       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
4158       $sections{"$phonenum $line"}{calls}++;
4159       $sections{"$phonenum $line"}{duration} += $detail->duration;
4160
4161       $lines{"$phonenum $line"}{$desc} ||= {
4162         description     => &{$escape}($description),
4163         #pkgpart         => $part_pkg->pkgpart,
4164         pkgnum          => '',
4165         ref             => '',
4166         amount          => 0,
4167         calls           => 0,
4168         duration        => 0,
4169         #unit_amount     => '',
4170         quantity        => '',
4171         product_code    => 'N/A',
4172         ext_description => [],
4173       };
4174
4175       $lines{"$phonenum $line"}{$desc}{amount} += $amount;
4176       $lines{"$phonenum $line"}{$desc}{calls}++;
4177       $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
4178       push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
4179            $detail->formatted('format' => $format);
4180
4181     }
4182   }
4183
4184   my %sectionmap = ();
4185   my $simple = new FS::usage_class { format => 'simple' }; #bleh
4186   foreach ( keys %sections ) {
4187     my @header = @{ $sections{$_}{header} || [] };
4188     my $usage_simple =
4189       new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
4190     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
4191     my $usage_class = $summary ? $simple : $usage_simple;
4192     my $ending = $summary ? ' usage charges' : '';
4193     my %gen_opt = ();
4194     unless ($summary) {
4195       $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
4196     }
4197     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
4198                         'amount'    => $sections{$_}{amount},    #subtotal
4199                         'calls'       => $sections{$_}{calls},
4200                         'duration'    => $sections{$_}{duration},
4201                         'summarized'  => '',
4202                         'tax_section' => '',
4203                         'phonenum'    => $sections{$_}{phonenum},
4204                         'sort_weight' => $sections{$_}{sort_weight},
4205                         'post_total'  => $summary, #inspire pagebreak
4206                         (
4207                           ( map { $_ => $usage_class->$_($format, %gen_opt) }
4208                             qw( description_generator
4209                                 header_generator
4210                                 total_generator
4211                                 total_line_generator
4212                               )
4213                           )
4214                         ), 
4215                       };
4216   }
4217
4218   my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
4219                         $a->{sort_weight} <=> $b->{sort_weight}
4220                       }
4221                  values %sectionmap;
4222
4223   my @lines = ();
4224   foreach my $section ( keys %lines ) {
4225     foreach my $line ( keys %{$lines{$section}} ) {
4226       my $l = $lines{$section}{$line};
4227       $l->{section}     = $sectionmap{$section};
4228       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
4229       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
4230       push @lines, $l;
4231     }
4232   }
4233   
4234   if($conf->exists('phone_usage_class_summary')) { 
4235       # this only works with Latex
4236       my @newlines;
4237       my @newsections;
4238
4239       # after this, we'll have only two sections per DID:
4240       # Calls Summary and Calls Detail
4241       foreach my $section ( @sections ) {
4242         if($section->{'post_total'}) {
4243             $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
4244             $section->{'total_line_generator'} = sub { '' };
4245             $section->{'total_generator'} = sub { '' };
4246             $section->{'header_generator'} = sub { '' };
4247             $section->{'description_generator'} = '';
4248             push @newsections, $section;
4249             my %calls_detail = %$section;
4250             $calls_detail{'post_total'} = '';
4251             $calls_detail{'sort_weight'} = '';
4252             $calls_detail{'description_generator'} = sub { '' };
4253             $calls_detail{'header_generator'} = sub {
4254                 return ' & Date/Time & Called Number & Duration & Price'
4255                     if $format eq 'latex';
4256                 '';
4257             };
4258             $calls_detail{'description'} = 'Calls Detail: '
4259                                                     . $section->{'phonenum'};
4260             push @newsections, \%calls_detail;  
4261         }
4262       }
4263
4264       # after this, each usage class is collapsed/summarized into a single
4265       # line under the Calls Summary section
4266       foreach my $newsection ( @newsections ) {
4267         if($newsection->{'post_total'}) { # this means Calls Summary
4268             foreach my $section ( @sections ) {
4269                 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'} 
4270                                 && !$section->{'post_total'});
4271                 my $newdesc = $section->{'description'};
4272                 my $tn = $section->{'phonenum'};
4273                 $newdesc =~ s/$tn//g;
4274                 my $line = {  ext_description => [],
4275                               pkgnum => '',
4276                               ref => '',
4277                               quantity => '',
4278                               calls => $section->{'calls'},
4279                               section => $newsection,
4280                               duration => $section->{'duration'},
4281                               description => $newdesc,
4282                               amount => sprintf("%.2f",$section->{'amount'}),
4283                               product_code => 'N/A',
4284                             };
4285                 push @newlines, $line;
4286             }
4287         }
4288       }
4289
4290       # after this, Calls Details is populated with all CDRs
4291       foreach my $newsection ( @newsections ) {
4292         if(!$newsection->{'post_total'}) { # this means Calls Details
4293             foreach my $line ( @lines ) {
4294                 next unless (scalar(@{$line->{'ext_description'}}) &&
4295                         $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
4296                             );
4297                 my @extdesc = @{$line->{'ext_description'}};
4298                 my @newextdesc;
4299                 foreach my $extdesc ( @extdesc ) {
4300                     $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
4301                     push @newextdesc, $extdesc;
4302                 }
4303                 $line->{'ext_description'} = \@newextdesc;
4304                 $line->{'section'} = $newsection;
4305                 push @newlines, $line;
4306             }
4307         }
4308       }
4309
4310       return(\@newsections, \@newlines);
4311   }
4312
4313   return(\@sections, \@lines);
4314
4315 }
4316
4317 sub _items {
4318   my $self = shift;
4319
4320   #my @display = scalar(@_)
4321   #              ? @_
4322   #              : qw( _items_previous _items_pkg );
4323   #              #: qw( _items_pkg );
4324   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
4325   my @display = qw( _items_previous _items_pkg );
4326
4327   my @b = ();
4328   foreach my $display ( @display ) {
4329     push @b, $self->$display(@_);
4330   }
4331   @b;
4332 }
4333
4334 sub _items_previous {
4335   my $self = shift;
4336   my $cust_main = $self->cust_main;
4337   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
4338   my @b = ();
4339   foreach ( @pr_cust_bill ) {
4340     my $date = $conf->exists('invoice_show_prior_due_date')
4341                ? 'due '. $_->due_date2str($date_format)
4342                : time2str($date_format, $_->_date);
4343     push @b, {
4344       'description' => 'Previous Balance, Invoice #'. $_->invnum. " ($date)",
4345       #'pkgpart'     => 'N/A',
4346       'pkgnum'      => 'N/A',
4347       'amount'      => sprintf("%.2f", $_->owed),
4348     };
4349   }
4350   @b;
4351
4352   #{
4353   #    'description'     => 'Previous Balance',
4354   #    #'pkgpart'         => 'N/A',
4355   #    'pkgnum'          => 'N/A',
4356   #    'amount'          => sprintf("%10.2f", $pr_total ),
4357   #    'ext_description' => [ map {
4358   #                                 "Invoice ". $_->invnum.
4359   #                                 " (". time2str("%x",$_->_date). ") ".
4360   #                                 sprintf("%10.2f", $_->owed)
4361   #                         } @pr_cust_bill ],
4362
4363   #};
4364 }
4365
4366 sub _items_pkg {
4367   my $self = shift;
4368   my %options = @_;
4369
4370   warn "$me _items_pkg searching for all package line items\n"
4371     if $DEBUG > 1;
4372
4373   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
4374
4375   warn "$me _items_pkg filtering line items\n"
4376     if $DEBUG > 1;
4377   my @items = $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4378
4379   if ($options{section} && $options{section}->{condensed}) {
4380
4381     warn "$me _items_pkg condensing section\n"
4382       if $DEBUG > 1;
4383
4384     my %itemshash = ();
4385     local $Storable::canonical = 1;
4386     foreach ( @items ) {
4387       my $item = { %$_ };
4388       delete $item->{ref};
4389       delete $item->{ext_description};
4390       my $key = freeze($item);
4391       $itemshash{$key} ||= 0;
4392       $itemshash{$key} ++; # += $item->{quantity};
4393     }
4394     @items = sort { $a->{description} cmp $b->{description} }
4395              map { my $i = thaw($_);
4396                    $i->{quantity} = $itemshash{$_};
4397                    $i->{amount} =
4398                      sprintf( "%.2f", $i->{quantity} * $i->{amount} );#unit_amount
4399                    $i;
4400                  }
4401              keys %itemshash;
4402   }
4403
4404   warn "$me _items_pkg returning ". scalar(@items). " items\n"
4405     if $DEBUG > 1;
4406
4407   @items;
4408 }
4409
4410 sub _taxsort {
4411   return 0 unless $a->itemdesc cmp $b->itemdesc;
4412   return -1 if $b->itemdesc eq 'Tax';
4413   return 1 if $a->itemdesc eq 'Tax';
4414   return -1 if $b->itemdesc eq 'Other surcharges';
4415   return 1 if $a->itemdesc eq 'Other surcharges';
4416   $a->itemdesc cmp $b->itemdesc;
4417 }
4418
4419 sub _items_tax {
4420   my $self = shift;
4421   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
4422   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
4423 }
4424
4425 sub _items_cust_bill_pkg {
4426   my $self = shift;
4427   my $cust_bill_pkgs = shift;
4428   my %opt = @_;
4429
4430   my $format = $opt{format} || '';
4431   my $escape_function = $opt{escape_function} || sub { shift };
4432   my $format_function = $opt{format_function} || '';
4433   my $unsquelched = $opt{unsquelched} || '';
4434   my $section = $opt{section}->{description} if $opt{section};
4435   my $summary_page = $opt{summary_page} || '';
4436   my $multilocation = $opt{multilocation} || '';
4437   my $multisection = $opt{multisection} || '';
4438   my $discount_show_always = 0;
4439
4440   my @b = ();
4441   my ($s, $r, $u) = ( undef, undef, undef );
4442   foreach my $cust_bill_pkg ( @$cust_bill_pkgs )
4443   {
4444
4445     warn "$me _items_cust_bill_pkg considering cust_bill_pkg $cust_bill_pkg\n"
4446       if $DEBUG > 1;
4447
4448     $discount_show_always = ($cust_bill_pkg->cust_bill_pkg_discount
4449                                 && $conf->exists('discount-show-always'));
4450
4451     foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4452       if ( $_ && !$cust_bill_pkg->hidden ) {
4453         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4454         $_->{amount}      =~ s/^\-0\.00$/0.00/;
4455         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4456         push @b, { %$_ }
4457           unless ( $_->{amount} == 0 && !$discount_show_always );
4458         $_ = undef;
4459       }
4460     }
4461
4462     foreach my $display ( grep { defined($section)
4463                                  ? $_->section eq $section
4464                                  : 1
4465                                }
4466                           #grep { !$_->summary || !$summary_page } # bunk!
4467                           grep { !$_->summary || $multisection }
4468                           $cust_bill_pkg->cust_bill_pkg_display
4469                         )
4470     {
4471
4472       warn "$me _items_cust_bill_pkg considering display item $display\n"
4473         if $DEBUG > 1;
4474
4475       my $type = $display->type;
4476
4477       my $desc = $cust_bill_pkg->desc;
4478       $desc = substr($desc, 0, 50). '...'
4479         if $format eq 'latex' && length($desc) > 50;
4480
4481       my %details_opt = ( 'format'          => $format,
4482                           'escape_function' => $escape_function,
4483                           'format_function' => $format_function,
4484                         );
4485
4486       if ( $cust_bill_pkg->pkgnum > 0 ) {
4487
4488         warn "$me _items_cust_bill_pkg cust_bill_pkg is non-tax\n"
4489           if $DEBUG > 1;
4490  
4491         my $cust_pkg = $cust_bill_pkg->cust_pkg;
4492
4493         if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
4494
4495           warn "$me _items_cust_bill_pkg adding setup\n"
4496             if $DEBUG > 1;
4497
4498           my $description = $desc;
4499           $description .= ' Setup' if $cust_bill_pkg->recur != 0;
4500
4501           my @d = ();
4502           unless ( $cust_pkg->part_pkg->hide_svc_detail
4503                 || $cust_bill_pkg->hidden )
4504           {
4505
4506             push @d, map &{$escape_function}($_),
4507                          $cust_pkg->h_labels_short($self->_date, undef, 'I')
4508               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4509
4510             if ( $multilocation ) {
4511               my $loc = $cust_pkg->location_label;
4512               $loc = substr($loc, 0, 50). '...'
4513                 if $format eq 'latex' && length($loc) > 50;
4514               push @d, &{$escape_function}($loc);
4515             }
4516
4517           }
4518
4519           push @d, $cust_bill_pkg->details(%details_opt)
4520             if $cust_bill_pkg->recur == 0;
4521
4522           if ( $cust_bill_pkg->hidden ) {
4523             $s->{amount}      += $cust_bill_pkg->setup;
4524             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
4525             push @{ $s->{ext_description} }, @d;
4526           } else {
4527             $s = {
4528               description     => $description,
4529               #pkgpart         => $part_pkg->pkgpart,
4530               pkgnum          => $cust_bill_pkg->pkgnum,
4531               amount          => $cust_bill_pkg->setup,
4532               unit_amount     => $cust_bill_pkg->unitsetup,
4533               quantity        => $cust_bill_pkg->quantity,
4534               ext_description => \@d,
4535             };
4536           };
4537
4538         }
4539
4540         if ( ( $cust_bill_pkg->recur != 0  || $cust_bill_pkg->setup == 0 || 
4541                 ($discount_show_always && $cust_bill_pkg->recur == 0) ) &&
4542              ( !$type || $type eq 'R' || $type eq 'U' )
4543            )
4544         {
4545
4546           warn "$me _items_cust_bill_pkg adding recur/usage\n"
4547             if $DEBUG > 1;
4548
4549           my $is_summary = $display->summary;
4550           my $description = ($is_summary && $type && $type eq 'U')
4551                             ? "Usage charges" : $desc;
4552
4553           $description .= " (" . time2str($date_format, $cust_bill_pkg->sdate).
4554                           " - ". time2str($date_format, $cust_bill_pkg->edate).
4555                           ")"
4556             unless $conf->exists('disable_line_item_date_ranges');
4557
4558           my @d = ();
4559
4560           #at least until cust_bill_pkg has "past" ranges in addition to
4561           #the "future" sdate/edate ones... see #3032
4562           my @dates = ( $self->_date );
4563           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
4564           push @dates, $prev->sdate if $prev;
4565           push @dates, undef if !$prev;
4566
4567           unless ( $cust_pkg->part_pkg->hide_svc_detail
4568                 || $cust_bill_pkg->itemdesc
4569                 || $cust_bill_pkg->hidden
4570                 || $is_summary && $type && $type eq 'U' )
4571           {
4572
4573             warn "$me _items_cust_bill_pkg adding service details\n"
4574               if $DEBUG > 1;
4575
4576             push @d, map &{$escape_function}($_),
4577                          $cust_pkg->h_labels_short(@dates, 'I')
4578                                                    #$cust_bill_pkg->edate,
4579                                                    #$cust_bill_pkg->sdate)
4580               unless $cust_bill_pkg->pkgpart_override; #don't redisplay services
4581
4582             warn "$me _items_cust_bill_pkg done adding service details\n"
4583               if $DEBUG > 1;
4584
4585             if ( $multilocation ) {
4586               my $loc = $cust_pkg->location_label;
4587               $loc = substr($loc, 0, 50). '...'
4588                 if $format eq 'latex' && length($loc) > 50;
4589               push @d, &{$escape_function}($loc);
4590             }
4591
4592           }
4593
4594           unless ( $is_summary ) {
4595             warn "$me _items_cust_bill_pkg adding details\n"
4596               if $DEBUG > 1;
4597
4598             #instead of omitting details entirely in this case (unwanted side
4599             # effects), just omit CDRs
4600             $details_opt{'format_function'} = sub { () }
4601               if $type && $type eq 'R';
4602
4603             push @d, $cust_bill_pkg->details(%details_opt);
4604           }
4605
4606           warn "$me _items_cust_bill_pkg calculating amount\n"
4607             if $DEBUG > 1;
4608   
4609           my $amount = 0;
4610           if (!$type) {
4611             $amount = $cust_bill_pkg->recur;
4612           } elsif ($type eq 'R') {
4613             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
4614           } elsif ($type eq 'U') {
4615             $amount = $cust_bill_pkg->usage;
4616           }
4617   
4618           if ( !$type || $type eq 'R' ) {
4619
4620             warn "$me _items_cust_bill_pkg adding recur\n"
4621               if $DEBUG > 1;
4622
4623             if ( $cust_bill_pkg->hidden ) {
4624               $r->{amount}      += $amount;
4625               $r->{unit_amount} += $cust_bill_pkg->unitrecur;
4626               push @{ $r->{ext_description} }, @d;
4627             } else {
4628               $r = {
4629                 description     => $description,
4630                 #pkgpart         => $part_pkg->pkgpart,
4631                 pkgnum          => $cust_bill_pkg->pkgnum,
4632                 amount          => $amount,
4633                 unit_amount     => $cust_bill_pkg->unitrecur,
4634                 quantity        => $cust_bill_pkg->quantity,
4635                 ext_description => \@d,
4636               };
4637             }
4638
4639           } else {  # $type eq 'U'
4640
4641             warn "$me _items_cust_bill_pkg adding usage\n"
4642               if $DEBUG > 1;
4643
4644             if ( $cust_bill_pkg->hidden ) {
4645               $u->{amount}      += $amount;
4646               $u->{unit_amount} += $cust_bill_pkg->unitrecur;
4647               push @{ $u->{ext_description} }, @d;
4648             } else {
4649               $u = {
4650                 description     => $description,
4651                 #pkgpart         => $part_pkg->pkgpart,
4652                 pkgnum          => $cust_bill_pkg->pkgnum,
4653                 amount          => $amount,
4654                 unit_amount     => $cust_bill_pkg->unitrecur,
4655                 quantity        => $cust_bill_pkg->quantity,
4656                 ext_description => \@d,
4657               };
4658             }
4659
4660           }
4661
4662         } # recurring or usage with recurring charge
4663
4664       } else { #pkgnum tax or one-shot line item (??)
4665
4666         warn "$me _items_cust_bill_pkg cust_bill_pkg is tax\n"
4667           if $DEBUG > 1;
4668
4669         if ( $cust_bill_pkg->setup != 0 ) {
4670           push @b, {
4671             'description' => $desc,
4672             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
4673           };
4674         }
4675         if ( $cust_bill_pkg->recur != 0 ) {
4676           push @b, {
4677             'description' => "$desc (".
4678                              time2str($date_format, $cust_bill_pkg->sdate). ' - '.
4679                              time2str($date_format, $cust_bill_pkg->edate). ')',
4680             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
4681           };
4682         }
4683
4684       }
4685
4686     }
4687
4688   }
4689
4690   warn "$me _items_cust_bill_pkg done considering cust_bill_pkgs\n"
4691     if $DEBUG > 1;
4692
4693   foreach ( $s, $r, ($opt{skip_usage} ? () : $u ) ) {
4694     if ( $_  ) {
4695       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
4696       $_->{amount}      =~ s/^\-0\.00$/0.00/;
4697       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
4698       push @b, { %$_ }
4699         unless ( $_->{amount} == 0 && !$discount_show_always );
4700     }
4701   }
4702
4703   @b;
4704
4705 }
4706
4707 sub _items_credits {
4708   my( $self, %opt ) = @_;
4709   my $trim_len = $opt{'trim_len'} || 60;
4710
4711   my @b;
4712   #credits
4713   foreach ( $self->cust_credited ) {
4714
4715     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
4716
4717     my $reason = substr($_->cust_credit->reason, 0, $trim_len);
4718     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
4719     $reason = " ($reason) " if $reason;
4720
4721     push @b, {
4722       #'description' => 'Credit ref\#'. $_->crednum.
4723       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
4724       #                 $reason,
4725       'description' => 'Credit applied '.
4726                        time2str($date_format,$_->cust_credit->_date). $reason,
4727       'amount'      => sprintf("%.2f",$_->amount),
4728     };
4729   }
4730
4731   @b;
4732
4733 }
4734
4735 sub _items_payments {
4736   my $self = shift;
4737
4738   my @b;
4739   #get & print payments
4740   foreach ( $self->cust_bill_pay ) {
4741
4742     #something more elaborate if $_->amount ne ->cust_pay->paid ?
4743
4744     push @b, {
4745       'description' => "Payment received ".
4746                        time2str($date_format,$_->cust_pay->_date ),
4747       'amount'      => sprintf("%.2f", $_->amount )
4748     };
4749   }
4750
4751   @b;
4752
4753 }
4754
4755 =item call_details [ OPTION => VALUE ... ]
4756
4757 Returns an array of CSV strings representing the call details for this invoice
4758 The only option available is the boolean prepend_billed_number
4759
4760 =cut
4761
4762 sub call_details {
4763   my ($self, %opt) = @_;
4764
4765   my $format_function = sub { shift };
4766
4767   if ($opt{prepend_billed_number}) {
4768     $format_function = sub {
4769       my $detail = shift;
4770       my $row = shift;
4771
4772       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
4773       
4774     };
4775   }
4776
4777   my @details = map { $_->details( 'format_function' => $format_function,
4778                                    'escape_function' => sub{ return() },
4779                                  )
4780                     }
4781                   grep { $_->pkgnum }
4782                   $self->cust_bill_pkg;
4783   my $header = $details[0];
4784   ( $header, grep { $_ ne $header } @details );
4785 }
4786
4787
4788 =back
4789
4790 =head1 SUBROUTINES
4791
4792 =over 4
4793
4794 =item process_reprint
4795
4796 =cut
4797
4798 sub process_reprint {
4799   process_re_X('print', @_);
4800 }
4801
4802 =item process_reemail
4803
4804 =cut
4805
4806 sub process_reemail {
4807   process_re_X('email', @_);
4808 }
4809
4810 =item process_refax
4811
4812 =cut
4813
4814 sub process_refax {
4815   process_re_X('fax', @_);
4816 }
4817
4818 =item process_reftp
4819
4820 =cut
4821
4822 sub process_reftp {
4823   process_re_X('ftp', @_);
4824 }
4825
4826 =item respool
4827
4828 =cut
4829
4830 sub process_respool {
4831   process_re_X('spool', @_);
4832 }
4833
4834 use Storable qw(thaw);
4835 use Data::Dumper;
4836 use MIME::Base64;
4837 sub process_re_X {
4838   my( $method, $job ) = ( shift, shift );
4839   warn "$me process_re_X $method for job $job\n" if $DEBUG;
4840
4841   my $param = thaw(decode_base64(shift));
4842   warn Dumper($param) if $DEBUG;
4843
4844   re_X(
4845     $method,
4846     $job,
4847     %$param,
4848   );
4849
4850 }
4851
4852 sub re_X {
4853   my($method, $job, %param ) = @_;
4854   if ( $DEBUG ) {
4855     warn "re_X $method for job $job with param:\n".
4856          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
4857   }
4858
4859   #some false laziness w/search/cust_bill.html
4860   my $distinct = '';
4861   my $orderby = 'ORDER BY cust_bill._date';
4862
4863   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
4864
4865   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
4866      
4867   my @cust_bill = qsearch( {
4868     #'select'    => "cust_bill.*",
4869     'table'     => 'cust_bill',
4870     'addl_from' => $addl_from,
4871     'hashref'   => {},
4872     'extra_sql' => $extra_sql,
4873     'order_by'  => $orderby,
4874     'debug' => 1,
4875   } );
4876
4877   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
4878
4879   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
4880     if $DEBUG;
4881
4882   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
4883   foreach my $cust_bill ( @cust_bill ) {
4884     $cust_bill->$method();
4885
4886     if ( $job ) { #progressbar foo
4887       $num++;
4888       if ( time - $min_sec > $last ) {
4889         my $error = $job->update_statustext(
4890           int( 100 * $num / scalar(@cust_bill) )
4891         );
4892         die $error if $error;
4893         $last = time;
4894       }
4895     }
4896
4897   }
4898
4899 }
4900
4901 =back
4902
4903 =head1 CLASS METHODS
4904
4905 =over 4
4906
4907 =item owed_sql
4908
4909 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
4910
4911 =cut
4912
4913 sub owed_sql {
4914   my ($class, $start, $end) = @_;
4915   'charged - '. 
4916     $class->paid_sql($start, $end). ' - '. 
4917     $class->credited_sql($start, $end);
4918 }
4919
4920 =item net_sql
4921
4922 Returns an SQL fragment to retreive the net amount (charged minus credited).
4923
4924 =cut
4925
4926 sub net_sql {
4927   my ($class, $start, $end) = @_;
4928   'charged - '. $class->credited_sql($start, $end);
4929 }
4930
4931 =item paid_sql
4932
4933 Returns an SQL fragment to retreive the amount paid against this invoice.
4934
4935 =cut
4936
4937 sub paid_sql {
4938   my ($class, $start, $end) = @_;
4939   $start &&= "AND cust_bill_pay._date <= $start";
4940   $end   &&= "AND cust_bill_pay._date > $end";
4941   $start = '' unless defined($start);
4942   $end   = '' unless defined($end);
4943   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
4944        WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
4945 }
4946
4947 =item credited_sql
4948
4949 Returns an SQL fragment to retreive the amount credited against this invoice.
4950
4951 =cut
4952
4953 sub credited_sql {
4954   my ($class, $start, $end) = @_;
4955   $start &&= "AND cust_credit_bill._date <= $start";
4956   $end   &&= "AND cust_credit_bill._date >  $end";
4957   $start = '' unless defined($start);
4958   $end   = '' unless defined($end);
4959   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
4960        WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
4961 }
4962
4963 =item due_date_sql
4964
4965 Returns an SQL fragment to retrieve the due date of an invoice.
4966 Currently only supported on PostgreSQL.
4967
4968 =cut
4969
4970 sub due_date_sql {
4971 'COALESCE(
4972   SUBSTRING(
4973     COALESCE(
4974       cust_bill.invoice_terms,
4975       cust_main.invoice_terms,
4976       \''.($conf->config('invoice_default_terms') || '').'\'
4977     ), E\'Net (\\\\d+)\'
4978   )::INTEGER, 0
4979 ) * 86400 + cust_bill._date'
4980 }
4981
4982 =item search_sql_where HASHREF
4983
4984 Class method which returns an SQL WHERE fragment to search for parameters
4985 specified in HASHREF.  Valid parameters are
4986
4987 =over 4
4988
4989 =item _date
4990
4991 List reference of start date, end date, as UNIX timestamps.
4992
4993 =item invnum_min
4994
4995 =item invnum_max
4996
4997 =item agentnum
4998
4999 =item charged
5000
5001 List reference of charged limits (exclusive).
5002
5003 =item owed
5004
5005 List reference of charged limits (exclusive).
5006
5007 =item open
5008
5009 flag, return open invoices only
5010
5011 =item net
5012
5013 flag, return net invoices only
5014
5015 =item days
5016
5017 =item newest_percust
5018
5019 =back
5020
5021 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
5022
5023 =cut
5024
5025 sub search_sql_where {
5026   my($class, $param) = @_;
5027   if ( $DEBUG ) {
5028     warn "$me search_sql_where called with params: \n".
5029          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
5030   }
5031
5032   my @search = ();
5033
5034   #agentnum
5035   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
5036     push @search, "cust_main.agentnum = $1";
5037   }
5038
5039   #_date
5040   if ( $param->{_date} ) {
5041     my($beginning, $ending) = @{$param->{_date}};
5042
5043     push @search, "cust_bill._date >= $beginning",
5044                   "cust_bill._date <  $ending";
5045   }
5046
5047   #invnum
5048   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
5049     push @search, "cust_bill.invnum >= $1";
5050   }
5051   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
5052     push @search, "cust_bill.invnum <= $1";
5053   }
5054
5055   #charged
5056   if ( $param->{charged} ) {
5057     my @charged = ref($param->{charged})
5058                     ? @{ $param->{charged} }
5059                     : ($param->{charged});
5060
5061     push @search, map { s/^charged/cust_bill.charged/; $_; }
5062                       @charged;
5063   }
5064
5065   my $owed_sql = FS::cust_bill->owed_sql;
5066
5067   #owed
5068   if ( $param->{owed} ) {
5069     my @owed = ref($param->{owed})
5070                  ? @{ $param->{owed} }
5071                  : ($param->{owed});
5072     push @search, map { s/^owed/$owed_sql/; $_; }
5073                       @owed;
5074   }
5075
5076   #open/net flags
5077   push @search, "0 != $owed_sql"
5078     if $param->{'open'};
5079   push @search, '0 != '. FS::cust_bill->net_sql
5080     if $param->{'net'};
5081
5082   #days
5083   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
5084     if $param->{'days'};
5085
5086   #newest_percust
5087   if ( $param->{'newest_percust'} ) {
5088
5089     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
5090     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
5091
5092     my @newest_where = map { my $x = $_;
5093                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
5094                              $x;
5095                            }
5096                            grep ! /^cust_main./, @search;
5097     my $newest_where = scalar(@newest_where)
5098                          ? ' AND '. join(' AND ', @newest_where)
5099                          : '';
5100
5101
5102     push @search, "cust_bill._date = (
5103       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
5104         WHERE newest_cust_bill.custnum = cust_bill.custnum
5105           $newest_where
5106     )";
5107
5108   }
5109
5110   #agent virtualization
5111   my $curuser = $FS::CurrentUser::CurrentUser;
5112   if ( $curuser->username eq 'fs_queue'
5113        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
5114     my $username = $1;
5115     my $newuser = qsearchs('access_user', {
5116       'username' => $username,
5117       'disabled' => '',
5118     } );
5119     if ( $newuser ) {
5120       $curuser = $newuser;
5121     } else {
5122       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
5123     }
5124   }
5125   push @search, $curuser->agentnums_sql;
5126
5127   join(' AND ', @search );
5128
5129 }
5130
5131 =back
5132
5133 =head1 BUGS
5134
5135 The delete method.
5136
5137 =head1 SEE ALSO
5138
5139 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
5140 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
5141 documentation.
5142
5143 =cut
5144
5145 1;
5146