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