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