improve emailed cdr csv file (#5727 again)
[freeside.git] / FS / FS / cust_bill.pm
1 package FS::cust_bill;
2
3 use strict;
4 use vars qw( @ISA $DEBUG $me $conf $money_char );
5 use vars qw( $invoice_lines @buf ); #yuck
6 use Fcntl qw(:flock); #for spool_csv
7 use List::Util qw(min max);
8 use Date::Format;
9 use Text::Template 1.20;
10 use File::Temp 0.14;
11 use String::ShellQuote;
12 use HTML::Entities;
13 use Locale::Country;
14 use FS::UID qw( datasrc );
15 use FS::Misc qw( send_email send_fax generate_ps generate_pdf do_print );
16 use FS::Record qw( qsearch qsearchs dbh );
17 use FS::cust_main_Mixin;
18 use FS::cust_main;
19 use FS::cust_bill_pkg;
20 use FS::cust_bill_pkg_display;
21 use FS::cust_credit;
22 use FS::cust_pay;
23 use FS::cust_pkg;
24 use FS::cust_credit_bill;
25 use FS::pay_batch;
26 use FS::cust_pay_batch;
27 use FS::cust_bill_event;
28 use FS::cust_event;
29 use FS::part_pkg;
30 use FS::cust_bill_pay;
31 use FS::cust_bill_pay_batch;
32 use FS::part_bill_event;
33 use FS::payby;
34
35 @ISA = qw( FS::cust_main_Mixin FS::Record );
36
37 $DEBUG = 0;
38 $me = '[FS::cust_bill]';
39
40 #ask FS::UID to run this stuff for us later
41 FS::UID->install_callback( sub { 
42   $conf = new FS::Conf;
43   $money_char = $conf->config('money_char') || '$';  
44 } );
45
46 =head1 NAME
47
48 FS::cust_bill - Object methods for cust_bill records
49
50 =head1 SYNOPSIS
51
52   use FS::cust_bill;
53
54   $record = new FS::cust_bill \%hash;
55   $record = new FS::cust_bill { 'column' => 'value' };
56
57   $error = $record->insert;
58
59   $error = $new_record->replace($old_record);
60
61   $error = $record->delete;
62
63   $error = $record->check;
64
65   ( $total_previous_balance, @previous_cust_bill ) = $record->previous;
66
67   @cust_bill_pkg_objects = $cust_bill->cust_bill_pkg;
68
69   ( $total_previous_credits, @previous_cust_credit ) = $record->cust_credit;
70
71   @cust_pay_objects = $cust_bill->cust_pay;
72
73   $tax_amount = $record->tax;
74
75   @lines = $cust_bill->print_text;
76   @lines = $cust_bill->print_text $time;
77
78 =head1 DESCRIPTION
79
80 An FS::cust_bill object represents an invoice; a declaration that a customer
81 owes you money.  The specific charges are itemized as B<cust_bill_pkg> records
82 (see L<FS::cust_bill_pkg>).  FS::cust_bill inherits from FS::Record.  The
83 following fields are currently supported:
84
85 =over 4
86
87 =item invnum - primary key (assigned automatically for new invoices)
88
89 =item custnum - customer (see L<FS::cust_main>)
90
91 =item _date - specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
92 L<Time::Local> and L<Date::Parse> for conversion functions.
93
94 =item charged - amount of this invoice
95
96 =item printed - deprecated
97
98 =item closed - books closed flag, empty or `Y'
99
100 =back
101
102 =head1 METHODS
103
104 =over 4
105
106 =item new HASHREF
107
108 Creates a new invoice.  To add the invoice to the database, see L<"insert">.
109 Invoices are normally created by calling the bill method of a customer object
110 (see L<FS::cust_main>).
111
112 =cut
113
114 sub table { 'cust_bill'; }
115
116 sub cust_linked { $_[0]->cust_main_custnum; } 
117 sub cust_unlinked_msg {
118   my $self = shift;
119   "WARNING: can't find cust_main.custnum ". $self->custnum.
120   ' (cust_bill.invnum '. $self->invnum. ')';
121 }
122
123 =item insert
124
125 Adds this invoice to the database ("Posts" the invoice).  If there is an error,
126 returns the error, otherwise returns false.
127
128 =item delete
129
130 This method now works but you probably shouldn't use it.  Instead, apply a
131 credit against the invoice.
132
133 Using this method to delete invoices outright is really, really bad.  There
134 would be no record you ever posted this invoice, and there are no check to
135 make sure charged = 0 or that there are no associated cust_bill_pkg records.
136
137 Really, don't use it.
138
139 =cut
140
141 sub delete {
142   my $self = shift;
143   return "Can't delete closed invoice" if $self->closed =~ /^Y/i;
144   $self->SUPER::delete(@_);
145 }
146
147 =item replace OLD_RECORD
148
149 Replaces the OLD_RECORD with this one in the database.  If there is an error,
150 returns the error, otherwise returns false.
151
152 Only printed may be changed.  printed is normally updated by calling the
153 collect method of a customer object (see L<FS::cust_main>).
154
155 =cut
156
157 #replace can be inherited from Record.pm
158
159 # replace_check is now the preferred way to #implement replace data checks
160 # (so $object->replace() works without an argument)
161
162 sub replace_check {
163   my( $new, $old ) = ( shift, shift );
164   return "Can't change custnum!" unless $old->custnum == $new->custnum;
165   #return "Can't change _date!" unless $old->_date eq $new->_date;
166   return "Can't change _date!" unless $old->_date == $new->_date;
167   return "Can't change charged!" unless $old->charged == $new->charged
168                                      || $old->charged == 0;
169
170   '';
171 }
172
173 =item check
174
175 Checks all fields to make sure this is a valid invoice.  If there is an error,
176 returns the error, otherwise returns false.  Called by the insert and replace
177 methods.
178
179 =cut
180
181 sub check {
182   my $self = shift;
183
184   my $error =
185     $self->ut_numbern('invnum')
186     || $self->ut_number('custnum')
187     || $self->ut_numbern('_date')
188     || $self->ut_money('charged')
189     || $self->ut_numbern('printed')
190     || $self->ut_enum('closed', [ '', 'Y' ])
191   ;
192   return $error if $error;
193
194   return "Unknown customer"
195     unless qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
196
197   $self->_date(time) unless $self->_date;
198
199   $self->printed(0) if $self->printed eq '';
200
201   $self->SUPER::check;
202 }
203
204 =item previous
205
206 Returns a list consisting of the total previous balance for this customer, 
207 followed by the previous outstanding invoices (as FS::cust_bill objects also).
208
209 =cut
210
211 sub previous {
212   my $self = shift;
213   my $total = 0;
214   my @cust_bill = sort { $a->_date <=> $b->_date }
215     grep { $_->owed != 0 && $_->_date < $self->_date }
216       qsearch( 'cust_bill', { 'custnum' => $self->custnum } ) 
217   ;
218   foreach ( @cust_bill ) { $total += $_->owed; }
219   $total, @cust_bill;
220 }
221
222 =item cust_bill_pkg
223
224 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice.
225
226 =cut
227
228 sub cust_bill_pkg {
229   my $self = shift;
230   qsearch(
231     { 'table'    => 'cust_bill_pkg',
232       'hashref'  => { 'invnum' => $self->invnum },
233       'order_by' => 'ORDER BY billpkgnum',
234     }
235   );
236 }
237
238 =item cust_bill_pkg_pkgnum PKGNUM
239
240 Returns the line items (see L<FS::cust_bill_pkg>) for this invoice and
241 specified pkgnum.
242
243 =cut
244
245 sub cust_bill_pkg_pkgnum {
246   my( $self, $pkgnum ) = @_;
247   qsearch(
248     { 'table'    => 'cust_bill_pkg',
249       'hashref'  => { 'invnum' => $self->invnum,
250                       'pkgnum' => $pkgnum,
251                     },
252       'order_by' => 'ORDER BY billpkgnum',
253     }
254   );
255 }
256
257 =item cust_pkg
258
259 Returns the packages (see L<FS::cust_pkg>) corresponding to the line items for
260 this invoice.
261
262 =cut
263
264 sub cust_pkg {
265   my $self = shift;
266   my @cust_pkg = map { $_->cust_pkg } $self->cust_bill_pkg;
267   my %saw = ();
268   grep { ! $saw{$_->pkgnum}++ } @cust_pkg;
269 }
270
271 =item open_cust_bill_pkg
272
273 Returns the open line items for this invoice.
274
275 Note that cust_bill_pkg with both setup and recur fees are returned as two
276 separate line items, each with only one fee.
277
278 =cut
279
280 # modeled after cust_main::open_cust_bill
281 sub open_cust_bill_pkg {
282   my $self = shift;
283
284   # grep { $_->owed > 0 } $self->cust_bill_pkg
285
286   my %other = ( 'recur' => 'setup',
287                 'setup' => 'recur', );
288   my @open = ();
289   foreach my $field ( qw( recur setup )) {
290     push @open, map  { $_->set( $other{$field}, 0 ); $_; }
291                 grep { $_->owed($field) > 0 }
292                 $self->cust_bill_pkg;
293   }
294
295   @open;
296 }
297
298 =item cust_bill_event
299
300 Returns the completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
301
302 =cut
303
304 sub cust_bill_event {
305   my $self = shift;
306   qsearch( 'cust_bill_event', { 'invnum' => $self->invnum } );
307 }
308
309 =item num_cust_bill_event
310
311 Returns the number of completed invoice events (deprecated, old-style events - see L<FS::cust_bill_event>) for this invoice.
312
313 =cut
314
315 sub num_cust_bill_event {
316   my $self = shift;
317   my $sql =
318     "SELECT COUNT(*) FROM cust_bill_event WHERE invnum = ?";
319   my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql"; 
320   $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
321   $sth->fetchrow_arrayref->[0];
322 }
323
324 =item cust_event
325
326 Returns the new-style customer billing events (see L<FS::cust_event>) for this invoice.
327
328 =cut
329
330 #false laziness w/cust_pkg.pm
331 sub cust_event {
332   my $self = shift;
333   qsearch({
334     'table'     => 'cust_event',
335     'addl_from' => 'JOIN part_event USING ( eventpart )',
336     'hashref'   => { 'tablenum' => $self->invnum },
337     'extra_sql' => " AND eventtable = 'cust_bill' ",
338   });
339 }
340
341 =item num_cust_event
342
343 Returns the number of new-style customer billing events (see L<FS::cust_event>) for this invoice.
344
345 =cut
346
347 #false laziness w/cust_pkg.pm
348 sub num_cust_event {
349   my $self = shift;
350   my $sql =
351     "SELECT COUNT(*) FROM cust_event JOIN part_event USING ( eventpart ) ".
352     "  WHERE tablenum = ? AND eventtable = 'cust_bill'";
353   my $sth = dbh->prepare($sql) or die  dbh->errstr. " preparing $sql"; 
354   $sth->execute($self->invnum) or die $sth->errstr. " executing $sql";
355   $sth->fetchrow_arrayref->[0];
356 }
357
358 =item cust_main
359
360 Returns the customer (see L<FS::cust_main>) for this invoice.
361
362 =cut
363
364 sub cust_main {
365   my $self = shift;
366   qsearchs( 'cust_main', { 'custnum' => $self->custnum } );
367 }
368
369 =item cust_suspend_if_balance_over AMOUNT
370
371 Suspends the customer associated with this invoice if the total amount owed on
372 this invoice and all older invoices is greater than the specified amount.
373
374 Returns a list: an empty list on success or a list of errors.
375
376 =cut
377
378 sub cust_suspend_if_balance_over {
379   my( $self, $amount ) = ( shift, shift );
380   my $cust_main = $self->cust_main;
381   if ( $cust_main->total_owed_date($self->_date) < $amount ) {
382     return ();
383   } else {
384     $cust_main->suspend(@_);
385   }
386 }
387
388 =item cust_credit
389
390 Depreciated.  See the cust_credited method.
391
392  #Returns a list consisting of the total previous credited (see
393  #L<FS::cust_credit>) and unapplied for this customer, followed by the previous
394  #outstanding credits (FS::cust_credit objects).
395
396 =cut
397
398 sub cust_credit {
399   use Carp;
400   croak "FS::cust_bill->cust_credit depreciated; see ".
401         "FS::cust_bill->cust_credit_bill";
402   #my $self = shift;
403   #my $total = 0;
404   #my @cust_credit = sort { $a->_date <=> $b->_date }
405   #  grep { $_->credited != 0 && $_->_date < $self->_date }
406   #    qsearch('cust_credit', { 'custnum' => $self->custnum } )
407   #;
408   #foreach (@cust_credit) { $total += $_->credited; }
409   #$total, @cust_credit;
410 }
411
412 =item cust_pay
413
414 Depreciated.  See the cust_bill_pay method.
415
416 #Returns all payments (see L<FS::cust_pay>) for this invoice.
417
418 =cut
419
420 sub cust_pay {
421   use Carp;
422   croak "FS::cust_bill->cust_pay depreciated; see FS::cust_bill->cust_bill_pay";
423   #my $self = shift;
424   #sort { $a->_date <=> $b->_date }
425   #  qsearch( 'cust_pay', { 'invnum' => $self->invnum } )
426   #;
427 }
428
429 =item cust_bill_pay
430
431 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice.
432
433 =cut
434
435 sub cust_bill_pay {
436   my $self = shift;
437   sort { $a->_date <=> $b->_date }
438     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum } );
439 }
440
441 =item cust_credited
442
443 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice.
444
445 =cut
446
447 sub cust_credited {
448   my $self = shift;
449   sort { $a->_date <=> $b->_date }
450     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum } )
451   ;
452 }
453
454 =item cust_bill_pay_pkgnum
455
456 Returns all payment applications (see L<FS::cust_bill_pay>) for this invoice
457 with matching pkgnum.
458
459 =cut
460
461 sub cust_bill_pay_pkgnum {
462   my( $self, $pkgnum ) = @_;
463   sort { $a->_date <=> $b->_date }
464     qsearch( 'cust_bill_pay', { 'invnum' => $self->invnum,
465                                 'pkgnum' => $pkgnum,
466                               }
467            );
468 }
469
470 =item cust_credited_pkgnum
471
472 Returns all applied credits (see L<FS::cust_credit_bill>) for this invoice
473 with matching pkgnum.
474
475 =cut
476
477 sub cust_credited_pkgnum {
478   my( $self, $pkgnum ) = @_;
479   sort { $a->_date <=> $b->_date }
480     qsearch( 'cust_credit_bill', { 'invnum' => $self->invnum,
481                                    'pkgnum' => $pkgnum,
482                                  }
483            );
484 }
485
486 =item tax
487
488 Returns the tax amount (see L<FS::cust_bill_pkg>) for this invoice.
489
490 =cut
491
492 sub tax {
493   my $self = shift;
494   my $total = 0;
495   my @taxlines = qsearch( 'cust_bill_pkg', { 'invnum' => $self->invnum ,
496                                              'pkgnum' => 0 } );
497   foreach (@taxlines) { $total += $_->setup; }
498   $total;
499 }
500
501 =item owed
502
503 Returns the amount owed (still outstanding) on this invoice, which is charged
504 minus all payment applications (see L<FS::cust_bill_pay>) and credit
505 applications (see L<FS::cust_credit_bill>).
506
507 =cut
508
509 sub owed {
510   my $self = shift;
511   my $balance = $self->charged;
512   $balance -= $_->amount foreach ( $self->cust_bill_pay );
513   $balance -= $_->amount foreach ( $self->cust_credited );
514   $balance = sprintf( "%.2f", $balance);
515   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
516   $balance;
517 }
518
519 sub owed_pkgnum {
520   my( $self, $pkgnum ) = @_;
521
522   #my $balance = $self->charged;
523   my $balance = 0;
524   $balance += $_->setup + $_->recur for $self->cust_bill_pkg_pkgnum($pkgnum);
525
526   $balance -= $_->amount            for $self->cust_bill_pay_pkgnum($pkgnum);
527   $balance -= $_->amount            for $self->cust_credited_pkgnum($pkgnum);
528
529   $balance = sprintf( "%.2f", $balance);
530   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
531   $balance;
532 }
533
534 =item apply_payments_and_credits
535
536 =cut
537
538 sub apply_payments_and_credits {
539   my $self = shift;
540
541   local $SIG{HUP} = 'IGNORE';
542   local $SIG{INT} = 'IGNORE';
543   local $SIG{QUIT} = 'IGNORE';
544   local $SIG{TERM} = 'IGNORE';
545   local $SIG{TSTP} = 'IGNORE';
546   local $SIG{PIPE} = 'IGNORE';
547
548   my $oldAutoCommit = $FS::UID::AutoCommit;
549   local $FS::UID::AutoCommit = 0;
550   my $dbh = dbh;
551
552   $self->select_for_update; #mutex
553
554   my @payments = grep { $_->unapplied > 0 } $self->cust_main->cust_pay;
555   my @credits  = grep { $_->credited > 0 } $self->cust_main->cust_credit;
556
557   if ( $conf->exists('pkg-balances') ) {
558     # limit @payments & @credits to those w/ a pkgnum grepped from $self
559     my %pkgnums = map { $_ => 1 } map $_->pkgnum, $self->cust_bill_pkg;
560     @payments = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @payments;
561     @credits  = grep { ! $_->pkgnum || $pkgnums{$_->pkgnum} } @credits;
562   }
563
564   while ( $self->owed > 0 and ( @payments || @credits ) ) {
565
566     my $app = '';
567     if ( @payments && @credits ) {
568
569       #decide which goes first by weight of top (unapplied) line item
570
571       my @open_lineitems = $self->open_cust_bill_pkg;
572
573       my $max_pay_weight =
574         max( map  { $_->part_pkg->pay_weight || 0 }
575              grep { $_ }
576              map  { $_->cust_pkg }
577                   @open_lineitems
578            );
579       my $max_credit_weight =
580         max( map  { $_->part_pkg->credit_weight || 0 }
581              grep { $_ } 
582              map  { $_->cust_pkg }
583                   @open_lineitems
584            );
585
586       #if both are the same... payments first?  it has to be something
587       if ( $max_pay_weight >= $max_credit_weight ) {
588         $app = 'pay';
589       } else {
590         $app = 'credit';
591       }
592     
593     } elsif ( @payments ) {
594       $app = 'pay';
595     } elsif ( @credits ) {
596       $app = 'credit';
597     } else {
598       die "guru meditation #12 and 35";
599     }
600
601     my $unapp_amount;
602     if ( $app eq 'pay' ) {
603
604       my $payment = shift @payments;
605       $unapp_amount = $payment->unapplied;
606       $app = new FS::cust_bill_pay { 'paynum'  => $payment->paynum };
607       $app->pkgnum( $payment->pkgnum )
608         if $conf->exists('pkg-balances') && $payment->pkgnum;
609
610     } elsif ( $app eq 'credit' ) {
611
612       my $credit = shift @credits;
613       $unapp_amount = $credit->credited;
614       $app = new FS::cust_credit_bill { 'crednum' => $credit->crednum };
615       $app->pkgnum( $credit->pkgnum )
616         if $conf->exists('pkg-balances') && $credit->pkgnum;
617
618     } else {
619       die "guru meditation #12 and 35";
620     }
621
622     my $owed;
623     if ( $conf->exists('pkg-balances') && $app->pkgnum ) {
624       warn "owed_pkgnum ". $app->pkgnum;
625       $owed = $self->owed_pkgnum($app->pkgnum);
626     } else {
627       $owed = $self->owed;
628     }
629     next unless $owed > 0;
630
631     warn "min ( $unapp_amount, $owed )\n";
632     $app->amount( sprintf('%.2f', min( $unapp_amount, $owed ) ) );
633
634     $app->invnum( $self->invnum );
635
636     my $error = $app->insert;
637     if ( $error ) {
638       $dbh->rollback if $oldAutoCommit;
639       return "Error inserting ". $app->table. " record: $error";
640     }
641     die $error if $error;
642
643   }
644
645   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
646   ''; #no error
647
648 }
649
650 =item generate_email OPTION => VALUE ...
651
652 Options:
653
654 =over 4
655
656 =item from
657
658 sender address, required
659
660 =item tempate
661
662 alternate template name, optional
663
664 =item print_text
665
666 text attachment arrayref, optional
667
668 =item subject
669
670 email subject, optional
671
672 =back
673
674 Returns an argument list to be passed to L<FS::Misc::send_email>.
675
676 =cut
677
678 use MIME::Entity;
679
680 sub generate_email {
681
682   my $self = shift;
683   my %args = @_;
684
685   my $me = '[FS::cust_bill::generate_email]';
686
687   my %return = (
688     'from'      => $args{'from'},
689     'subject'   => (($args{'subject'}) ? $args{'subject'} : 'Invoice'),
690   );
691
692   my %cdrs = ( 'unsquelch_cdr' => $conf->exists('voip-cdr_email') );
693
694   if (ref($args{'to'}) eq 'ARRAY') {
695     $return{'to'} = $args{'to'};
696   } else {
697     $return{'to'} = [ grep { $_ !~ /^(POST|FAX)$/ }
698                            $self->cust_main->invoicing_list
699                     ];
700   }
701
702   if ( $conf->exists('invoice_html') ) {
703
704     warn "$me creating HTML/text multipart message"
705       if $DEBUG;
706
707     $return{'nobody'} = 1;
708
709     my $alternative = build MIME::Entity
710       'Type'        => 'multipart/alternative',
711       'Encoding'    => '7bit',
712       'Disposition' => 'inline'
713     ;
714
715     my $data;
716     if ( $conf->exists('invoice_email_pdf')
717          and scalar($conf->config('invoice_email_pdf_note')) ) {
718
719       warn "$me using 'invoice_email_pdf_note' in multipart message"
720         if $DEBUG;
721       $data = [ map { $_ . "\n" }
722                     $conf->config('invoice_email_pdf_note')
723               ];
724
725     } else {
726
727       warn "$me not using 'invoice_email_pdf_note' in multipart message"
728         if $DEBUG;
729       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
730         $data = $args{'print_text'};
731       } else {
732         $data = [ $self->print_text('', $args{'template'}, %cdrs) ];
733       }
734
735     }
736
737     $alternative->attach(
738       'Type'        => 'text/plain',
739       #'Encoding'    => 'quoted-printable',
740       'Encoding'    => '7bit',
741       'Data'        => $data,
742       'Disposition' => 'inline',
743     );
744
745     $args{'from'} =~ /\@([\w\.\-]+)/;
746     my $from = $1 || 'example.com';
747     my $content_id = join('.', rand()*(2**32), $$, time). "\@$from";
748
749     my $logo;
750     my $agentnum = $self->cust_main->agentnum;
751     if ( defined($args{'template'}) && length($args{'template'})
752          && $conf->exists( 'logo_'. $args{'template'}. '.png', $agentnum )
753        )
754     {
755       $logo = 'logo_'. $args{'template'}. '.png';
756     } else {
757       $logo = "logo.png";
758     }
759     my $image_data = $conf->config_binary( $logo, $agentnum);
760
761     my $image = build MIME::Entity
762       'Type'       => 'image/png',
763       'Encoding'   => 'base64',
764       'Data'       => $image_data,
765       'Filename'   => 'logo.png',
766       'Content-ID' => "<$content_id>",
767     ;
768
769     $alternative->attach(
770       'Type'        => 'text/html',
771       'Encoding'    => 'quoted-printable',
772       'Data'        => [ '<html>',
773                          '  <head>',
774                          '    <title>',
775                          '      '. encode_entities($return{'subject'}), 
776                          '    </title>',
777                          '  </head>',
778                          '  <body bgcolor="#e8e8e8">',
779                          $self->print_html({ time          => '',
780                                              template      => $args{'template'},
781                                              cid           => $content_id,
782                                              %cdrs,
783                                           }),
784                          '  </body>',
785                          '</html>',
786                        ],
787       'Disposition' => 'inline',
788       #'Filename'    => 'invoice.pdf',
789     );
790
791     my @otherparts = ();
792     if ( $self->cust_main->email_csv_cdr ) {
793
794       push @otherparts, build MIME::Entity
795         'Type'        => 'text/csv',
796         'Encoding'    => '7bit',
797         'Data'        => [ map { "$_\n" }
798                              $self->call_details('prepend_billed_number' => 1)
799                          ],
800         'Disposition' => 'attachment',
801         'Filename'    => 'usage-'. $self->invnum. '.csv',
802       ;
803
804     }
805
806     if ( $conf->exists('invoice_email_pdf') ) {
807
808       #attaching pdf too:
809       # multipart/mixed
810       #   multipart/related
811       #     multipart/alternative
812       #       text/plain
813       #       text/html
814       #     image/png
815       #   application/pdf
816
817       my $related = build MIME::Entity 'Type'     => 'multipart/related',
818                                        'Encoding' => '7bit';
819
820       #false laziness w/Misc::send_email
821       $related->head->replace('Content-type',
822         $related->mime_type.
823         '; boundary="'. $related->head->multipart_boundary. '"'.
824         '; type=multipart/alternative'
825       );
826
827       $related->add_part($alternative);
828
829       $related->add_part($image);
830
831       my $pdf = build MIME::Entity $self->mimebuild_pdf('', $args{'template'}, %cdrs);
832
833       $return{'mimeparts'} = [ $related, $pdf, @otherparts ];
834
835     } else {
836
837       #no other attachment:
838       # multipart/related
839       #   multipart/alternative
840       #     text/plain
841       #     text/html
842       #   image/png
843
844       $return{'content-type'} = 'multipart/related';
845       $return{'mimeparts'} = [ $alternative, $image, @otherparts ];
846       $return{'type'} = 'multipart/alternative'; #Content-Type of first part...
847       #$return{'disposition'} = 'inline';
848
849     }
850   
851   } else {
852
853     if ( $conf->exists('invoice_email_pdf') ) {
854       warn "$me creating PDF attachment"
855         if $DEBUG;
856
857       #mime parts arguments a la MIME::Entity->build().
858       $return{'mimeparts'} = [
859         { $self->mimebuild_pdf('', $args{'template'}, %cdrs) }
860       ];
861     }
862   
863     if ( $conf->exists('invoice_email_pdf')
864          and scalar($conf->config('invoice_email_pdf_note')) ) {
865
866       warn "$me using 'invoice_email_pdf_note'"
867         if $DEBUG;
868       $return{'body'} = [ map { $_ . "\n" }
869                               $conf->config('invoice_email_pdf_note')
870                         ];
871
872     } else {
873
874       warn "$me not using 'invoice_email_pdf_note'"
875         if $DEBUG;
876       if ( ref($args{'print_text'}) eq 'ARRAY' ) {
877         $return{'body'} = $args{'print_text'};
878       } else {
879         $return{'body'} = [ $self->print_text('', $args{'template'}, %cdrs) ];
880       }
881
882     }
883
884   }
885
886   %return;
887
888 }
889
890 =item mimebuild_pdf
891
892 Returns a list suitable for passing to MIME::Entity->build(), representing
893 this invoice as PDF attachment.
894
895 =cut
896
897 sub mimebuild_pdf {
898   my $self = shift;
899   (
900     'Type'        => 'application/pdf',
901     'Encoding'    => 'base64',
902     'Data'        => [ $self->print_pdf(@_) ],
903     'Disposition' => 'attachment',
904     'Filename'    => 'invoice-'. $self->invnum. '.pdf',
905   );
906 }
907
908 =item send [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
909
910 Sends this invoice to the destinations configured for this customer: sends
911 email, prints and/or faxes.  See L<FS::cust_main_invoice>.
912
913 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
914
915 AGENTNUM, if specified, means that this invoice will only be sent for customers
916 of the specified agent or agent(s).  AGENTNUM can be a scalar agentnum (for a
917 single agent) or an arrayref of agentnums.
918
919 INVOICE_FROM, if specified, overrides the default email invoice From: address.
920
921 AMOUNT, if specified, only sends the invoice if the total amount owed on this
922 invoice and all older invoices is greater than the specified amount.
923
924 =cut
925
926 sub queueable_send {
927   my %opt = @_;
928
929   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
930     or die "invalid invoice number: " . $opt{invnum};
931
932   my @args = ( $opt{template}, $opt{agentnum} );
933   push @args, $opt{invoice_from}
934     if exists($opt{invoice_from}) && $opt{invoice_from};
935
936   my $error = $self->send( @args );
937   die $error if $error;
938
939 }
940
941 sub send {
942   my $self = shift;
943   my $template = scalar(@_) ? shift : '';
944   if ( scalar(@_) && $_[0]  ) {
945     my $agentnums = ref($_[0]) ? shift : [ shift ];
946     return 'N/A' unless grep { $_ == $self->cust_main->agentnum } @$agentnums;
947   }
948
949   my $invoice_from =
950     scalar(@_)
951       ? shift
952       : ( $self->_agent_invoice_from ||    #XXX should go away
953           $conf->config('invoice_from', $self->cust_main->agentnum )
954         );
955
956   my $balance_over = ( scalar(@_) && $_[0] !~ /^\s*$/ ) ? shift : 0;
957
958   return ''
959     unless $self->cust_main->total_owed_date($self->_date) > $balance_over;
960
961   my @invoicing_list = $self->cust_main->invoicing_list;
962
963   #$self->email_invoice($template, $invoice_from)
964   $self->email($template, $invoice_from)
965     if grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list or !@invoicing_list;
966
967   #$self->print_invoice($template)
968   $self->print($template)
969     if grep { $_ eq 'POST' } @invoicing_list; #postal
970
971   $self->fax_invoice($template)
972     if grep { $_ eq 'FAX' } @invoicing_list; #fax
973
974   '';
975
976 }
977
978 =item email [ TEMPLATENAME  [ , INVOICE_FROM ] ] 
979
980 Emails this invoice.
981
982 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
983
984 INVOICE_FROM, if specified, overrides the default email invoice From: address.
985
986 =cut
987
988 sub queueable_email {
989   my %opt = @_;
990
991   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
992     or die "invalid invoice number: " . $opt{invnum};
993
994   my @args = ( $opt{template} );
995   push @args, $opt{invoice_from}
996     if exists($opt{invoice_from}) && $opt{invoice_from};
997
998   my $error = $self->email( @args );
999   die $error if $error;
1000
1001 }
1002
1003 #sub email_invoice {
1004 sub email {
1005   my $self = shift;
1006   my $template = scalar(@_) ? shift : '';
1007   my $invoice_from =
1008     scalar(@_)
1009       ? shift
1010       : ( $self->_agent_invoice_from ||    #XXX should go away
1011           $conf->config('invoice_from', $self->cust_main->agentnum )
1012         );
1013
1014
1015   my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } 
1016                             $self->cust_main->invoicing_list;
1017
1018   #better to notify this person than silence
1019   @invoicing_list = ($invoice_from) unless @invoicing_list;
1020
1021   my $subject = $self->email_subject($template);
1022
1023   my $error = send_email(
1024     $self->generate_email(
1025       'from'       => $invoice_from,
1026       'to'         => [ grep { $_ !~ /^(POST|FAX)$/ } @invoicing_list ],
1027       'subject'    => $subject,
1028       'template'   => $template,
1029     )
1030   );
1031   die "can't email invoice: $error\n" if $error;
1032   #die "$error\n" if $error;
1033
1034 }
1035
1036 sub email_subject {
1037   my $self = shift;
1038
1039   #my $template = scalar(@_) ? shift : '';
1040   #per-template?
1041
1042   my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1043                 || 'Invoice';
1044
1045   my $cust_main = $self->cust_main;
1046   my $name = $cust_main->name;
1047   my $name_short = $cust_main->name_short;
1048   my $invoice_number = $self->invnum;
1049   my $invoice_date = $self->_date_pretty;
1050
1051   eval qq("$subject");
1052 }
1053
1054 =item lpr_data [ TEMPLATENAME ]
1055
1056 Returns the postscript or plaintext for this invoice as an arrayref.
1057
1058 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
1059
1060 =cut
1061
1062 sub lpr_data {
1063   my( $self, $template) = @_;
1064   $conf->exists('invoice_latex')
1065     ? [ $self->print_ps('', $template) ]
1066     : [ $self->print_text('', $template) ];
1067 }
1068
1069 =item print [ TEMPLATENAME ]
1070
1071 Prints this invoice.
1072
1073 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
1074
1075 =cut
1076
1077 #sub print_invoice {
1078 sub print {
1079   my $self = shift;
1080   my $template = scalar(@_) ? shift : '';
1081
1082   do_print $self->lpr_data($template);
1083 }
1084
1085 =item fax_invoice [ TEMPLATENAME ] 
1086
1087 Faxes this invoice.
1088
1089 TEMPLATENAME, if specified, is the name of a suffix for alternate invoices.
1090
1091 =cut
1092
1093 sub fax_invoice {
1094   my $self = shift;
1095   my $template = scalar(@_) ? shift : '';
1096
1097   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1098     unless $conf->exists('invoice_latex');
1099
1100   my $dialstring = $self->cust_main->getfield('fax');
1101   #Check $dialstring?
1102
1103   my $error = send_fax( 'docdata'    => $self->lpr_data($template),
1104                         'dialstring' => $dialstring,
1105                       );
1106   die $error if $error;
1107
1108 }
1109
1110 =item ftp_invoice [ TEMPLATENAME ] 
1111
1112 Sends this invoice data via FTP.
1113
1114 TEMPLATENAME is unused?
1115
1116 =cut
1117
1118 sub ftp_invoice {
1119   my $self = shift;
1120   my $template = scalar(@_) ? shift : '';
1121
1122   $self->send_csv(
1123     'protocol'   => 'ftp',
1124     'server'     => $conf->config('cust_bill-ftpserver'),
1125     'username'   => $conf->config('cust_bill-ftpusername'),
1126     'password'   => $conf->config('cust_bill-ftppassword'),
1127     'dir'        => $conf->config('cust_bill-ftpdir'),
1128     'format'     => $conf->config('cust_bill-ftpformat'),
1129   );
1130 }
1131
1132 =item spool_invoice [ TEMPLATENAME ] 
1133
1134 Spools this invoice data (see L<FS::spool_csv>)
1135
1136 TEMPLATENAME is unused?
1137
1138 =cut
1139
1140 sub spool_invoice {
1141   my $self = shift;
1142   my $template = scalar(@_) ? shift : '';
1143
1144   $self->spool_csv(
1145     'format'       => $conf->config('cust_bill-spoolformat'),
1146     'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1147   );
1148 }
1149
1150 =item send_if_newest [ TEMPLATENAME [ , AGENTNUM [ , INVOICE_FROM ] ] ]
1151
1152 Like B<send>, but only sends the invoice if it is the newest open invoice for
1153 this customer.
1154
1155 =cut
1156
1157 sub send_if_newest {
1158   my $self = shift;
1159
1160   return ''
1161     if scalar(
1162                grep { $_->owed > 0 } 
1163                     qsearch('cust_bill', {
1164                       'custnum' => $self->custnum,
1165                       #'_date'   => { op=>'>', value=>$self->_date },
1166                       'invnum'  => { op=>'>', value=>$self->invnum },
1167                     } )
1168              );
1169     
1170   $self->send(@_);
1171 }
1172
1173 =item send_csv OPTION => VALUE, ...
1174
1175 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1176
1177 Options are:
1178
1179 protocol - currently only "ftp"
1180 server
1181 username
1182 password
1183 dir
1184
1185 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1186 and YYMMDDHHMMSS is a timestamp.
1187
1188 See L</print_csv> for a description of the output format.
1189
1190 =cut
1191
1192 sub send_csv {
1193   my($self, %opt) = @_;
1194
1195   #create file(s)
1196
1197   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1198   mkdir $spooldir, 0700 unless -d $spooldir;
1199
1200   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1201   my $file = "$spooldir/$tracctnum.csv";
1202   
1203   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1204
1205   open(CSV, ">$file") or die "can't open $file: $!";
1206   print CSV $header;
1207
1208   print CSV $detail;
1209
1210   close CSV;
1211
1212   my $net;
1213   if ( $opt{protocol} eq 'ftp' ) {
1214     eval "use Net::FTP;";
1215     die $@ if $@;
1216     $net = Net::FTP->new($opt{server}) or die @$;
1217   } else {
1218     die "unknown protocol: $opt{protocol}";
1219   }
1220
1221   $net->login( $opt{username}, $opt{password} )
1222     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1223
1224   $net->binary or die "can't set binary mode";
1225
1226   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1227
1228   $net->put($file) or die "can't put $file: $!";
1229
1230   $net->quit;
1231
1232   unlink $file;
1233
1234 }
1235
1236 =item spool_csv
1237
1238 Spools CSV invoice data.
1239
1240 Options are:
1241
1242 =over 4
1243
1244 =item format - 'default' or 'billco'
1245
1246 =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>).
1247
1248 =item agent_spools - if set to a true value, will spool to per-agent files rather than a single global file
1249
1250 =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.
1251
1252 =back
1253
1254 =cut
1255
1256 sub spool_csv {
1257   my($self, %opt) = @_;
1258
1259   my $cust_main = $self->cust_main;
1260
1261   if ( $opt{'dest'} ) {
1262     my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1263                              $cust_main->invoicing_list;
1264     return 'N/A' unless $invoicing_list{$opt{'dest'}}
1265                      || ! keys %invoicing_list;
1266   }
1267
1268   if ( $opt{'balanceover'} ) {
1269     return 'N/A'
1270       if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1271   }
1272
1273   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1274   mkdir $spooldir, 0700 unless -d $spooldir;
1275
1276   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1277
1278   my $file =
1279     "$spooldir/".
1280     ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1281     ( lc($opt{'format'}) eq 'billco' ? '-header' : '' ) .
1282     '.csv';
1283   
1284   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1285
1286   open(CSV, ">>$file") or die "can't open $file: $!";
1287   flock(CSV, LOCK_EX);
1288   seek(CSV, 0, 2);
1289
1290   print CSV $header;
1291
1292   if ( lc($opt{'format'}) eq 'billco' ) {
1293
1294     flock(CSV, LOCK_UN);
1295     close CSV;
1296
1297     $file =
1298       "$spooldir/".
1299       ( $opt{'agent_spools'} ? 'agentnum'.$cust_main->agentnum : 'spool' ).
1300       '-detail.csv';
1301
1302     open(CSV,">>$file") or die "can't open $file: $!";
1303     flock(CSV, LOCK_EX);
1304     seek(CSV, 0, 2);
1305   }
1306
1307   print CSV $detail;
1308
1309   flock(CSV, LOCK_UN);
1310   close CSV;
1311
1312   return '';
1313
1314 }
1315
1316 =item print_csv OPTION => VALUE, ...
1317
1318 Returns CSV data for this invoice.
1319
1320 Options are:
1321
1322 format - 'default' or 'billco'
1323
1324 Returns a list consisting of two scalars.  The first is a single line of CSV
1325 header information for this invoice.  The second is one or more lines of CSV
1326 detail information for this invoice.
1327
1328 If I<format> is not specified or "default", the fields of the CSV file are as
1329 follows:
1330
1331 record_type, invnum, custnum, _date, charged, first, last, company, address1, address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1332
1333 =over 4
1334
1335 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1336
1337 B<record_type> is C<cust_bill> for the initial header line only.  The
1338 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1339 fields are filled in.
1340
1341 B<record_type> is C<cust_bill_pkg> for detail lines.  Only the first two fields
1342 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1343 are filled in.
1344
1345 =item invnum - invoice number
1346
1347 =item custnum - customer number
1348
1349 =item _date - invoice date
1350
1351 =item charged - total invoice amount
1352
1353 =item first - customer first name
1354
1355 =item last - customer first name
1356
1357 =item company - company name
1358
1359 =item address1 - address line 1
1360
1361 =item address2 - address line 1
1362
1363 =item city
1364
1365 =item state
1366
1367 =item zip
1368
1369 =item country
1370
1371 =item pkg - line item description
1372
1373 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1374
1375 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1376
1377 =item sdate - start date for recurring fee
1378
1379 =item edate - end date for recurring fee
1380
1381 =back
1382
1383 If I<format> is "billco", the fields of the header CSV file are as follows:
1384
1385   +-------------------------------------------------------------------+
1386   |                        FORMAT HEADER FILE                         |
1387   |-------------------------------------------------------------------|
1388   | Field | Description                   | Name       | Type | Width |
1389   | 1     | N/A-Leave Empty               | RC         | CHAR |     2 |
1390   | 2     | N/A-Leave Empty               | CUSTID     | CHAR |    15 |
1391   | 3     | Transaction Account No        | TRACCTNUM  | CHAR |    15 |
1392   | 4     | Transaction Invoice No        | TRINVOICE  | CHAR |    15 |
1393   | 5     | Transaction Zip Code          | TRZIP      | CHAR |     5 |
1394   | 6     | Transaction Company Bill To   | TRCOMPANY  | CHAR |    30 |
1395   | 7     | Transaction Contact Bill To   | TRNAME     | CHAR |    30 |
1396   | 8     | Additional Address Unit Info  | TRADDR1    | CHAR |    30 |
1397   | 9     | Bill To Street Address        | TRADDR2    | CHAR |    30 |
1398   | 10    | Ancillary Billing Information | TRADDR3    | CHAR |    30 |
1399   | 11    | Transaction City Bill To      | TRCITY     | CHAR |    20 |
1400   | 12    | Transaction State Bill To     | TRSTATE    | CHAR |     2 |
1401   | 13    | Bill Cycle Close Date         | CLOSEDATE  | CHAR |    10 |
1402   | 14    | Bill Due Date                 | DUEDATE    | CHAR |    10 |
1403   | 15    | Previous Balance              | BALFWD     | NUM* |     9 |
1404   | 16    | Pmt/CR Applied                | CREDAPPLY  | NUM* |     9 |
1405   | 17    | Total Current Charges         | CURRENTCHG | NUM* |     9 |
1406   | 18    | Total Amt Due                 | TOTALDUE   | NUM* |     9 |
1407   | 19    | Total Amt Due                 | AMTDUE     | NUM* |     9 |
1408   | 20    | 30 Day Aging                  | AMT30      | NUM* |     9 |
1409   | 21    | 60 Day Aging                  | AMT60      | NUM* |     9 |
1410   | 22    | 90 Day Aging                  | AMT90      | NUM* |     9 |
1411   | 23    | Y/N                           | AGESWITCH  | CHAR |     1 |
1412   | 24    | Remittance automation         | SCANLINE   | CHAR |   100 |
1413   | 25    | Total Taxes & Fees            | TAXTOT     | NUM* |     9 |
1414   | 26    | Customer Reference Number     | CUSTREF    | CHAR |    15 |
1415   | 27    | Federal Tax***                | FEDTAX     | NUM* |     9 |
1416   | 28    | State Tax***                  | STATETAX   | NUM* |     9 |
1417   | 29    | Other Taxes & Fees***         | OTHERTAX   | NUM* |     9 |
1418   +-------+-------------------------------+------------+------+-------+
1419
1420 If I<format> is "billco", the fields of the detail CSV file are as follows:
1421
1422                                   FORMAT FOR DETAIL FILE
1423         |                            |           |      |
1424   Field | Description                | Name      | Type | Width
1425   1     | N/A-Leave Empty            | RC        | CHAR |     2
1426   2     | N/A-Leave Empty            | CUSTID    | CHAR |    15
1427   3     | Account Number             | TRACCTNUM | CHAR |    15
1428   4     | Invoice Number             | TRINVOICE | CHAR |    15
1429   5     | Line Sequence (sort order) | LINESEQ   | NUM  |     6
1430   6     | Transaction Detail         | DETAILS   | CHAR |   100
1431   7     | Amount                     | AMT       | NUM* |     9
1432   8     | Line Format Control**      | LNCTRL    | CHAR |     2
1433   9     | Grouping Code              | GROUP     | CHAR |     2
1434   10    | User Defined               | ACCT CODE | CHAR |    15
1435
1436 =cut
1437
1438 sub print_csv {
1439   my($self, %opt) = @_;
1440   
1441   eval "use Text::CSV_XS";
1442   die $@ if $@;
1443
1444   my $cust_main = $self->cust_main;
1445
1446   my $csv = Text::CSV_XS->new({'always_quote'=>1});
1447
1448   if ( lc($opt{'format'}) eq 'billco' ) {
1449
1450     my $taxtotal = 0;
1451     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1452
1453     my $duedate = $self->due_date2str('%m/%d/%Y'); #date_format?
1454
1455     my( $previous_balance, @unused ) = $self->previous; #previous balance
1456
1457     my $pmt_cr_applied = 0;
1458     $pmt_cr_applied += $_->{'amount'}
1459       foreach ( $self->_items_payments, $self->_items_credits ) ;
1460
1461     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1462
1463     $csv->combine(
1464       '',                         #  1 | N/A-Leave Empty               CHAR   2
1465       '',                         #  2 | N/A-Leave Empty               CHAR  15
1466       $opt{'tracctnum'},          #  3 | Transaction Account No        CHAR  15
1467       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
1468       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
1469       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
1470       #$cust_main->payname,        #  7 | Transaction Contact Bill To   CHAR  30
1471       $cust_main->contact,        #  7 | Transaction Contact Bill To   CHAR  30
1472       $cust_main->address2,       #  8 | Additional Address Unit Info  CHAR  30
1473       $cust_main->address1,       #  9 | Bill To Street Address        CHAR  30
1474       '',                         # 10 | Ancillary Billing Information CHAR  30
1475       $cust_main->city,           # 11 | Transaction City Bill To      CHAR  20
1476       $cust_main->state,          # 12 | Transaction State Bill To     CHAR   2
1477
1478       # XXX ?
1479       time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR  10
1480
1481       # XXX ?
1482       $duedate,                   # 14 | Bill Due Date                 CHAR  10
1483
1484       $previous_balance,          # 15 | Previous Balance              NUM*   9
1485       $pmt_cr_applied,            # 16 | Pmt/CR Applied                NUM*   9
1486       sprintf("%.2f", $self->charged), # 17 | Total Current Charges    NUM*   9
1487       $totaldue,                  # 18 | Total Amt Due                 NUM*   9
1488       $totaldue,                  # 19 | Total Amt Due                 NUM*   9
1489       '',                         # 20 | 30 Day Aging                  NUM*   9
1490       '',                         # 21 | 60 Day Aging                  NUM*   9
1491       '',                         # 22 | 90 Day Aging                  NUM*   9
1492       'N',                        # 23 | Y/N                           CHAR   1
1493       '',                         # 24 | Remittance automation         CHAR 100
1494       $taxtotal,                  # 25 | Total Taxes & Fees            NUM*   9
1495       $self->custnum,             # 26 | Customer Reference Number     CHAR  15
1496       '0',                        # 27 | Federal Tax***                NUM*   9
1497       sprintf("%.2f", $taxtotal), # 28 | State Tax***                  NUM*   9
1498       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
1499     );
1500
1501   } else {
1502   
1503     $csv->combine(
1504       'cust_bill',
1505       $self->invnum,
1506       $self->custnum,
1507       time2str("%x", $self->_date),
1508       sprintf("%.2f", $self->charged),
1509       ( map { $cust_main->getfield($_) }
1510           qw( first last company address1 address2 city state zip country ) ),
1511       map { '' } (1..5),
1512     ) or die "can't create csv";
1513   }
1514
1515   my $header = $csv->string. "\n";
1516
1517   my $detail = '';
1518   if ( lc($opt{'format'}) eq 'billco' ) {
1519
1520     my $lineseq = 0;
1521     foreach my $item ( $self->_items_pkg ) {
1522
1523       $csv->combine(
1524         '',                     #  1 | N/A-Leave Empty            CHAR   2
1525         '',                     #  2 | N/A-Leave Empty            CHAR  15
1526         $opt{'tracctnum'},      #  3 | Account Number             CHAR  15
1527         $self->invnum,          #  4 | Invoice Number             CHAR  15
1528         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
1529         $item->{'description'}, #  6 | Transaction Detail         CHAR 100
1530         $item->{'amount'},      #  7 | Amount                     NUM*   9
1531         '',                     #  8 | Line Format Control**      CHAR   2
1532         '',                     #  9 | Grouping Code              CHAR   2
1533         '',                     # 10 | User Defined               CHAR  15
1534       );
1535
1536       $detail .= $csv->string. "\n";
1537
1538     }
1539
1540   } else {
1541
1542     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1543
1544       my($pkg, $setup, $recur, $sdate, $edate);
1545       if ( $cust_bill_pkg->pkgnum ) {
1546       
1547         ($pkg, $setup, $recur, $sdate, $edate) = (
1548           $cust_bill_pkg->part_pkg->pkg,
1549           ( $cust_bill_pkg->setup != 0
1550             ? sprintf("%.2f", $cust_bill_pkg->setup )
1551             : '' ),
1552           ( $cust_bill_pkg->recur != 0
1553             ? sprintf("%.2f", $cust_bill_pkg->recur )
1554             : '' ),
1555           ( $cust_bill_pkg->sdate 
1556             ? time2str("%x", $cust_bill_pkg->sdate)
1557             : '' ),
1558           ($cust_bill_pkg->edate 
1559             ?time2str("%x", $cust_bill_pkg->edate)
1560             : '' ),
1561         );
1562   
1563       } else { #pkgnum tax
1564         next unless $cust_bill_pkg->setup != 0;
1565         $pkg = $cust_bill_pkg->desc;
1566         $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1567         ( $sdate, $edate ) = ( '', '' );
1568       }
1569   
1570       $csv->combine(
1571         'cust_bill_pkg',
1572         $self->invnum,
1573         ( map { '' } (1..11) ),
1574         ($pkg, $setup, $recur, $sdate, $edate)
1575       ) or die "can't create csv";
1576
1577       $detail .= $csv->string. "\n";
1578
1579     }
1580
1581   }
1582
1583   ( $header, $detail );
1584
1585 }
1586
1587 =item comp
1588
1589 Pays this invoice with a compliemntary payment.  If there is an error,
1590 returns the error, otherwise returns false.
1591
1592 =cut
1593
1594 sub comp {
1595   my $self = shift;
1596   my $cust_pay = new FS::cust_pay ( {
1597     'invnum'   => $self->invnum,
1598     'paid'     => $self->owed,
1599     '_date'    => '',
1600     'payby'    => 'COMP',
1601     'payinfo'  => $self->cust_main->payinfo,
1602     'paybatch' => '',
1603   } );
1604   $cust_pay->insert;
1605 }
1606
1607 =item realtime_card
1608
1609 Attempts to pay this invoice with a credit card payment via a
1610 Business::OnlinePayment realtime gateway.  See
1611 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1612 for supported processors.
1613
1614 =cut
1615
1616 sub realtime_card {
1617   my $self = shift;
1618   $self->realtime_bop( 'CC', @_ );
1619 }
1620
1621 =item realtime_ach
1622
1623 Attempts to pay this invoice with an electronic check (ACH) payment via a
1624 Business::OnlinePayment realtime gateway.  See
1625 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1626 for supported processors.
1627
1628 =cut
1629
1630 sub realtime_ach {
1631   my $self = shift;
1632   $self->realtime_bop( 'ECHECK', @_ );
1633 }
1634
1635 =item realtime_lec
1636
1637 Attempts to pay this invoice with phone bill (LEC) payment via a
1638 Business::OnlinePayment realtime gateway.  See
1639 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
1640 for supported processors.
1641
1642 =cut
1643
1644 sub realtime_lec {
1645   my $self = shift;
1646   $self->realtime_bop( 'LEC', @_ );
1647 }
1648
1649 sub realtime_bop {
1650   my( $self, $method ) = @_;
1651
1652   my $cust_main = $self->cust_main;
1653   my $balance = $cust_main->balance;
1654   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
1655   $amount = sprintf("%.2f", $amount);
1656   return "not run (balance $balance)" unless $amount > 0;
1657
1658   my $description = 'Internet Services';
1659   if ( $conf->exists('business-onlinepayment-description') ) {
1660     my $dtempl = $conf->config('business-onlinepayment-description');
1661
1662     my $agent_obj = $cust_main->agent
1663       or die "can't retreive agent for $cust_main (agentnum ".
1664              $cust_main->agentnum. ")";
1665     my $agent = $agent_obj->agent;
1666     my $pkgs = join(', ',
1667       map { $_->part_pkg->pkg }
1668         grep { $_->pkgnum } $self->cust_bill_pkg
1669     );
1670     $description = eval qq("$dtempl");
1671   }
1672
1673   $cust_main->realtime_bop($method, $amount,
1674     'description' => $description,
1675     'invnum'      => $self->invnum,
1676   );
1677
1678 }
1679
1680 =item batch_card OPTION => VALUE...
1681
1682 Adds a payment for this invoice to the pending credit card batch (see
1683 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
1684 runs the payment using a realtime gateway.
1685
1686 =cut
1687
1688 sub batch_card {
1689   my ($self, %options) = @_;
1690   my $cust_main = $self->cust_main;
1691
1692   $options{invnum} = $self->invnum;
1693   
1694   $cust_main->batch_card(%options);
1695 }
1696
1697 sub _agent_template {
1698   my $self = shift;
1699   $self->cust_main->agent_template;
1700 }
1701
1702 sub _agent_invoice_from {
1703   my $self = shift;
1704   $self->cust_main->agent_invoice_from;
1705 }
1706
1707 =item print_text [ TIME [ , TEMPLATE ] ]
1708
1709 Returns an text invoice, as a list of lines.
1710
1711 TIME an optional value used to control the printing of overdue messages.  The
1712 default is now.  It isn't the date of the invoice; that's the `_date' field.
1713 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1714 L<Time::Local> and L<Date::Parse> for conversion functions.
1715
1716 =cut
1717
1718 sub print_text {
1719   my( $self, $today, $template, %opt ) = @_;
1720
1721   my %params = ( 'format' => 'template' );
1722   $params{'time'} = $today if $today;
1723   $params{'template'} = $template if $template;
1724   $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
1725
1726   $self->print_generic( %params );
1727 }
1728
1729 =item print_latex [ TIME [ , TEMPLATE ] ]
1730
1731 Internal method - returns a filename of a filled-in LaTeX template for this
1732 invoice (Note: add ".tex" to get the actual filename), and a filename of
1733 an associated logo (with the .eps extension included).
1734
1735 See print_ps and print_pdf for methods that return PostScript and PDF output.
1736
1737 TIME an optional value used to control the printing of overdue messages.  The
1738 default is now.  It isn't the date of the invoice; that's the `_date' field.
1739 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1740 L<Time::Local> and L<Date::Parse> for conversion functions.
1741
1742 =cut
1743
1744 sub print_latex {
1745   my( $self, $today, $template, %opt ) = @_;
1746
1747   my %params = ( 'format' => 'latex' );
1748   $params{'time'} = $today if $today;
1749   $params{'template'} = $template if $template;
1750   $params{'unsquelch_cdr'} = $opt{'unsquelch_cdr'} if $opt{'unsquelch_cdr'};
1751
1752   $template ||= $self->_agent_template;
1753
1754   my $dir = $FS::UID::conf_dir. "/cache.". $FS::UID::datasrc;
1755   my $lh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1756                            DIR      => $dir,
1757                            SUFFIX   => '.eps',
1758                            UNLINK   => 0,
1759                          ) or die "can't open temp file: $!\n";
1760
1761   my $agentnum = $self->cust_main->agentnum;
1762
1763   if ( $template && $conf->exists("logo_${template}.eps", $agentnum) ) {
1764     print $lh $conf->config_binary("logo_${template}.eps", $agentnum)
1765       or die "can't write temp file: $!\n";
1766   } else {
1767     print $lh $conf->config_binary('logo.eps', $agentnum)
1768       or die "can't write temp file: $!\n";
1769   }
1770   close $lh;
1771   $params{'logo_file'} = $lh->filename;
1772
1773   my @filled_in = $self->print_generic( %params );
1774   
1775   my $fh = new File::Temp( TEMPLATE => 'invoice.'. $self->invnum. '.XXXXXXXX',
1776                            DIR      => $dir,
1777                            SUFFIX   => '.tex',
1778                            UNLINK   => 0,
1779                          ) or die "can't open temp file: $!\n";
1780   print $fh join('', @filled_in );
1781   close $fh;
1782
1783   $fh->filename =~ /^(.*).tex$/ or die "unparsable filename: ". $fh->filename;
1784   return ($1, $params{'logo_file'});
1785
1786 }
1787
1788 =item print_generic OPTIONS_HASH
1789
1790 Internal method - returns a filled-in template for this invoice as a scalar.
1791
1792 See print_ps and print_pdf for methods that return PostScript and PDF output.
1793
1794 Non optional options include 
1795   format - latex, html, template
1796
1797 Optional options include
1798
1799 template - a value used as a suffix for a configuration template
1800
1801 time - a value used to control the printing of overdue messages.  The
1802 default is now.  It isn't the date of the invoice; that's the `_date' field.
1803 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
1804 L<Time::Local> and L<Date::Parse> for conversion functions.
1805
1806 cid - 
1807
1808 unsquelch_cdr - overrides any per customer cdr squelching when true
1809
1810 =cut
1811
1812 #what's with all the sprintf('%10.2f')'s in here?  will it cause any
1813 # (alignment?) problems to change them all to '%.2f' ?
1814 sub print_generic {
1815
1816   my( $self, %params ) = @_;
1817   my $today = $params{today} ? $params{today} : time;
1818   warn "FS::cust_bill::print_generic called on $self with suffix $params{template}\n"
1819     if $DEBUG;
1820
1821   my $format = $params{format};
1822   die "Unknown format: $format"
1823     unless $format =~ /^(latex|html|template)$/;
1824
1825   my $cust_main = $self->cust_main;
1826   $cust_main->payname( $cust_main->first. ' '. $cust_main->getfield('last') )
1827     unless $cust_main->payname
1828         && $cust_main->payby !~ /^(CARD|DCRD|CHEK|DCHK)$/;
1829
1830   my %delimiters = ( 'latex'    => [ '[@--', '--@]' ],
1831                      'html'     => [ '<%=', '%>' ],
1832                      'template' => [ '{', '}' ],
1833                    );
1834
1835   #create the template
1836   my $template = $params{template} ? $params{template} : $self->_agent_template;
1837   my $templatefile = "invoice_$format";
1838   $templatefile .= "_$template"
1839     if length($template);
1840   my @invoice_template = map "$_\n", $conf->config($templatefile)
1841     or die "cannot load config data $templatefile";
1842
1843   my $old_latex = '';
1844   if ( $format eq 'latex' && grep { /^%%Detail/ } @invoice_template ) {
1845     #change this to a die when the old code is removed
1846     warn "old-style invoice template $templatefile; ".
1847          "patch with conf/invoice_latex.diff or use new conf/invoice_latex*\n";
1848     $old_latex = 'true';
1849     @invoice_template = _translate_old_latex_format(@invoice_template);
1850   } 
1851
1852   my $text_template = new Text::Template(
1853     TYPE => 'ARRAY',
1854     SOURCE => \@invoice_template,
1855     DELIMITERS => $delimiters{$format},
1856   );
1857
1858   $text_template->compile()
1859     or die "Can't compile $templatefile: $Text::Template::ERROR\n";
1860
1861
1862   # additional substitution could possibly cause breakage in existing templates
1863   my %convert_maps = ( 
1864     'latex' => {
1865                  'notes'         => sub { map "$_", @_ },
1866                  'footer'        => sub { map "$_", @_ },
1867                  'smallfooter'   => sub { map "$_", @_ },
1868                  'returnaddress' => sub { map "$_", @_ },
1869                  'coupon'        => sub { map "$_", @_ },
1870                },
1871     'html'  => {
1872                  'notes' =>
1873                    sub {
1874                      map { 
1875                        s/%%(.*)$/<!-- $1 -->/g;
1876                        s/\\section\*\{\\textsc\{(.)(.*)\}\}/<p><b><font size="+1">$1<\/font>\U$2<\/b>/g;
1877                        s/\\begin\{enumerate\}/<ol>/g;
1878                        s/\\item /  <li>/g;
1879                        s/\\end\{enumerate\}/<\/ol>/g;
1880                        s/\\textbf\{(.*)\}/<b>$1<\/b>/g;
1881                        s/\\\\\*/<br>/g;
1882                        s/\\dollar ?/\$/g;
1883                        s/\\#/#/g;
1884                        s/~/&nbsp;/g;
1885                        $_;
1886                      }  @_
1887                    },
1888                  'footer' =>
1889                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1890                  'smallfooter' =>
1891                    sub { map { s/~/&nbsp;/g; s/\\\\\*?\s*$/<BR>/; $_; } @_ },
1892                  'returnaddress' =>
1893                    sub {
1894                      map { 
1895                        s/~/&nbsp;/g;
1896                        s/\\\\\*?\s*$/<BR>/;
1897                        s/\\hyphenation\{[\w\s\-]+}//;
1898                        s/\\([&])/$1/g;
1899                        $_;
1900                      }  @_
1901                    },
1902                  'coupon'        => sub { "" },
1903                },
1904     'template' => {
1905                  'notes' =>
1906                    sub {
1907                      map { 
1908                        s/%%.*$//g;
1909                        s/\\section\*\{\\textsc\{(.*)\}\}/\U$1/g;
1910                        s/\\begin\{enumerate\}//g;
1911                        s/\\item /  * /g;
1912                        s/\\end\{enumerate\}//g;
1913                        s/\\textbf\{(.*)\}/$1/g;
1914                        s/\\\\\*/ /;
1915                        s/\\dollar ?/\$/g;
1916                        $_;
1917                      }  @_
1918                    },
1919                  'footer' =>
1920                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1921                  'smallfooter' =>
1922                    sub { map { s/~/ /g; s/\\\\\*?\s*$/\n/; $_; } @_ },
1923                  'returnaddress' =>
1924                    sub {
1925                      map { 
1926                        s/~/ /g;
1927                        s/\\\\\*?\s*$/\n/;             # dubious
1928                        s/\\hyphenation\{[\w\s\-]+}//;
1929                        $_;
1930                      }  @_
1931                    },
1932                  'coupon'        => sub { "" },
1933                },
1934   );
1935
1936
1937   # hashes for differing output formats
1938   my %nbsps = ( 'latex'    => '~',
1939                 'html'     => '',    # '&nbps;' would be nice
1940                 'template' => '',    # not used
1941               );
1942   my $nbsp = $nbsps{$format};
1943
1944   my %escape_functions = ( 'latex'    => \&_latex_escape,
1945                            'html'     => \&encode_entities,
1946                            'template' => sub { shift },
1947                          );
1948   my $escape_function = $escape_functions{$format};
1949
1950   my %date_formats = ( 'latex'    => '%b %o, %Y',
1951                        'html'     => '%b&nbsp;%o,&nbsp;%Y',
1952                        'template' => '%s',
1953                      );
1954   my $date_format = $date_formats{$format};
1955
1956   my %embolden_functions = ( 'latex'    => sub { return '\textbf{'. shift(). '}'
1957                                                },
1958                              'html'     => sub { return '<b>'. shift(). '</b>'
1959                                                },
1960                              'template' => sub { shift },
1961                            );
1962   my $embolden_function = $embolden_functions{$format};
1963
1964
1965   # generate template variables
1966   my $returnaddress;
1967   if (
1968          defined( $conf->config_orbase( "invoice_${format}returnaddress",
1969                                         $template
1970                                       )
1971                 )
1972        && length( $conf->config_orbase( "invoice_${format}returnaddress",
1973                                         $template
1974                                       )
1975                 )
1976   ) {
1977
1978     $returnaddress = join("\n",
1979       $conf->config_orbase("invoice_${format}returnaddress", $template)
1980     );
1981
1982   } elsif ( grep /\S/,
1983             $conf->config_orbase('invoice_latexreturnaddress', $template) ) {
1984
1985     my $convert_map = $convert_maps{$format}{'returnaddress'};
1986     $returnaddress =
1987       join( "\n",
1988             &$convert_map( $conf->config_orbase( "invoice_latexreturnaddress",
1989                                                  $template
1990                                                )
1991                          )
1992           );
1993   } elsif ( grep /\S/, $conf->config('company_address', $self->cust_main->agentnum) ) {
1994
1995     my $convert_map = $convert_maps{$format}{'returnaddress'};
1996     $returnaddress = join( "\n", &$convert_map(
1997                                    map { s/( {2,})/'~' x length($1)/eg;
1998                                          s/$/\\\\\*/;
1999                                          $_
2000                                        }
2001                                      ( $conf->config('company_name', $self->cust_main->agentnum),
2002                                        $conf->config('company_address', $self->cust_main->agentnum),
2003                                      )
2004                                  )
2005                      );
2006
2007   } else {
2008
2009     my $warning = "Couldn't find a return address; ".
2010                   "do you need to set the company_address configuration value?";
2011     warn "$warning\n";
2012     $returnaddress = $nbsp;
2013     #$returnaddress = $warning;
2014
2015   }
2016
2017   my %invoice_data = (
2018     'company_name'    => scalar( $conf->config('company_name', $self->cust_main->agentnum) ),
2019     'company_address' => join("\n", $conf->config('company_address', $self->cust_main->agentnum) ). "\n",
2020     'custnum'         => $cust_main->display_custnum,
2021     'invnum'          => $self->invnum,
2022     'date'            => time2str($date_format, $self->_date),
2023     'today'           => time2str('%b %o, %Y', $today),
2024     'agent'           => &$escape_function($cust_main->agent->agent),
2025     'agent_custid'    => &$escape_function($cust_main->agent_custid),
2026     'payname'         => &$escape_function($cust_main->payname),
2027     'company'         => &$escape_function($cust_main->company),
2028     'address1'        => &$escape_function($cust_main->address1),
2029     'address2'        => &$escape_function($cust_main->address2),
2030     'city'            => &$escape_function($cust_main->city),
2031     'state'           => &$escape_function($cust_main->state),
2032     'zip'             => &$escape_function($cust_main->zip),
2033     'fax'             => &$escape_function($cust_main->fax),
2034     'returnaddress'   => $returnaddress,
2035     #'quantity'        => 1,
2036     'terms'           => $self->terms,
2037     'template'        => $template, #params{'template'},
2038     #'notes'           => join("\n", $conf->config('invoice_latexnotes') ),
2039     # better hang on to conf_dir for a while
2040     'conf_dir'        => "$FS::UID::conf_dir/conf.$FS::UID::datasrc",
2041     'page'            => 1,
2042     'total_pages'     => 1,
2043     'current_charges' => sprintf("%.2f", $self->charged),
2044     'duedate'         => $self->due_date2str('%m/%d/%Y'), #date_format?
2045     'ship_enable'     => $conf->exists('invoice-ship_address'),
2046     'unitprices'      => $conf->exists('invoice-unitprice'),
2047   );
2048
2049   my $countrydefault = $conf->config('countrydefault') || 'US';
2050   my $prefix = $cust_main->has_ship_address ? 'ship_' : '';
2051   foreach ( qw( contact company address1 address2 city state zip country fax) ){
2052     my $method = $prefix.$_;
2053     $invoice_data{"ship_$_"} = _latex_escape($cust_main->$method);
2054   }
2055   $invoice_data{'ship_country'} = ''
2056     if ( $invoice_data{'ship_country'} eq $countrydefault );
2057   
2058   $invoice_data{'cid'} = $params{'cid'}
2059     if $params{'cid'};
2060
2061   if ( $cust_main->country eq $countrydefault ) {
2062     $invoice_data{'country'} = '';
2063   } else {
2064     $invoice_data{'country'} = &$escape_function(code2country($cust_main->country));
2065   }
2066
2067   my @address = ();
2068   $invoice_data{'address'} = \@address;
2069   push @address,
2070     $cust_main->payname.
2071       ( ( $cust_main->payby eq 'BILL' ) && $cust_main->payinfo
2072         ? " (P.O. #". $cust_main->payinfo. ")"
2073         : ''
2074       )
2075   ;
2076   push @address, $cust_main->company
2077     if $cust_main->company;
2078   push @address, $cust_main->address1;
2079   push @address, $cust_main->address2
2080     if $cust_main->address2;
2081   push @address,
2082     $cust_main->city. ", ". $cust_main->state. "  ".  $cust_main->zip;
2083   push @address, $invoice_data{'country'}
2084     if $invoice_data{'country'};
2085   push @address, ''
2086     while (scalar(@address) < 5);
2087
2088   $invoice_data{'logo_file'} = $params{'logo_file'}
2089     if $params{'logo_file'};
2090
2091   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2092 #  my( $cr_total, @cr_cust_credit ) = $self->cust_credit; #credits
2093   #my $balance_due = $self->owed + $pr_total - $cr_total;
2094   my $balance_due = $self->owed + $pr_total;
2095   $invoice_data{'previous_balance'} = sprintf("%.2f", $pr_total);
2096   $invoice_data{'balance'} = sprintf("%.2f", $balance_due);
2097
2098   my $agentnum = $self->cust_main->agentnum;
2099
2100   #do variable substitution in notes, footer, smallfooter
2101   foreach my $include (qw( notes footer smallfooter coupon )) {
2102
2103     my $inc_file = $conf->key_orbase("invoice_${format}$include", $template);
2104     my @inc_src;
2105
2106     if ( $conf->exists($inc_file, $agentnum)
2107          && length( $conf->config($inc_file, $agentnum) ) ) {
2108
2109       @inc_src = $conf->config($inc_file, $agentnum);
2110
2111     } else {
2112
2113       $inc_file = $conf->key_orbase("invoice_latex$include", $template);
2114
2115       my $convert_map = $convert_maps{$format}{$include};
2116
2117       @inc_src = map { s/\[\@--/$delimiters{$format}[0]/g;
2118                        s/--\@\]/$delimiters{$format}[1]/g;
2119                        $_;
2120                      } 
2121                  &$convert_map( $conf->config($inc_file, $agentnum) );
2122
2123     }
2124
2125     my $inc_tt = new Text::Template (
2126       TYPE       => 'ARRAY',
2127       SOURCE     => [ map "$_\n", @inc_src ],
2128       DELIMITERS => $delimiters{$format},
2129     ) or die "Can't create new Text::Template object: $Text::Template::ERROR";
2130
2131     unless ( $inc_tt->compile() ) {
2132       my $error = "Can't compile $inc_file template: $Text::Template::ERROR\n";
2133       warn $error. "Template:\n". join('', map "$_\n", @inc_src);
2134       die $error;
2135     }
2136
2137     $invoice_data{$include} = $inc_tt->fill_in( HASH => \%invoice_data );
2138
2139     $invoice_data{$include} =~ s/\n+$//
2140       if ($format eq 'latex');
2141   }
2142
2143   $invoice_data{'po_line'} =
2144     (  $cust_main->payby eq 'BILL' && $cust_main->payinfo )
2145       ? &$escape_function("Purchase Order #". $cust_main->payinfo)
2146       : $nbsp;
2147
2148   my %money_chars = ( 'latex'    => '',
2149                       'html'     => $conf->config('money_char') || '$',
2150                       'template' => '',
2151                     );
2152   my $money_char = $money_chars{$format};
2153
2154   my %other_money_chars = ( 'latex'    => '\dollar ',#XXX should be a config too
2155                             'html'     => $conf->config('money_char') || '$',
2156                             'template' => '',
2157                           );
2158   my $other_money_char = $other_money_chars{$format};
2159
2160   my @detail_items = ();
2161   my @total_items = ();
2162   my @buf = ();
2163   my @sections = ();
2164
2165   $invoice_data{'detail_items'} = \@detail_items;
2166   $invoice_data{'total_items'} = \@total_items;
2167   $invoice_data{'buf'} = \@buf;
2168   $invoice_data{'sections'} = \@sections;
2169   
2170   my $previous_section = { 'description' => 'Previous Charges',
2171                            'subtotal'    => $other_money_char.
2172                                             sprintf('%.2f', $pr_total),
2173                          };
2174
2175   my $taxtotal = 0;
2176   my $tax_section = { 'description' => 'Taxes, Surcharges, and Fees',
2177                       'subtotal'    => $taxtotal }; # adjusted below
2178
2179   my $adjusttotal = 0;
2180   my $adjust_section = { 'description' => 'Credits, Payments, and Adjustments',
2181                          'subtotal'    => 0 }; # adjusted below
2182
2183   my $unsquelched = $params{unsquelch_cdr} || $cust_main->squelch_cdr ne 'Y';
2184   my $multisection = $conf->exists('invoice_sections', $cust_main->agentnum);
2185   my $late_sections = [];
2186   if ( $multisection ) {
2187     push @sections, $self->_items_sections( $late_sections );
2188   }else{
2189     push @sections, { 'description' => '', 'subtotal' => '' };
2190   }
2191
2192   unless (    $conf->exists('disable_previous_balance')
2193            || $conf->exists('previous_balance-summary_only')
2194          )
2195   {
2196
2197     foreach my $line_item ( $self->_items_previous ) {
2198
2199       my $detail = {
2200         ext_description => [],
2201       };
2202       $detail->{'ref'} = $line_item->{'pkgnum'};
2203       $detail->{'quantity'} = 1;
2204       $detail->{'section'} = $previous_section;
2205       $detail->{'description'} = &$escape_function($line_item->{'description'});
2206       if ( exists $line_item->{'ext_description'} ) {
2207         @{$detail->{'ext_description'}} = map {
2208           &$escape_function($_);
2209         } @{$line_item->{'ext_description'}};
2210       }
2211       $detail->{'amount'} = ( $old_latex ? '' : $money_char).
2212                             $line_item->{'amount'};
2213       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2214
2215       push @detail_items, $detail;
2216       push @buf, [ $detail->{'description'},
2217                    $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2218                  ];
2219     }
2220
2221   }
2222
2223   if ( @pr_cust_bill && !$conf->exists('disable_previous_balance') ) {
2224     push @buf, ['','-----------'];
2225     push @buf, [ 'Total Previous Balance',
2226                  $money_char. sprintf("%10.2f", $pr_total) ];
2227     push @buf, ['',''];
2228   }
2229
2230   foreach my $section (@sections, @$late_sections) {
2231
2232     $section->{'subtotal'} = $other_money_char.
2233                              sprintf('%.2f', $section->{'subtotal'})
2234       if $multisection;
2235
2236     if ( $section->{'description'} ) {
2237       push @buf, ( [ &$escape_function($section->{'description'}), '' ],
2238                    [ '', '' ],
2239                  );
2240     }
2241
2242     my %options = ();
2243     $options{'section'} = $section if $multisection;
2244     $options{'format'} = $format;
2245     $options{'escape_function'} = $escape_function;
2246     $options{'format_function'} = sub { () } unless $unsquelched;
2247     $options{'unsquelched'} = $unsquelched;
2248
2249     foreach my $line_item ( $self->_items_pkg(%options) ) {
2250       my $detail = {
2251         ext_description => [],
2252       };
2253       $detail->{'ref'} = $line_item->{'pkgnum'};
2254       $detail->{'quantity'} = $line_item->{'quantity'};
2255       $detail->{'section'} = $section;
2256       $detail->{'description'} = &$escape_function($line_item->{'description'});
2257       if ( exists $line_item->{'ext_description'} ) {
2258         @{$detail->{'ext_description'}} = @{$line_item->{'ext_description'}};
2259       }
2260       $detail->{'amount'} = ( $old_latex ? '' : $money_char ).
2261                               $line_item->{'amount'};
2262       $detail->{'unit_amount'} = ( $old_latex ? '' : $money_char ).
2263                                  $line_item->{'unit_amount'};
2264       $detail->{'product_code'} = $line_item->{'pkgpart'} || 'N/A';
2265   
2266       push @detail_items, $detail;
2267       push @buf, ( [ $detail->{'description'},
2268                      $money_char. sprintf("%10.2f", $line_item->{'amount'}),
2269                    ],
2270                    map { [ " ". $_, '' ] } @{$detail->{'ext_description'}},
2271                  );
2272     }
2273
2274     if ( $section->{'description'} ) {
2275       push @buf, ( ['','-----------'],
2276                    [ $section->{'description'}. ' sub-total',
2277                       $money_char. sprintf("%10.2f", $section->{'subtotal'})
2278                    ],
2279                    [ '', '' ],
2280                    [ '', '' ],
2281                  );
2282     }
2283   
2284   }
2285   
2286   if ( $multisection && !$conf->exists('disable_previous_balance') ) {
2287     unshift @sections, $previous_section if $pr_total;
2288   }
2289
2290   foreach my $tax ( $self->_items_tax ) {
2291
2292     $taxtotal += $tax->{'amount'};
2293
2294     my $description = &$escape_function( $tax->{'description'} );
2295     my $amount      = sprintf( '%.2f', $tax->{'amount'} );
2296
2297     if ( $multisection ) {
2298
2299       my $money = $old_latex ? '' : $money_char;
2300       push @detail_items, {
2301         ext_description => [],
2302         ref          => '',
2303         quantity     => '',
2304         description  => $description,
2305         amount       => $money. $amount,
2306         product_code => '',
2307         section      => $tax_section,
2308       };
2309
2310     } else {
2311
2312       push @total_items, {
2313         'total_item'   => $description,
2314         'total_amount' => $other_money_char. $amount,
2315       };
2316
2317     }
2318
2319     push @buf,[ $description,
2320                 $money_char. $amount,
2321               ];
2322
2323   }
2324   
2325   if ( $taxtotal ) {
2326     my $total = {};
2327     $total->{'total_item'} = 'Sub-total';
2328     $total->{'total_amount'} =
2329       $other_money_char. sprintf('%.2f', $self->charged - $taxtotal );
2330
2331     if ( $multisection ) {
2332       $tax_section->{'subtotal'} = $other_money_char.
2333                                    sprintf('%.2f', $taxtotal);
2334       $tax_section->{'pretotal'} = 'New charges sub-total '.
2335                                    $total->{'total_amount'};
2336       push @sections, $tax_section if $taxtotal;
2337     }else{
2338       unshift @total_items, $total;
2339     }
2340   }
2341   $invoice_data{'taxtotal'} = sprintf('%.2f', $taxtotal);
2342   
2343   push @buf,['','-----------'];
2344   push @buf,[( $conf->exists('disable_previous_balance') 
2345                ? 'Total Charges'
2346                : 'Total New Charges'
2347              ),
2348              $money_char. sprintf("%10.2f",$self->charged) ];
2349   push @buf,['',''];
2350
2351   {
2352     my $total = {};
2353     $total->{'total_item'} = &$embolden_function('Total');
2354     $total->{'total_amount'} =
2355       &$embolden_function(
2356         $other_money_char.
2357         sprintf( '%.2f',
2358                  $self->charged + ( $conf->exists('disable_previous_balance')
2359                                     ? 0
2360                                     : $pr_total
2361                                   )
2362                )
2363       );
2364     if ( $multisection ) {
2365       $adjust_section->{'pretotal'} = 'New charges total '. $other_money_char.
2366                                       sprintf('%.2f', $self->charged );
2367     }else{
2368       push @total_items, $total;
2369     }
2370     push @buf,['','-----------'];
2371     push @buf,['Total Charges',
2372                $money_char.
2373                sprintf( '%10.2f', $self->charged +
2374                                     ( $conf->exists('disable_previous_balance')
2375                                         ? 0
2376                                         : $pr_total
2377                                     )
2378                       )
2379               ];
2380     push @buf,['',''];
2381   }
2382   
2383   unless ( $conf->exists('disable_previous_balance') ) {
2384     #foreach my $thing ( sort { $a->_date <=> $b->_date } $self->_items_credits, $self->_items_payments
2385   
2386     # credits
2387     my $credittotal = 0;
2388     foreach my $credit ( $self->_items_credits('trim_len'=>60) ) {
2389
2390       my $total;
2391       $total->{'total_item'} = &$escape_function($credit->{'description'});
2392       $credittotal += $credit->{'amount'};
2393       $total->{'total_amount'} = '-'. $other_money_char. $credit->{'amount'};
2394       $adjusttotal += $credit->{'amount'};
2395       if ( $multisection ) {
2396         my $money = $old_latex ? '' : $money_char;
2397         push @detail_items, {
2398           ext_description => [],
2399           ref          => '',
2400           quantity     => '',
2401           description  => &$escape_function($credit->{'description'}),
2402           amount       => $money. $credit->{'amount'},
2403           product_code => '',
2404           section      => $adjust_section,
2405         };
2406       } else {
2407         push @total_items, $total;
2408       }
2409
2410     }
2411     $invoice_data{'credittotal'} = sprintf('%.2f', $credittotal);
2412
2413     #credits (again)
2414     foreach my $credit ( $self->_items_credits('trim_len'=>32) ) {
2415       push @buf, [ $credit->{'description'}, $money_char.$credit->{'amount'} ];
2416     }
2417   
2418     # payments
2419     my $paymenttotal = 0;
2420     foreach my $payment ( $self->_items_payments ) {
2421       my $total = {};
2422       $total->{'total_item'} = &$escape_function($payment->{'description'});
2423       $paymenttotal += $payment->{'amount'};
2424       $total->{'total_amount'} = '-'. $other_money_char. $payment->{'amount'};
2425       $adjusttotal += $payment->{'amount'};
2426       if ( $multisection ) {
2427         my $money = $old_latex ? '' : $money_char;
2428         push @detail_items, {
2429           ext_description => [],
2430           ref          => '',
2431           quantity     => '',
2432           description  => &$escape_function($payment->{'description'}),
2433           amount       => $money. $payment->{'amount'},
2434           product_code => '',
2435           section      => $adjust_section,
2436         };
2437       }else{
2438         push @total_items, $total;
2439       }
2440       push @buf, [ $payment->{'description'},
2441                    $money_char. sprintf("%10.2f", $payment->{'amount'}),
2442                  ];
2443     }
2444     $invoice_data{'paymenttotal'} = sprintf('%.2f', $paymenttotal);
2445   
2446     if ( $multisection ) {
2447       $adjust_section->{'subtotal'} = $other_money_char.
2448                                       sprintf('%.2f', $adjusttotal);
2449       push @sections, $adjust_section;
2450     }
2451
2452     { 
2453       my $total;
2454       $total->{'total_item'} = &$embolden_function($self->balance_due_msg);
2455       $total->{'total_amount'} =
2456         &$embolden_function(
2457           $other_money_char. sprintf('%.2f', $self->owed + $pr_total )
2458         );
2459       if ( $multisection ) {
2460         $adjust_section->{'posttotal'} = $total->{'total_item'}. ' '.
2461                                          $total->{'total_amount'};
2462       }else{
2463         push @total_items, $total;
2464       }
2465       push @buf,['','-----------'];
2466       push @buf,[$self->balance_due_msg, $money_char. 
2467         sprintf("%10.2f", $balance_due ) ];
2468     }
2469   }
2470
2471   if ( $multisection ) {
2472     push @sections, @$late_sections
2473       if $unsquelched;
2474   }
2475
2476   $invoice_lines = 0;
2477   my $wasfunc = 0;
2478   foreach ( grep /invoice_lines\(\d*\)/, @invoice_template ) { #kludgy
2479     /invoice_lines\((\d*)\)/;
2480     $invoice_lines += $1 || scalar(@buf);
2481     $wasfunc=1;
2482   }
2483   die "no invoice_lines() functions in template?"
2484     if ( $format eq 'template' && !$wasfunc );
2485
2486   if ($format eq 'template') {
2487
2488     if ( $invoice_lines ) {
2489       $invoice_data{'total_pages'} = int( scalar(@buf) / $invoice_lines );
2490       $invoice_data{'total_pages'}++
2491         if scalar(@buf) % $invoice_lines;
2492     }
2493
2494     #setup subroutine for the template
2495     sub FS::cust_bill::_template::invoice_lines {
2496       my $lines = shift || scalar(@FS::cust_bill::_template::buf);
2497       map { 
2498         scalar(@FS::cust_bill::_template::buf)
2499           ? shift @FS::cust_bill::_template::buf
2500           : [ '', '' ];
2501       }
2502       ( 1 .. $lines );
2503     }
2504
2505     my $lines;
2506     my @collect;
2507     while (@buf) {
2508       push @collect, split("\n",
2509         $text_template->fill_in( HASH => \%invoice_data,
2510                                  PACKAGE => 'FS::cust_bill::_template'
2511                                )
2512       );
2513       $FS::cust_bill::_template::page++;
2514     }
2515     map "$_\n", @collect;
2516   }else{
2517     warn "filling in template for invoice ". $self->invnum. "\n"
2518       if $DEBUG;
2519     warn join("\n", map " $_ => ". $invoice_data{$_}, keys %invoice_data). "\n"
2520       if $DEBUG > 1;
2521
2522     $text_template->fill_in(HASH => \%invoice_data);
2523   }
2524 }
2525
2526 =item print_ps [ TIME [ , TEMPLATE ] ]
2527
2528 Returns an postscript invoice, as a scalar.
2529
2530 TIME an optional value used to control the printing of overdue messages.  The
2531 default is now.  It isn't the date of the invoice; that's the `_date' field.
2532 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2533 L<Time::Local> and L<Date::Parse> for conversion functions.
2534
2535 =cut
2536
2537 sub print_ps {
2538   my $self = shift;
2539
2540   my ($file, $lfile) = $self->print_latex(@_);
2541   my $ps = generate_ps($file);
2542   unlink($lfile);
2543
2544   $ps;
2545 }
2546
2547 =item print_pdf [ TIME [ , TEMPLATE ] ]
2548
2549 Returns an PDF invoice, as a scalar.
2550
2551 TIME an optional value used to control the printing of overdue messages.  The
2552 default is now.  It isn't the date of the invoice; that's the `_date' field.
2553 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2554 L<Time::Local> and L<Date::Parse> for conversion functions.
2555
2556 =cut
2557
2558 sub print_pdf {
2559   my $self = shift;
2560
2561   my ($file, $lfile) = $self->print_latex(@_);
2562   my $pdf = generate_pdf($file);
2563   unlink($lfile);
2564
2565   $pdf;
2566 }
2567
2568 =item print_html [ TIME [ , TEMPLATE [ , CID ] ] ]
2569
2570 Returns an HTML invoice, as a scalar.
2571
2572 TIME an optional value used to control the printing of overdue messages.  The
2573 default is now.  It isn't the date of the invoice; that's the `_date' field.
2574 It is specified as a UNIX timestamp; see L<perlfunc/"time">.  Also see
2575 L<Time::Local> and L<Date::Parse> for conversion functions.
2576
2577 CID is a MIME Content-ID used to create a "cid:" URL for the logo image, used
2578 when emailing the invoice as part of a multipart/related MIME email.
2579
2580 =cut
2581
2582 sub print_html {
2583   my $self = shift;
2584   my %params;
2585   if ( ref $_[0]  ) {
2586     %params = %{ shift() }; 
2587   }else{
2588     $params{'time'} = shift;
2589     $params{'template'} = shift;
2590     $params{'cid'} = shift;
2591   }
2592
2593   $params{'format'} = 'html';
2594
2595   $self->print_generic( %params );
2596 }
2597
2598 # quick subroutine for print_latex
2599 #
2600 # There are ten characters that LaTeX treats as special characters, which
2601 # means that they do not simply typeset themselves: 
2602 #      # $ % & ~ _ ^ \ { }
2603 #
2604 # TeX ignores blanks following an escaped character; if you want a blank (as
2605 # in "10% of ..."), you have to "escape" the blank as well ("10\%\ of ..."). 
2606
2607 sub _latex_escape {
2608   my $value = shift;
2609   $value =~ s/([#\$%&~_\^{}])( )?/"\\$1". ( ( defined($2) && length($2) ) ? "\\$2" : '' )/ge;
2610   $value =~ s/([<>])/\$$1\$/g;
2611   $value;
2612 }
2613
2614 #utility methods for print_*
2615
2616 sub _translate_old_latex_format {
2617   warn "_translate_old_latex_format called\n"
2618     if $DEBUG; 
2619
2620   my @template = ();
2621   while ( @_ ) {
2622     my $line = shift;
2623   
2624     if ( $line =~ /^%%Detail\s*$/ ) {
2625   
2626       push @template, q![@--!,
2627                       q!  foreach my $_tr_line (@detail_items) {!,
2628                       q!    if ( scalar ($_tr_item->{'ext_description'} ) ) {!,
2629                       q!      $_tr_line->{'description'} .= !, 
2630                       q!        "\\tabularnewline\n~~".!,
2631                       q!        join( "\\tabularnewline\n~~",!,
2632                       q!          @{$_tr_line->{'ext_description'}}!,
2633                       q!        );!,
2634                       q!    }!;
2635
2636       while ( ( my $line_item_line = shift )
2637               !~ /^%%EndDetail\s*$/                            ) {
2638         $line_item_line =~ s/'/\\'/g;    # nice LTS
2639         $line_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
2640         $line_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2641         push @template, "    \$OUT .= '$line_item_line';";
2642       }
2643   
2644       push @template, '}',
2645                       '--@]';
2646
2647     } elsif ( $line =~ /^%%TotalDetails\s*$/ ) {
2648
2649       push @template, '[@--',
2650                       '  foreach my $_tr_line (@total_items) {';
2651
2652       while ( ( my $total_item_line = shift )
2653               !~ /^%%EndTotalDetails\s*$/                      ) {
2654         $total_item_line =~ s/'/\\'/g;    # nice LTS
2655         $total_item_line =~ s/\\/\\\\/g;  # escape quotes and backslashes
2656         $total_item_line =~ s/\$(\w+)/'. \$_tr_line->{$1}. '/g;
2657         push @template, "    \$OUT .= '$total_item_line';";
2658       }
2659
2660       push @template, '}',
2661                       '--@]';
2662
2663     } else {
2664       $line =~ s/\$(\w+)/[\@-- \$$1 --\@]/g;
2665       push @template, $line;  
2666     }
2667   
2668   }
2669
2670   if ($DEBUG) {
2671     warn "$_\n" foreach @template;
2672   }
2673
2674   (@template);
2675 }
2676
2677 sub terms {
2678   my $self = shift;
2679
2680   #check for an invoice- specific override (eventually)
2681   
2682   #check for a customer- specific override
2683   return $self->cust_main->invoice_terms
2684     if $self->cust_main->invoice_terms;
2685
2686   #use configured default
2687   $conf->config('invoice_default_terms') || '';
2688 }
2689
2690 sub due_date {
2691   my $self = shift;
2692   my $duedate = '';
2693   if ( $self->terms =~ /^\s*Net\s*(\d+)\s*$/ ) {
2694     $duedate = $self->_date() + ( $1 * 86400 );
2695   }
2696   $duedate;
2697 }
2698
2699 sub due_date2str {
2700   my $self = shift;
2701   $self->due_date ? time2str(shift, $self->due_date) : '';
2702 }
2703
2704 sub balance_due_msg {
2705   my $self = shift;
2706   my $msg = 'Balance Due';
2707   return $msg unless $self->terms;
2708   if ( $self->due_date ) {
2709     $msg .= ' - Please pay by '. $self->due_date2str('%x');
2710   } elsif ( $self->terms ) {
2711     $msg .= ' - '. $self->terms;
2712   }
2713   $msg;
2714 }
2715
2716 sub balance_due_date {
2717   my $self = shift;
2718   my $duedate = '';
2719   if (    $conf->exists('invoice_default_terms') 
2720        && $conf->config('invoice_default_terms')=~ /^\s*Net\s*(\d+)\s*$/ ) {
2721     $duedate = time2str("%m/%d/%Y", $self->_date + ($1*86400) );
2722   }
2723   $duedate;
2724 }
2725
2726 =item invnum_date_pretty
2727
2728 Returns a string with the invoice number and date, for example:
2729 "Invoice #54 (3/20/2008)"
2730
2731 =cut
2732
2733 sub invnum_date_pretty {
2734   my $self = shift;
2735   'Invoice #'. $self->invnum. ' ('. $self->_date_pretty. ')';
2736 }
2737
2738 =item _date_pretty
2739
2740 Returns a string with the date, for example: "3/20/2008"
2741
2742 =cut
2743
2744 sub _date_pretty {
2745   my $self = shift;
2746   time2str('%x', $self->_date);
2747 }
2748
2749 sub _items_sections {
2750   my $self = shift;
2751   my $late = shift;
2752
2753   my %s = ();
2754   my %l = ();
2755
2756   foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2757   {
2758
2759     if ( $cust_bill_pkg->pkgnum > 0 ) {
2760       my $usage = $cust_bill_pkg->usage;
2761
2762       foreach my $display ($cust_bill_pkg->cust_bill_pkg_display) {
2763         my $desc = $display->section;
2764         my $type = $display->type;
2765
2766         if ( $display->post_total ) {
2767           if (! $type || $type eq 'S') {
2768             $l{$desc} += $cust_bill_pkg->setup
2769               if ( $cust_bill_pkg->setup != 0 );
2770           }
2771
2772           if (! $type) {
2773             $l{$desc} += $cust_bill_pkg->recur
2774               if ( $cust_bill_pkg->recur != 0 );
2775           }
2776
2777           if ($type && $type eq 'R') {
2778             $l{$desc} += $cust_bill_pkg->recur - $usage
2779               if ( $cust_bill_pkg->recur != 0 );
2780           }
2781           
2782           if ($type && $type eq 'U') {
2783             $l{$desc} += $usage;
2784           }
2785
2786         } else {
2787           if (! $type || $type eq 'S') {
2788             $s{$desc} += $cust_bill_pkg->setup
2789               if ( $cust_bill_pkg->setup != 0 );
2790           }
2791
2792           if (! $type) {
2793             $s{$desc} += $cust_bill_pkg->recur
2794               if ( $cust_bill_pkg->recur != 0 );
2795           }
2796
2797           if ($type && $type eq 'R') {
2798             $s{$desc} += $cust_bill_pkg->recur - $usage
2799               if ( $cust_bill_pkg->recur != 0 );
2800           }
2801           
2802           if ($type && $type eq 'U') {
2803             $s{$desc} += $usage;
2804           }
2805
2806         }
2807
2808       }
2809
2810     }
2811
2812   }
2813
2814   push @$late, map { { 'description' => $_,
2815                        'subtotal'    => $l{$_},
2816                        'post_total'  => 1,
2817                    } } sort keys %l;
2818
2819   map { {'description' => $_, 'subtotal' => $s{$_}} } sort keys %s;
2820
2821 }
2822
2823 sub _items {
2824   my $self = shift;
2825
2826   #my @display = scalar(@_)
2827   #              ? @_
2828   #              : qw( _items_previous _items_pkg );
2829   #              #: qw( _items_pkg );
2830   #              #: qw( _items_previous _items_pkg _items_tax _items_credits _items_payments );
2831   my @display = qw( _items_previous _items_pkg );
2832
2833   my @b = ();
2834   foreach my $display ( @display ) {
2835     push @b, $self->$display(@_);
2836   }
2837   @b;
2838 }
2839
2840 sub _items_previous {
2841   my $self = shift;
2842   my $cust_main = $self->cust_main;
2843   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2844   my @b = ();
2845   foreach ( @pr_cust_bill ) {
2846     push @b, {
2847       'description' => 'Previous Balance, Invoice #'. $_->invnum. 
2848                        ' ('. time2str('%x',$_->_date). ')',
2849       #'pkgpart'     => 'N/A',
2850       'pkgnum'      => 'N/A',
2851       'amount'      => sprintf("%.2f", $_->owed),
2852     };
2853   }
2854   @b;
2855
2856   #{
2857   #    'description'     => 'Previous Balance',
2858   #    #'pkgpart'         => 'N/A',
2859   #    'pkgnum'          => 'N/A',
2860   #    'amount'          => sprintf("%10.2f", $pr_total ),
2861   #    'ext_description' => [ map {
2862   #                                 "Invoice ". $_->invnum.
2863   #                                 " (". time2str("%x",$_->_date). ") ".
2864   #                                 sprintf("%10.2f", $_->owed)
2865   #                         } @pr_cust_bill ],
2866
2867   #};
2868 }
2869
2870 sub _items_pkg {
2871   my $self = shift;
2872   my @cust_bill_pkg = grep { $_->pkgnum } $self->cust_bill_pkg;
2873   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2874 }
2875
2876 sub _taxsort {
2877   return 0 unless $a cmp $b;
2878   return -1 if $b eq 'Tax';
2879   return 1 if $a eq 'Tax';
2880   return -1 if $b eq 'Other surcharges';
2881   return 1 if $a eq 'Other surcharges';
2882   $a cmp $b;
2883 }
2884
2885 sub _items_tax {
2886   my $self = shift;
2887   my @cust_bill_pkg = sort _taxsort grep { ! $_->pkgnum } $self->cust_bill_pkg;
2888   $self->_items_cust_bill_pkg(\@cust_bill_pkg, @_);
2889 }
2890
2891 sub _items_cust_bill_pkg {
2892   my $self = shift;
2893   my $cust_bill_pkg = shift;
2894   my %opt = @_;
2895
2896   my $format = $opt{format} || '';
2897   my $escape_function = $opt{escape_function} || sub { shift };
2898   my $format_function = $opt{format_function} || '';
2899   my $unsquelched = $opt{unsquelched} || '';
2900   my $section = $opt{section}->{description} if $opt{section};
2901
2902   my @b = ();
2903   my ($s, $r, $u) = ( undef, undef, undef );
2904   foreach my $cust_bill_pkg ( @$cust_bill_pkg )
2905   {
2906
2907     foreach ( $s, $r, $u ) {
2908       if ( $_ && !$cust_bill_pkg->hidden ) {
2909         $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
2910         $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
2911         push @b, { %$_ };
2912         $_ = undef;
2913       }
2914     }
2915
2916     foreach my $display ( grep { defined($section)
2917                                  ? $_->section eq $section
2918                                  : 1
2919                                }
2920                           $cust_bill_pkg->cust_bill_pkg_display
2921                         )
2922     {
2923
2924       my $type = $display->type;
2925
2926       my $cust_pkg = $cust_bill_pkg->cust_pkg;
2927
2928       my $desc = $cust_bill_pkg->desc;
2929       $desc = substr($desc, 0, 50). '...'
2930         if $format eq 'latex' && length($desc) > 50;
2931
2932       my %details_opt = ( 'format'          => $format,
2933                           'escape_function' => $escape_function,
2934                           'format_function' => $format_function,
2935                         );
2936
2937       if ( $cust_bill_pkg->pkgnum > 0 ) {
2938
2939         if ( $cust_bill_pkg->setup != 0 && (!$type || $type eq 'S') ) {
2940
2941           my $description = $desc;
2942           $description .= ' Setup' if $cust_bill_pkg->recur != 0;
2943
2944           my @d = ();
2945           push @d, map &{$escape_function}($_),
2946                        $cust_pkg->h_labels_short($self->_date)
2947             unless $cust_pkg->part_pkg->hide_svc_detail
2948                 || $cust_bill_pkg->hidden;
2949           push @d, $cust_bill_pkg->details(%details_opt)
2950             if $cust_bill_pkg->recur == 0;
2951
2952           if ( $cust_bill_pkg->hidden ) {
2953             $s->{amount}      += $cust_bill_pkg->setup;
2954             $s->{unit_amount} += $cust_bill_pkg->unitsetup;
2955             push @{ $s->{ext_description} }, @d;
2956           } else {
2957             $s = {
2958               description     => $description,
2959               #pkgpart         => $part_pkg->pkgpart,
2960               pkgnum          => $cust_bill_pkg->pkgnum,
2961               amount          => $cust_bill_pkg->setup,
2962               unit_amount     => $cust_bill_pkg->unitsetup,
2963               quantity        => $cust_bill_pkg->quantity,
2964               ext_description => \@d,
2965             };
2966           };
2967
2968         }
2969
2970         if ( $cust_bill_pkg->recur != 0 &&
2971              ( !$type || $type eq 'R' || $type eq 'U' )
2972            )
2973         {
2974
2975           my $is_summary = $display->summary;
2976           my $description = $is_summary ? "Usage charges" : $desc;
2977
2978           unless ( $conf->exists('disable_line_item_date_ranges') ) {
2979             $description .= " (" . time2str("%x", $cust_bill_pkg->sdate).
2980                             " - ". time2str("%x", $cust_bill_pkg->edate). ")";
2981           }
2982
2983           my @d = ();
2984
2985           #at least until cust_bill_pkg has "past" ranges in addition to
2986           #the "future" sdate/edate ones... see #3032
2987           my @dates = ( $self->_date );
2988           my $prev = $cust_bill_pkg->previous_cust_bill_pkg;
2989           push @dates, $prev->sdate if $prev;
2990
2991           push @d, map &{$escape_function}($_),
2992                        $cust_pkg->h_labels_short(@dates)
2993                                                  #$cust_bill_pkg->edate,
2994                                                  #$cust_bill_pkg->sdate)
2995             unless $cust_pkg->part_pkg->hide_svc_detail
2996                 || $cust_bill_pkg->itemdesc
2997                 || $cust_bill_pkg->hidden
2998                 || $is_summary;
2999
3000           push @d, $cust_bill_pkg->details(%details_opt)
3001             unless ($is_summary || $type && $type eq 'R');
3002   
3003           my $amount = 0;
3004           if (!$type) {
3005             $amount = $cust_bill_pkg->recur;
3006           }elsif($type eq 'R') {
3007             $amount = $cust_bill_pkg->recur - $cust_bill_pkg->usage;
3008           }elsif($type eq 'U') {
3009             $amount = $cust_bill_pkg->usage;
3010           }
3011   
3012           if ( !$type || $type eq 'R' ) {
3013
3014             if ( $cust_bill_pkg->hidden ) {
3015               $r->{amount}      += $amount;
3016               $r->{unit_amount} += $cust_bill_pkg->unitrecur;
3017               push @{ $r->{ext_description} }, @d;
3018             } else {
3019               $r = {
3020                 description     => $description,
3021                 #pkgpart         => $part_pkg->pkgpart,
3022                 pkgnum          => $cust_bill_pkg->pkgnum,
3023                 amount          => $amount,
3024                 unit_amount     => $cust_bill_pkg->unitrecur,
3025                 quantity        => $cust_bill_pkg->quantity,
3026                 ext_description => \@d,
3027               };
3028             }
3029
3030           } elsif ( $amount ) {  # && $type eq 'U'
3031
3032             if ( $cust_bill_pkg->hidden ) {
3033               $u->{amount}      += $amount;
3034               $u->{unit_amount} += $cust_bill_pkg->unitrecur;
3035               push @{ $u->{ext_description} }, @d;
3036             } else {
3037               $u = {
3038                 description     => $description,
3039                 #pkgpart         => $part_pkg->pkgpart,
3040                 pkgnum          => $cust_bill_pkg->pkgnum,
3041                 amount          => $amount,
3042                 unit_amount     => $cust_bill_pkg->unitrecur,
3043                 quantity        => $cust_bill_pkg->quantity,
3044                 ext_description => \@d,
3045               };
3046             }
3047
3048           }
3049
3050         } # recurring or usage with recurring charge
3051
3052       } else { #pkgnum tax or one-shot line item (??)
3053
3054         if ( $cust_bill_pkg->setup != 0 ) {
3055           push @b, {
3056             'description' => $desc,
3057             'amount'      => sprintf("%.2f", $cust_bill_pkg->setup),
3058           };
3059         }
3060         if ( $cust_bill_pkg->recur != 0 ) {
3061           push @b, {
3062             'description' => "$desc (".
3063                              time2str("%x", $cust_bill_pkg->sdate). ' - '.
3064                              time2str("%x", $cust_bill_pkg->edate). ')',
3065             'amount'      => sprintf("%.2f", $cust_bill_pkg->recur),
3066           };
3067         }
3068
3069       }
3070
3071     }
3072
3073   }
3074
3075   foreach ( $s, $r, $u ) {
3076     if ( $_ ) {
3077       $_->{amount}      = sprintf( "%.2f", $_->{amount} ),
3078       $_->{unit_amount} = sprintf( "%.2f", $_->{unit_amount} ),
3079       push @b, { %$_ };
3080     }
3081   }
3082
3083   @b;
3084
3085 }
3086
3087 sub _items_credits {
3088   my( $self, %opt ) = @_;
3089   my $trim_len = $opt{'trim_len'} || 60;
3090
3091   my @b;
3092   #credits
3093   foreach ( $self->cust_credited ) {
3094
3095     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
3096
3097     my $reason = substr($_->cust_credit->reason, 0, $trim_len);
3098     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
3099     $reason = " ($reason) " if $reason;
3100
3101     push @b, {
3102       #'description' => 'Credit ref\#'. $_->crednum.
3103       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
3104       #                 $reason,
3105       'description' => 'Credit applied '.
3106                        time2str("%x",$_->cust_credit->_date). $reason,
3107       'amount'      => sprintf("%.2f",$_->amount),
3108     };
3109   }
3110
3111   @b;
3112
3113 }
3114
3115 sub _items_payments {
3116   my $self = shift;
3117
3118   my @b;
3119   #get & print payments
3120   foreach ( $self->cust_bill_pay ) {
3121
3122     #something more elaborate if $_->amount ne ->cust_pay->paid ?
3123
3124     push @b, {
3125       'description' => "Payment received ".
3126                        time2str("%x",$_->cust_pay->_date ),
3127       'amount'      => sprintf("%.2f", $_->amount )
3128     };
3129   }
3130
3131   @b;
3132
3133 }
3134
3135 =item call_details [ OPTION => VALUE ... ]
3136
3137 Returns an array of CSV strings representing the call details for this invoice
3138 The only option available is the boolean prepend_billed_number
3139
3140 =cut
3141
3142 sub call_details {
3143   my ($self, %opt) = @_;
3144
3145   my $format_function = sub { shift };
3146
3147   if ($opt{prepend_billed_number}) {
3148     $format_function = sub {
3149       my $detail = shift;
3150       my $row = shift;
3151
3152       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
3153       
3154     };
3155   }
3156
3157   my @details = map { $_->details( 'format_function' => $format_function,
3158                                    'escape_function' => sub{ return() },
3159                                  )
3160                     }
3161                   grep { $_->pkgnum }
3162                   $self->cust_bill_pkg;
3163   my $header = $details[0];
3164   ( $header, grep { $_ ne $header } @details );
3165 }
3166
3167
3168 =back
3169
3170 =head1 SUBROUTINES
3171
3172 =over 4
3173
3174 =item process_reprint
3175
3176 =cut
3177
3178 sub process_reprint {
3179   process_re_X('print', @_);
3180 }
3181
3182 =item process_reemail
3183
3184 =cut
3185
3186 sub process_reemail {
3187   process_re_X('email', @_);
3188 }
3189
3190 =item process_refax
3191
3192 =cut
3193
3194 sub process_refax {
3195   process_re_X('fax', @_);
3196 }
3197
3198 =item process_reftp
3199
3200 =cut
3201
3202 sub process_reftp {
3203   process_re_X('ftp', @_);
3204 }
3205
3206 =item respool
3207
3208 =cut
3209
3210 sub process_respool {
3211   process_re_X('spool', @_);
3212 }
3213
3214 use Storable qw(thaw);
3215 use Data::Dumper;
3216 use MIME::Base64;
3217 sub process_re_X {
3218   my( $method, $job ) = ( shift, shift );
3219   warn "$me process_re_X $method for job $job\n" if $DEBUG;
3220
3221   my $param = thaw(decode_base64(shift));
3222   warn Dumper($param) if $DEBUG;
3223
3224   re_X(
3225     $method,
3226     $job,
3227     %$param,
3228   );
3229
3230 }
3231
3232 sub re_X {
3233   my($method, $job, %param ) = @_;
3234   if ( $DEBUG ) {
3235     warn "re_X $method for job $job with param:\n".
3236          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
3237   }
3238
3239   #some false laziness w/search/cust_bill.html
3240   my $distinct = '';
3241   my $orderby = 'ORDER BY cust_bill._date';
3242
3243   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql(\%param);
3244
3245   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
3246      
3247   my @cust_bill = qsearch( {
3248     #'select'    => "cust_bill.*",
3249     'table'     => 'cust_bill',
3250     'addl_from' => $addl_from,
3251     'hashref'   => {},
3252     'extra_sql' => $extra_sql,
3253     'order_by'  => $orderby,
3254     'debug' => 1,
3255   } );
3256
3257   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3258
3259   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3260     if $DEBUG;
3261
3262   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3263   foreach my $cust_bill ( @cust_bill ) {
3264     $cust_bill->$method();
3265
3266     if ( $job ) { #progressbar foo
3267       $num++;
3268       if ( time - $min_sec > $last ) {
3269         my $error = $job->update_statustext(
3270           int( 100 * $num / scalar(@cust_bill) )
3271         );
3272         die $error if $error;
3273         $last = time;
3274       }
3275     }
3276
3277   }
3278
3279 }
3280
3281 =back
3282
3283 =head1 CLASS METHODS
3284
3285 =over 4
3286
3287 =item owed_sql
3288
3289 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3290
3291 =cut
3292
3293 sub owed_sql {
3294   my $class = shift;
3295   'charged - '. $class->paid_sql. ' - '. $class->credited_sql;
3296 }
3297
3298 =item net_sql
3299
3300 Returns an SQL fragment to retreive the net amount (charged minus credited).
3301
3302 =cut
3303
3304 sub net_sql {
3305   my $class = shift;
3306   'charged - '. $class->credited_sql;
3307 }
3308
3309 =item paid_sql
3310
3311 Returns an SQL fragment to retreive the amount paid against this invoice.
3312
3313 =cut
3314
3315 sub paid_sql {
3316   #my $class = shift;
3317   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3318        WHERE cust_bill.invnum = cust_bill_pay.invnum   )";
3319 }
3320
3321 =item credited_sql
3322
3323 Returns an SQL fragment to retreive the amount credited against this invoice.
3324
3325 =cut
3326
3327 sub credited_sql {
3328   #my $class = shift;
3329   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3330        WHERE cust_bill.invnum = cust_credit_bill.invnum   )";
3331 }
3332
3333 =item search_sql HASHREF
3334
3335 Class method which returns an SQL WHERE fragment to search for parameters
3336 specified in HASHREF.  Valid parameters are
3337
3338 =over 4
3339
3340 =item begin
3341
3342 Epoch date (UNIX timestamp) setting a lower bound for _date values
3343
3344 =item end
3345
3346 Epoch date (UNIX timestamp) setting an upper bound for _date values
3347
3348 =item invnum_min
3349
3350 =item invnum_max
3351
3352 =item agentnum
3353
3354 =item owed
3355
3356 =item net
3357
3358 =item days
3359
3360 =item newest_percust
3361
3362 =back
3363
3364 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3365
3366 =cut
3367
3368 sub search_sql {
3369   my($class, $param) = @_;
3370   if ( $DEBUG ) {
3371     warn "$me search_sql called with params: \n".
3372          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
3373   }
3374
3375   my @search = ();
3376
3377   if ( $param->{'begin'} =~ /^(\d+)$/ ) {
3378     push @search, "cust_bill._date >= $1";
3379   }
3380   if ( $param->{'end'} =~ /^(\d+)$/ ) {
3381     push @search, "cust_bill._date < $1";
3382   }
3383   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3384     push @search, "cust_bill.invnum >= $1";
3385   }
3386   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3387     push @search, "cust_bill.invnum <= $1";
3388   }
3389   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3390     push @search, "cust_main.agentnum = $1";
3391   }
3392
3393   push @search, '0 != '. FS::cust_bill->owed_sql
3394     if $param->{'open'};
3395
3396   push @search, '0 != '. FS::cust_bill->net_sql
3397     if $param->{'net'};
3398
3399   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3400     if $param->{'days'};
3401
3402   if ( $param->{'newest_percust'} ) {
3403
3404     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3405     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3406
3407     my @newest_where = map { my $x = $_;
3408                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
3409                              $x;
3410                            }
3411                            grep ! /^cust_main./, @search;
3412     my $newest_where = scalar(@newest_where)
3413                          ? ' AND '. join(' AND ', @newest_where)
3414                          : '';
3415
3416
3417     push @search, "cust_bill._date = (
3418       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3419         WHERE newest_cust_bill.custnum = cust_bill.custnum
3420           $newest_where
3421     )";
3422
3423   }
3424
3425   my $curuser = $FS::CurrentUser::CurrentUser;
3426   if ( $curuser->username eq 'fs_queue'
3427        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3428     my $username = $1;
3429     my $newuser = qsearchs('access_user', {
3430       'username' => $username,
3431       'disabled' => '',
3432     } );
3433     if ( $newuser ) {
3434       $curuser = $newuser;
3435     } else {
3436       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3437     }
3438   }
3439
3440   push @search, $curuser->agentnums_sql;
3441
3442   join(' AND ', @search );
3443
3444 }
3445
3446 =back
3447
3448 =head1 BUGS
3449
3450 The delete method.
3451
3452 =head1 SEE ALSO
3453
3454 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3455 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
3456 documentation.
3457
3458 =cut
3459
3460 1;
3461