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