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