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