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