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