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