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