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