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