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