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