internal doc updates, #30003
[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 =cut
1569
1570 sub batch_invoice {
1571   my ($self, $opt) = @_;
1572   my $bill_batch = $self->get_open_bill_batch;
1573   my $cust_bill_batch = FS::cust_bill_batch->new({
1574       batchnum => $bill_batch->batchnum,
1575       invnum   => $self->invnum,
1576   });
1577   return $cust_bill_batch->insert($opt);
1578 }
1579
1580 =item get_open_batch
1581
1582 Returns the currently open batch as an FS::bill_batch object, creating a new
1583 one if necessary.  (A per-agent batch if invoice_print_pdf-spoolagent is
1584 enabled)
1585
1586 =cut
1587
1588 sub get_open_bill_batch {
1589   my $self = shift;
1590   my $conf = $self->conf;
1591   my $hashref = { status => 'O' };
1592   $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1593                              ? $self->cust_main->agentnum
1594                              : '';
1595   my $batch = qsearchs('bill_batch', $hashref);
1596   return $batch if $batch;
1597   $batch = FS::bill_batch->new($hashref);
1598   my $error = $batch->insert;
1599   die $error if $error;
1600   return $batch;
1601 }
1602
1603 =item ftp_invoice [ TEMPLATENAME ] 
1604
1605 Sends this invoice data via FTP.
1606
1607 TEMPLATENAME is unused?
1608
1609 =cut
1610
1611 sub ftp_invoice {
1612   my $self = shift;
1613   my $conf = $self->conf;
1614   my $template = scalar(@_) ? shift : '';
1615
1616   $self->send_csv(
1617     'protocol'   => 'ftp',
1618     'server'     => $conf->config('cust_bill-ftpserver'),
1619     'username'   => $conf->config('cust_bill-ftpusername'),
1620     'password'   => $conf->config('cust_bill-ftppassword'),
1621     'dir'        => $conf->config('cust_bill-ftpdir'),
1622     'format'     => $conf->config('cust_bill-ftpformat'),
1623   );
1624 }
1625
1626 =item spool_invoice [ TEMPLATENAME ] 
1627
1628 Spools this invoice data (see L<FS::spool_csv>)
1629
1630 TEMPLATENAME is unused?
1631
1632 =cut
1633
1634 sub spool_invoice {
1635   my $self = shift;
1636   my $conf = $self->conf;
1637   my $template = scalar(@_) ? shift : '';
1638
1639   $self->spool_csv(
1640     'format'       => $conf->config('cust_bill-spoolformat'),
1641     'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1642   );
1643 }
1644
1645 =item send_csv OPTION => VALUE, ...
1646
1647 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1648
1649 Options are:
1650
1651 protocol - currently only "ftp"
1652 server
1653 username
1654 password
1655 dir
1656
1657 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1658 and YYMMDDHHMMSS is a timestamp.
1659
1660 See L</print_csv> for a description of the output format.
1661
1662 =cut
1663
1664 sub send_csv {
1665   my($self, %opt) = @_;
1666
1667   #create file(s)
1668
1669   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1670   mkdir $spooldir, 0700 unless -d $spooldir;
1671
1672   # don't localize dates here, they're a defined format
1673   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1674   my $file = "$spooldir/$tracctnum.csv";
1675   
1676   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1677
1678   open(CSV, ">$file") or die "can't open $file: $!";
1679   print CSV $header;
1680
1681   print CSV $detail;
1682
1683   close CSV;
1684
1685   my $net;
1686   if ( $opt{protocol} eq 'ftp' ) {
1687     eval "use Net::FTP;";
1688     die $@ if $@;
1689     $net = Net::FTP->new($opt{server}) or die @$;
1690   } else {
1691     die "unknown protocol: $opt{protocol}";
1692   }
1693
1694   $net->login( $opt{username}, $opt{password} )
1695     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1696
1697   $net->binary or die "can't set binary mode";
1698
1699   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1700
1701   $net->put($file) or die "can't put $file: $!";
1702
1703   $net->quit;
1704
1705   unlink $file;
1706
1707 }
1708
1709 =item spool_csv
1710
1711 Spools CSV invoice data.
1712
1713 Options are:
1714
1715 =over 4
1716
1717 =item format - any of FS::Misc::::Invoicing::spool_formats
1718
1719 =item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the
1720 customer has the corresponding invoice destinations set (see
1721 L<FS::cust_main_invoice>).
1722
1723 =item agent_spools - if set to a true value, will spool to per-agent files
1724 rather than a single global file
1725
1726 =item upload_targetnum - if set to a target (see L<FS::upload_target>), will
1727 append to that spool.  L<FS::Cron::upload> will then send the spool file to
1728 that destination.
1729
1730 =item balanceover - if set, only spools the invoice if the total amount owed on
1731 this invoice and all older invoices is greater than the specified amount.
1732
1733 =item time - the "current time".  Controls the printing of past due messages
1734 in the ICS format.
1735
1736 =back
1737
1738 =cut
1739
1740 sub spool_csv {
1741   my($self, %opt) = @_;
1742
1743   my $time = $opt{'time'} || time;
1744   my $cust_main = $self->cust_main;
1745
1746   if ( $opt{'dest'} ) {
1747     my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1748                              $cust_main->invoicing_list;
1749     return 'N/A' unless $invoicing_list{$opt{'dest'}}
1750                      || ! keys %invoicing_list;
1751   }
1752
1753   if ( $opt{'balanceover'} ) {
1754     return 'N/A'
1755       if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1756   }
1757
1758   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1759   mkdir $spooldir, 0700 unless -d $spooldir;
1760
1761   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', $time);
1762
1763   my $file;
1764   if ( $opt{'agent_spools'} ) {
1765     $file = 'agentnum'.$cust_main->agentnum;
1766   } else {
1767     $file = 'spool';
1768   }
1769
1770   if ( $opt{'upload_targetnum'} ) {
1771     $spooldir .= '/target'.$opt{'upload_targetnum'};
1772     mkdir $spooldir, 0700 unless -d $spooldir;
1773   } # otherwise it just goes into export.xxx/cust_bill
1774
1775   if ( lc($opt{'format'}) eq 'billco' ) {
1776     $file .= '-header';
1777   }
1778
1779   $file = "$spooldir/$file.csv";
1780   
1781   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum);
1782
1783   open(CSV, ">>$file") or die "can't open $file: $!";
1784   flock(CSV, LOCK_EX);
1785   seek(CSV, 0, 2);
1786
1787   print CSV $header;
1788
1789   if ( lc($opt{'format'}) eq 'billco' ) {
1790
1791     flock(CSV, LOCK_UN);
1792     close CSV;
1793
1794     $file =~ s/-header.csv$/-detail.csv/;
1795
1796     open(CSV,">>$file") or die "can't open $file: $!";
1797     flock(CSV, LOCK_EX);
1798     seek(CSV, 0, 2);
1799   }
1800
1801   print CSV $detail if defined($detail);
1802
1803   flock(CSV, LOCK_UN);
1804   close CSV;
1805
1806   return '';
1807
1808 }
1809
1810 =item print_csv OPTION => VALUE, ...
1811
1812 Returns CSV data for this invoice.
1813
1814 Options are:
1815
1816 format - 'default', 'billco', 'oneline', 'bridgestone'
1817
1818 Returns a list consisting of two scalars.  The first is a single line of CSV
1819 header information for this invoice.  The second is one or more lines of CSV
1820 detail information for this invoice.
1821
1822 If I<format> is not specified or "default", the fields of the CSV file are as
1823 follows:
1824
1825 record_type, invnum, custnum, _date, charged, first, last, company, address1, 
1826 address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1827
1828 =over 4
1829
1830 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1831
1832 B<record_type> is C<cust_bill> for the initial header line only.  The
1833 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1834 fields are filled in.
1835
1836 B<record_type> is C<cust_bill_pkg> for detail lines.  Only the first two fields
1837 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1838 are filled in.
1839
1840 =item invnum - invoice number
1841
1842 =item custnum - customer number
1843
1844 =item _date - invoice date
1845
1846 =item charged - total invoice amount
1847
1848 =item first - customer first name
1849
1850 =item last - customer first name
1851
1852 =item company - company name
1853
1854 =item address1 - address line 1
1855
1856 =item address2 - address line 1
1857
1858 =item city
1859
1860 =item state
1861
1862 =item zip
1863
1864 =item country
1865
1866 =item pkg - line item description
1867
1868 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1869
1870 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1871
1872 =item sdate - start date for recurring fee
1873
1874 =item edate - end date for recurring fee
1875
1876 =back
1877
1878 If I<format> is "billco", the fields of the header CSV file are as follows:
1879
1880   +-------------------------------------------------------------------+
1881   |                        FORMAT HEADER FILE                         |
1882   |-------------------------------------------------------------------|
1883   | Field | Description                   | Name       | Type | Width |
1884   | 1     | N/A-Leave Empty               | RC         | CHAR |     2 |
1885   | 2     | N/A-Leave Empty               | CUSTID     | CHAR |    15 |
1886   | 3     | Transaction Account No        | TRACCTNUM  | CHAR |    15 |
1887   | 4     | Transaction Invoice No        | TRINVOICE  | CHAR |    15 |
1888   | 5     | Transaction Zip Code          | TRZIP      | CHAR |     5 |
1889   | 6     | Transaction Company Bill To   | TRCOMPANY  | CHAR |    30 |
1890   | 7     | Transaction Contact Bill To   | TRNAME     | CHAR |    30 |
1891   | 8     | Additional Address Unit Info  | TRADDR1    | CHAR |    30 |
1892   | 9     | Bill To Street Address        | TRADDR2    | CHAR |    30 |
1893   | 10    | Ancillary Billing Information | TRADDR3    | CHAR |    30 |
1894   | 11    | Transaction City Bill To      | TRCITY     | CHAR |    20 |
1895   | 12    | Transaction State Bill To     | TRSTATE    | CHAR |     2 |
1896   | 13    | Bill Cycle Close Date         | CLOSEDATE  | CHAR |    10 |
1897   | 14    | Bill Due Date                 | DUEDATE    | CHAR |    10 |
1898   | 15    | Previous Balance              | BALFWD     | NUM* |     9 |
1899   | 16    | Pmt/CR Applied                | CREDAPPLY  | NUM* |     9 |
1900   | 17    | Total Current Charges         | CURRENTCHG | NUM* |     9 |
1901   | 18    | Total Amt Due                 | TOTALDUE   | NUM* |     9 |
1902   | 19    | Total Amt Due                 | AMTDUE     | NUM* |     9 |
1903   | 20    | 30 Day Aging                  | AMT30      | NUM* |     9 |
1904   | 21    | 60 Day Aging                  | AMT60      | NUM* |     9 |
1905   | 22    | 90 Day Aging                  | AMT90      | NUM* |     9 |
1906   | 23    | Y/N                           | AGESWITCH  | CHAR |     1 |
1907   | 24    | Remittance automation         | SCANLINE   | CHAR |   100 |
1908   | 25    | Total Taxes & Fees            | TAXTOT     | NUM* |     9 |
1909   | 26    | Customer Reference Number     | CUSTREF    | CHAR |    15 |
1910   | 27    | Federal Tax***                | FEDTAX     | NUM* |     9 |
1911   | 28    | State Tax***                  | STATETAX   | NUM* |     9 |
1912   | 29    | Other Taxes & Fees***         | OTHERTAX   | NUM* |     9 |
1913   +-------+-------------------------------+------------+------+-------+
1914
1915 If I<format> is "billco", the fields of the detail CSV file are as follows:
1916
1917                                   FORMAT FOR DETAIL FILE
1918         |                            |           |      |
1919   Field | Description                | Name      | Type | Width
1920   1     | N/A-Leave Empty            | RC        | CHAR |     2
1921   2     | N/A-Leave Empty            | CUSTID    | CHAR |    15
1922   3     | Account Number             | TRACCTNUM | CHAR |    15
1923   4     | Invoice Number             | TRINVOICE | CHAR |    15
1924   5     | Line Sequence (sort order) | LINESEQ   | NUM  |     6
1925   6     | Transaction Detail         | DETAILS   | CHAR |   100
1926   7     | Amount                     | AMT       | NUM* |     9
1927   8     | Line Format Control**      | LNCTRL    | CHAR |     2
1928   9     | Grouping Code              | GROUP     | CHAR |     2
1929   10    | User Defined               | ACCT CODE | CHAR |    15
1930
1931 If format is 'oneline', there is no detail file.  Each invoice has a 
1932 header line only, with the fields:
1933
1934 Agent number, agent name, customer number, first name, last name, address
1935 line 1, address line 2, city, state, zip, invoice date, invoice number,
1936 amount charged, amount due, previous balance, due date.
1937
1938 and then, for each line item, three columns containing the package number,
1939 description, and amount.
1940
1941 If format is 'bridgestone', there is no detail file.  Each invoice has a 
1942 header line with the following fields in a fixed-width format:
1943
1944 Customer number (in display format), date, name (first last), company,
1945 address 1, address 2, city, state, zip.
1946
1947 This is a mailing list format, and has no per-invoice fields.  To avoid
1948 sending redundant notices, the spooling event should have a "once" or 
1949 "once_percust_every" condition.
1950
1951 =cut
1952
1953 sub print_csv {
1954   my($self, %opt) = @_;
1955   
1956   eval "use Text::CSV_XS";
1957   die $@ if $@;
1958
1959   my $cust_main = $self->cust_main;
1960
1961   my $csv = Text::CSV_XS->new({'always_quote'=>1});
1962   my $format = lc($opt{'format'});
1963
1964   my $time = $opt{'time'} || time;
1965
1966   my $tracctnum = ''; #leaking out from billco-specific sections :/
1967   if ( $format eq 'billco' ) {
1968
1969     my $account_num =
1970       $self->conf->config('billco-account_num', $cust_main->agentnum);
1971
1972     $tracctnum = $account_num eq 'display_custnum'
1973                    ? $cust_main->display_custnum
1974                    : $opt{'tracctnum'};
1975
1976     my $taxtotal = 0;
1977     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1978
1979     my $duedate = $self->due_date2str('%m/%d/%Y'); # hardcoded, NOT date_format
1980
1981     my( $previous_balance, @unused ) = $self->previous; #previous balance
1982
1983     my $pmt_cr_applied = 0;
1984     $pmt_cr_applied += $_->{'amount'}
1985       foreach ( $self->_items_payments(%opt), $self->_items_credits(%opt) ) ;
1986
1987     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1988
1989     $csv->combine(
1990       '',                         #  1 | N/A-Leave Empty               CHAR   2
1991       '',                         #  2 | N/A-Leave Empty               CHAR  15
1992       $tracctnum,                 #  3 | Transaction Account No        CHAR  15
1993       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
1994       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
1995       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
1996       #$cust_main->payname,        #  7 | Transaction Contact Bill To   CHAR  30
1997       $cust_main->contact,        #  7 | Transaction Contact Bill To   CHAR  30
1998       $cust_main->address2,       #  8 | Additional Address Unit Info  CHAR  30
1999       $cust_main->address1,       #  9 | Bill To Street Address        CHAR  30
2000       '',                         # 10 | Ancillary Billing Information CHAR  30
2001       $cust_main->city,           # 11 | Transaction City Bill To      CHAR  20
2002       $cust_main->state,          # 12 | Transaction State Bill To     CHAR   2
2003
2004       # XXX ?
2005       time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR  10
2006
2007       # XXX ?
2008       $duedate,                   # 14 | Bill Due Date                 CHAR  10
2009
2010       $previous_balance,          # 15 | Previous Balance              NUM*   9
2011       $pmt_cr_applied,            # 16 | Pmt/CR Applied                NUM*   9
2012       sprintf("%.2f", $self->charged), # 17 | Total Current Charges    NUM*   9
2013       $totaldue,                  # 18 | Total Amt Due                 NUM*   9
2014       $totaldue,                  # 19 | Total Amt Due                 NUM*   9
2015       '',                         # 20 | 30 Day Aging                  NUM*   9
2016       '',                         # 21 | 60 Day Aging                  NUM*   9
2017       '',                         # 22 | 90 Day Aging                  NUM*   9
2018       'N',                        # 23 | Y/N                           CHAR   1
2019       '',                         # 24 | Remittance automation         CHAR 100
2020       $taxtotal,                  # 25 | Total Taxes & Fees            NUM*   9
2021       $self->custnum,             # 26 | Customer Reference Number     CHAR  15
2022       '0',                        # 27 | Federal Tax***                NUM*   9
2023       sprintf("%.2f", $taxtotal), # 28 | State Tax***                  NUM*   9
2024       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
2025     );
2026
2027   } elsif ( $format eq 'oneline' ) { #name
2028   
2029     my ($previous_balance) = $self->previous; 
2030     $previous_balance = sprintf('%.2f', $previous_balance);
2031     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
2032     my @items = map {
2033                       $_->{pkgnum},
2034                       $_->{description},
2035                       $_->{amount}
2036                     }
2037                   $self->_items_pkg, #_items_nontax?  no sections or anything
2038                                      # with this format
2039                   $self->_items_tax;
2040
2041     $csv->combine(
2042       $cust_main->agentnum,
2043       $cust_main->agent->agent,
2044       $self->custnum,
2045       $cust_main->first,
2046       $cust_main->last,
2047       $cust_main->company,
2048       $cust_main->address1,
2049       $cust_main->address2,
2050       $cust_main->city,
2051       $cust_main->state,
2052       $cust_main->zip,
2053
2054       # invoice fields
2055       time2str("%x", $self->_date),
2056       $self->invnum,
2057       $self->charged,
2058       $totaldue,
2059       $previous_balance,
2060       $self->due_date2str("%x"),
2061
2062       @items,
2063     );
2064
2065   } elsif ( $format eq 'bridgestone' ) {
2066
2067     # bypass the CSV stuff and just return this
2068     my $longdate = time2str('%B %d, %Y', $time); #current time, right?
2069     my $zip = $cust_main->zip;
2070     $zip =~ s/\D//;
2071     my $prefix = $self->conf->config('bridgestone-prefix', $cust_main->agentnum)
2072       || '';
2073     return (
2074       sprintf(
2075         "%-5s%-15s%-20s%-30s%-30s%-30s%-30s%-20s%-2s%-9s\n",
2076         $prefix,
2077         $cust_main->display_custnum,
2078         $longdate,
2079         uc(substr($cust_main->contact_firstlast,0,30)),
2080         uc(substr($cust_main->company          ,0,30)),
2081         uc(substr($cust_main->address1         ,0,30)),
2082         uc(substr($cust_main->address2         ,0,30)),
2083         uc(substr($cust_main->city             ,0,20)),
2084         uc($cust_main->state),
2085         $zip
2086       ),
2087       '' #detail
2088       );
2089
2090   } elsif ( $format eq 'ics' ) {
2091
2092     my $bill = $cust_main->bill_location;
2093     my $zip = $bill->zip;
2094     my $zip4 = '';
2095
2096     $zip =~ s/\D//;
2097     if ( $zip =~ /^(\d{5})(\d{4})$/ ) {
2098       $zip = $1;
2099       $zip4 = $2;
2100     }
2101
2102     # minor false laziness with print_generic
2103     my ($previous_balance) = $self->previous;
2104     my $balance_due = $self->owed + $previous_balance;
2105     my $payment_total = sum(0, map { $_->{'amount'} } $self->_items_payments);
2106     my $credit_total  = sum(0, map { $_->{'amount'} } $self->_items_credits);
2107
2108     my $past_due = '';
2109     if ( $self->due_date and $time >= $self->due_date ) {
2110       $past_due = sprintf('Past due:$%0.2f Due Immediately', $balance_due);
2111     }
2112
2113     # again, bypass CSV
2114     my $header = sprintf(
2115       '%-10s%-30s%-48s%-2s%-50s%-30s%-30s%-25s%-2s%-5s%-4s%-8s%-8s%-10s%-10s%-10s%-10s%-10s%-10s%-480s%-35s',
2116       $cust_main->display_custnum, #BID
2117       uc($cust_main->first), #FNAME
2118       uc($cust_main->last), #LNAME
2119       '00', #BATCH, should this ever be anything else?
2120       uc($cust_main->company), #COMP
2121       uc($bill->address1), #STREET1
2122       uc($bill->address2), #STREET2
2123       uc($bill->city), #CITY
2124       uc($bill->state), #STATE
2125       $zip,
2126       $zip4,
2127       time2str('%Y%m%d', $self->_date), #BILL_DATE
2128       $self->due_date2str('%Y%m%d'), #DUE_DATE,
2129       ( map {sprintf('%0.2f', $_)}
2130         $balance_due, #AMNT_DUE
2131         $previous_balance, #PREV_BAL
2132         $payment_total, #PYMT_RCVD
2133         $credit_total, #CREDITS
2134         $previous_balance, #BEG_BAL--is this correct?
2135         $self->charged, #NEW_CHRG
2136       ),
2137       'img01', #MRKT_MSG?
2138       $past_due, #PAST_MSG
2139     );
2140
2141     my @details;
2142     my %svc_class = ('' => ''); # maybe cache this more persistently?
2143
2144     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2145
2146       my $show_pkgnum = $cust_bill_pkg->pkgnum || '';
2147       my $cust_pkg = $cust_bill_pkg->cust_pkg if $show_pkgnum;
2148
2149       if ( $cust_pkg ) {
2150
2151         my @dates = ( $self->_date, undef );
2152         if ( my $prev = $cust_bill_pkg->previous_cust_bill_pkg ) {
2153           $dates[1] = $prev->sdate; #questionable
2154         }
2155
2156         # generate an 01 detail for each service
2157         my @svcs = $cust_pkg->h_cust_svc(@dates, 'I');
2158         foreach my $cust_svc ( @svcs ) {
2159           $show_pkgnum = ''; # hide it if we're showing svcnums
2160
2161           my $svcpart = $cust_svc->svcpart;
2162           if (!exists($svc_class{$svcpart})) {
2163             my $classnum = $cust_svc->part_svc->classnum;
2164             my $part_svc_class = FS::part_svc_class->by_key($classnum)
2165               if $classnum;
2166             $svc_class{$svcpart} = $part_svc_class ? 
2167                                    $part_svc_class->classname :
2168                                    '';
2169           }
2170
2171           my @h_label = $cust_svc->label(@dates, 'I');
2172           push @details, sprintf('01%-9s%-20s%-47s',
2173             $cust_svc->svcnum,
2174             $svc_class{$svcpart},
2175             $h_label[1],
2176           );
2177         } #foreach $cust_svc
2178       } #if $cust_pkg
2179
2180       my $desc = $cust_bill_pkg->desc; # itemdesc or part_pkg.pkg
2181       if ($cust_bill_pkg->recur > 0) {
2182         $desc .= ' '.time2str('%d-%b-%Y', $cust_bill_pkg->sdate).' to '.
2183                      time2str('%d-%b-%Y', $cust_bill_pkg->edate - 86400);
2184       }
2185       push @details, sprintf('02%-6s%-60s%-10s',
2186         $show_pkgnum,
2187         $desc,
2188         sprintf('%0.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
2189       );
2190     } #foreach $cust_bill_pkg
2191
2192     # Tag this row so that we know whether this is one page (1), two pages
2193     # (2), # or "big" (B).  The tag will be stripped off before uploading.
2194     if ( scalar(@details) < 12 ) {
2195       push @details, '1';
2196     } elsif ( scalar(@details) < 58 ) {
2197       push @details, '2';
2198     } else {
2199       push @details, 'B';
2200     }
2201
2202     return join('', $header, @details, "\n");
2203
2204   } else { # default
2205   
2206     $csv->combine(
2207       'cust_bill',
2208       $self->invnum,
2209       $self->custnum,
2210       time2str("%x", $self->_date),
2211       sprintf("%.2f", $self->charged),
2212       ( map { $cust_main->getfield($_) }
2213           qw( first last company address1 address2 city state zip country ) ),
2214       map { '' } (1..5),
2215     ) or die "can't create csv";
2216   }
2217
2218   my $header = $csv->string. "\n";
2219
2220   my $detail = '';
2221   if ( lc($opt{'format'}) eq 'billco' ) {
2222
2223     my $lineseq = 0;
2224     foreach my $item ( $self->_items_pkg ) {
2225
2226       $csv->combine(
2227         '',                     #  1 | N/A-Leave Empty            CHAR   2
2228         '',                     #  2 | N/A-Leave Empty            CHAR  15
2229         $tracctnum,             #  3 | Account Number             CHAR  15
2230         $self->invnum,          #  4 | Invoice Number             CHAR  15
2231         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
2232         $item->{'description'}, #  6 | Transaction Detail         CHAR 100
2233         $item->{'amount'},      #  7 | Amount                     NUM*   9
2234         '',                     #  8 | Line Format Control**      CHAR   2
2235         '',                     #  9 | Grouping Code              CHAR   2
2236         '',                     # 10 | User Defined               CHAR  15
2237       );
2238
2239       $detail .= $csv->string. "\n";
2240
2241     }
2242
2243   } elsif ( lc($opt{'format'}) eq 'oneline' ) {
2244
2245     #do nothing
2246
2247   } else {
2248
2249     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2250
2251       my($pkg, $setup, $recur, $sdate, $edate);
2252       if ( $cust_bill_pkg->pkgnum ) {
2253       
2254         ($pkg, $setup, $recur, $sdate, $edate) = (
2255           $cust_bill_pkg->part_pkg->pkg,
2256           ( $cust_bill_pkg->setup != 0
2257             ? sprintf("%.2f", $cust_bill_pkg->setup )
2258             : '' ),
2259           ( $cust_bill_pkg->recur != 0
2260             ? sprintf("%.2f", $cust_bill_pkg->recur )
2261             : '' ),
2262           ( $cust_bill_pkg->sdate 
2263             ? time2str("%x", $cust_bill_pkg->sdate)
2264             : '' ),
2265           ($cust_bill_pkg->edate 
2266             ? time2str("%x", $cust_bill_pkg->edate)
2267             : '' ),
2268         );
2269   
2270       } else { #pkgnum tax
2271         next unless $cust_bill_pkg->setup != 0;
2272         $pkg = $cust_bill_pkg->desc;
2273         $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2274         ( $sdate, $edate ) = ( '', '' );
2275       }
2276   
2277       $csv->combine(
2278         'cust_bill_pkg',
2279         $self->invnum,
2280         ( map { '' } (1..11) ),
2281         ($pkg, $setup, $recur, $sdate, $edate)
2282       ) or die "can't create csv";
2283
2284       $detail .= $csv->string. "\n";
2285
2286     }
2287
2288   }
2289
2290   ( $header, $detail );
2291
2292 }
2293
2294 =item comp
2295
2296 Pays this invoice with a compliemntary payment.  If there is an error,
2297 returns the error, otherwise returns false.
2298
2299 =cut
2300
2301 sub comp {
2302   my $self = shift;
2303   my $cust_pay = new FS::cust_pay ( {
2304     'invnum'   => $self->invnum,
2305     'paid'     => $self->owed,
2306     '_date'    => '',
2307     'payby'    => 'COMP',
2308     'payinfo'  => $self->cust_main->payinfo,
2309     'paybatch' => '',
2310   } );
2311   $cust_pay->insert;
2312 }
2313
2314 =item realtime_card
2315
2316 Attempts to pay this invoice with a credit card payment via a
2317 Business::OnlinePayment realtime gateway.  See
2318 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2319 for supported processors.
2320
2321 =cut
2322
2323 sub realtime_card {
2324   my $self = shift;
2325   $self->realtime_bop( 'CC', @_ );
2326 }
2327
2328 =item realtime_ach
2329
2330 Attempts to pay this invoice with an electronic check (ACH) payment via a
2331 Business::OnlinePayment realtime gateway.  See
2332 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2333 for supported processors.
2334
2335 =cut
2336
2337 sub realtime_ach {
2338   my $self = shift;
2339   $self->realtime_bop( 'ECHECK', @_ );
2340 }
2341
2342 =item realtime_lec
2343
2344 Attempts to pay this invoice with phone bill (LEC) payment via a
2345 Business::OnlinePayment realtime gateway.  See
2346 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2347 for supported processors.
2348
2349 =cut
2350
2351 sub realtime_lec {
2352   my $self = shift;
2353   $self->realtime_bop( 'LEC', @_ );
2354 }
2355
2356 sub realtime_bop {
2357   my( $self, $method ) = (shift,shift);
2358   my $conf = $self->conf;
2359   my %opt = @_;
2360
2361   my $cust_main = $self->cust_main;
2362   my $balance = $cust_main->balance;
2363   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2364   $amount = sprintf("%.2f", $amount);
2365   return "not run (balance $balance)" unless $amount > 0;
2366
2367   my $description = 'Internet Services';
2368   if ( $conf->exists('business-onlinepayment-description') ) {
2369     my $dtempl = $conf->config('business-onlinepayment-description');
2370
2371     my $agent_obj = $cust_main->agent
2372       or die "can't retreive agent for $cust_main (agentnum ".
2373              $cust_main->agentnum. ")";
2374     my $agent = $agent_obj->agent;
2375     my $pkgs = join(', ',
2376       map { $_->part_pkg->pkg }
2377         grep { $_->pkgnum } $self->cust_bill_pkg
2378     );
2379     $description = eval qq("$dtempl");
2380   }
2381
2382   $cust_main->realtime_bop($method, $amount,
2383     'description' => $description,
2384     'invnum'      => $self->invnum,
2385 #this didn't do what we want, it just calls apply_payments_and_credits
2386 #    'apply'       => 1,
2387     'apply_to_invoice' => 1,
2388     %opt,
2389  #what we want:
2390  #this changes application behavior: auto payments
2391                         #triggered against a specific invoice are now applied
2392                         #to that invoice instead of oldest open.
2393                         #seem okay to me...
2394   );
2395
2396 }
2397
2398 =item batch_card OPTION => VALUE...
2399
2400 Adds a payment for this invoice to the pending credit card batch (see
2401 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2402 runs the payment using a realtime gateway.
2403
2404 =cut
2405
2406 sub batch_card {
2407   my ($self, %options) = @_;
2408   my $cust_main = $self->cust_main;
2409
2410   $options{invnum} = $self->invnum;
2411   
2412   $cust_main->batch_card(%options);
2413 }
2414
2415 sub _agent_template {
2416   my $self = shift;
2417   $self->cust_main->agent_template;
2418 }
2419
2420 sub _agent_invoice_from {
2421   my $self = shift;
2422   $self->cust_main->agent_invoice_from;
2423 }
2424
2425 =item invoice_barcode DIR_OR_FALSE
2426
2427 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2428 it is taken as the temp directory where the PNG file will be generated and the
2429 PNG file name is returned. Otherwise, the PNG image itself is returned.
2430
2431 =cut
2432
2433 sub invoice_barcode {
2434     my ($self, $dir) = (shift,shift);
2435     
2436     my $gdbar = new GD::Barcode('Code39',$self->invnum);
2437         die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2438     my $gd = $gdbar->plot(Height => 30);
2439
2440     if($dir) {
2441         my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2442                            DIR      => $dir,
2443                            SUFFIX   => '.png',
2444                            UNLINK   => 0,
2445                          ) or die "can't open temp file: $!\n";
2446         print $bh $gd->png or die "cannot write barcode to file: $!\n";
2447         my $png_file = $bh->filename;
2448         close $bh;
2449         return $png_file;
2450     }
2451     return $gd->png;
2452 }
2453
2454 =item invnum_date_pretty
2455
2456 Returns a string with the invoice number and date, for example:
2457 "Invoice #54 (3/20/2008)".
2458
2459 Intended for back-end context, with regard to translation and date formatting.
2460
2461 =cut
2462
2463 #note: this uses _date_pretty_unlocalized because _date_pretty is too expensive
2464 # for backend use (and also does the wrong thing, localizing for end customer
2465 # instead of backoffice configured date format)
2466 sub invnum_date_pretty {
2467   my $self = shift;
2468   #$self->mt('Invoice #').
2469   'Invoice #'. #XXX should be translated ala web UI user (not invoice customer)
2470     $self->invnum. ' ('. $self->_date_pretty_unlocalized. ')';
2471 }
2472
2473 #sub _items_extra_usage_sections {
2474 #  my $self = shift;
2475 #  my $escape = shift;
2476 #
2477 #  my %sections = ();
2478 #
2479 #  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
2480 #  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2481 #  {
2482 #    next unless $cust_bill_pkg->pkgnum > 0;
2483 #
2484 #    foreach my $section ( keys %usage_class ) {
2485 #
2486 #      my $usage = $cust_bill_pkg->usage($section);
2487 #
2488 #      next unless $usage && $usage > 0;
2489 #
2490 #      $sections{$section} ||= 0;
2491 #      $sections{$section} += $usage;
2492 #
2493 #    }
2494 #
2495 #  }
2496 #
2497 #  map { { 'description' => &{$escape}($_),
2498 #          'subtotal'    => $sections{$_},
2499 #          'summarized'  => '',
2500 #          'tax_section' => '',
2501 #        }
2502 #      }
2503 #    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
2504 #
2505 #}
2506
2507 sub _items_extra_usage_sections {
2508   my $self = shift;
2509   my $conf = $self->conf;
2510   my $escape = shift;
2511   my $format = shift;
2512
2513   my %sections = ();
2514   my %classnums = ();
2515   my %lines = ();
2516
2517   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2518
2519   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
2520   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2521     next unless $cust_bill_pkg->pkgnum > 0;
2522
2523     foreach my $classnum ( keys %usage_class ) {
2524       my $section = $usage_class{$classnum}->classname;
2525       $classnums{$section} = $classnum;
2526
2527       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
2528         my $amount = $detail->amount;
2529         next unless $amount && $amount > 0;
2530  
2531         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
2532         $sections{$section}{amount} += $amount;  #subtotal
2533         $sections{$section}{calls}++;
2534         $sections{$section}{duration} += $detail->duration;
2535
2536         my $desc = $detail->regionname; 
2537         my $description = $desc;
2538         $description = substr($desc, 0, $maxlength). '...'
2539           if $format eq 'latex' && length($desc) > $maxlength;
2540
2541         $lines{$section}{$desc} ||= {
2542           description     => &{$escape}($description),
2543           #pkgpart         => $part_pkg->pkgpart,
2544           pkgnum          => $cust_bill_pkg->pkgnum,
2545           ref             => '',
2546           amount          => 0,
2547           calls           => 0,
2548           duration        => 0,
2549           #unit_amount     => $cust_bill_pkg->unitrecur,
2550           quantity        => $cust_bill_pkg->quantity,
2551           product_code    => 'N/A',
2552           ext_description => [],
2553         };
2554
2555         $lines{$section}{$desc}{amount} += $amount;
2556         $lines{$section}{$desc}{calls}++;
2557         $lines{$section}{$desc}{duration} += $detail->duration;
2558
2559       }
2560     }
2561   }
2562
2563   my %sectionmap = ();
2564   foreach (keys %sections) {
2565     my $usage_class = $usage_class{$classnums{$_}};
2566     $sectionmap{$_} = { 'description' => &{$escape}($_),
2567                         'amount'    => $sections{$_}{amount},    #subtotal
2568                         'calls'       => $sections{$_}{calls},
2569                         'duration'    => $sections{$_}{duration},
2570                         'summarized'  => '',
2571                         'tax_section' => '',
2572                         'sort_weight' => $usage_class->weight,
2573                         ( $usage_class->format
2574                           ? ( map { $_ => $usage_class->$_($format) }
2575                               qw( description_generator header_generator total_generator total_line_generator )
2576                             )
2577                           : ()
2578                         ), 
2579                       };
2580   }
2581
2582   my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
2583                  values %sectionmap;
2584
2585   my @lines = ();
2586   foreach my $section ( keys %lines ) {
2587     foreach my $line ( keys %{$lines{$section}} ) {
2588       my $l = $lines{$section}{$line};
2589       $l->{section}     = $sectionmap{$section};
2590       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
2591       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
2592       push @lines, $l;
2593     }
2594   }
2595
2596   return(\@sections, \@lines);
2597
2598 }
2599
2600 sub _did_summary {
2601     my $self = shift;
2602     my $end = $self->_date;
2603
2604     # start at date of previous invoice + 1 second or 0 if no previous invoice
2605     my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
2606     $start = 0 if !$start;
2607     $start++;
2608
2609     my $cust_main = $self->cust_main;
2610     my @pkgs = $cust_main->all_pkgs;
2611     my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
2612         = (0,0,0,0,0);
2613     my @seen = ();
2614     foreach my $pkg ( @pkgs ) {
2615         my @h_cust_svc = $pkg->h_cust_svc($end);
2616         foreach my $h_cust_svc ( @h_cust_svc ) {
2617             next if grep {$_ eq $h_cust_svc->svcnum} @seen;
2618             next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
2619
2620             my $inserted = $h_cust_svc->date_inserted;
2621             my $deleted = $h_cust_svc->date_deleted;
2622             my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
2623             my $phone_deleted;
2624             $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
2625             
2626 # DID either activated or ported in; cannot be both for same DID simultaneously
2627             if ($inserted >= $start && $inserted <= $end && $phone_inserted
2628                 && (!$phone_inserted->lnp_status 
2629                     || $phone_inserted->lnp_status eq ''
2630                     || $phone_inserted->lnp_status eq 'native')) {
2631                 $num_activated++;
2632             }
2633             else { # this one not so clean, should probably move to (h_)svc_phone
2634                  my $phone_portedin = qsearchs( 'h_svc_phone',
2635                       { 'svcnum' => $h_cust_svc->svcnum, 
2636                         'lnp_status' => 'portedin' },  
2637                       FS::h_svc_phone->sql_h_searchs($end),  
2638                     );
2639                  $num_portedin++ if $phone_portedin;
2640             }
2641
2642 # DID either deactivated or ported out; cannot be both for same DID simultaneously
2643             if($deleted >= $start && $deleted <= $end && $phone_deleted
2644                 && (!$phone_deleted->lnp_status 
2645                     || $phone_deleted->lnp_status ne 'portingout')) {
2646                 $num_deactivated++;
2647             } 
2648             elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
2649                 && $phone_deleted->lnp_status 
2650                 && $phone_deleted->lnp_status eq 'portingout') {
2651                 $num_portedout++;
2652             }
2653
2654             # increment usage minutes
2655         if ( $phone_inserted ) {
2656             my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
2657             $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
2658         }
2659         else {
2660             warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
2661         }
2662
2663             # don't look at this service again
2664             push @seen, $h_cust_svc->svcnum;
2665         }
2666     }
2667
2668     $minutes = sprintf("%d", $minutes);
2669     ("Activated: $num_activated  Ported-In: $num_portedin  Deactivated: "
2670         . "$num_deactivated  Ported-Out: $num_portedout ",
2671             "Total Minutes: $minutes");
2672 }
2673
2674 sub _items_accountcode_cdr {
2675     my $self = shift;
2676     my $escape = shift;
2677     my $format = shift;
2678
2679     my $section = { 'amount'        => 0,
2680                     'calls'         => 0,
2681                     'duration'      => 0,
2682                     'sort_weight'   => '',
2683                     'phonenum'      => '',
2684                     'description'   => 'Usage by Account Code',
2685                     'post_total'    => '',
2686                     'summarized'    => '',
2687                     'header'        => '',
2688                   };
2689     my @lines;
2690     my %accountcodes = ();
2691
2692     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2693         next unless $cust_bill_pkg->pkgnum > 0;
2694
2695         my @header = $cust_bill_pkg->details_header;
2696         next unless scalar(@header);
2697         $section->{'header'} = join(',',@header);
2698
2699         foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
2700
2701             $section->{'header'} = $detail->formatted('format' => $format)
2702                 if($detail->detail eq $section->{'header'}); 
2703       
2704             my $accountcode = $detail->accountcode;
2705             next unless $accountcode;
2706
2707             my $amount = $detail->amount;
2708             next unless $amount && $amount > 0;
2709
2710             $accountcodes{$accountcode} ||= {
2711                     description => $accountcode,
2712                     pkgnum      => '',
2713                     ref         => '',
2714                     amount      => 0,
2715                     calls       => 0,
2716                     duration    => 0,
2717                     quantity    => '',
2718                     product_code => 'N/A',
2719                     section     => $section,
2720                     ext_description => [ $section->{'header'} ],
2721                     detail_temp => [],
2722             };
2723
2724             $section->{'amount'} += $amount;
2725             $accountcodes{$accountcode}{'amount'} += $amount;
2726             $accountcodes{$accountcode}{calls}++;
2727             $accountcodes{$accountcode}{duration} += $detail->duration;
2728             push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
2729         }
2730     }
2731
2732     foreach my $l ( values %accountcodes ) {
2733         $l->{amount} = sprintf( "%.2f", $l->{amount} );
2734         my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
2735         foreach my $sorted_detail ( @sorted_detail ) {
2736             push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
2737         }
2738         delete $l->{detail_temp};
2739         push @lines, $l;
2740     }
2741
2742     my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
2743
2744     return ($section,\@sorted_lines);
2745 }
2746
2747 sub _items_svc_phone_sections {
2748   my $self = shift;
2749   my $conf = $self->conf;
2750   my $escape = shift;
2751   my $format = shift;
2752
2753   my %sections = ();
2754   my %classnums = ();
2755   my %lines = ();
2756
2757   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2758
2759   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
2760   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
2761
2762   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2763     next unless $cust_bill_pkg->pkgnum > 0;
2764
2765     my @header = $cust_bill_pkg->details_header;
2766     next unless scalar(@header);
2767
2768     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
2769
2770       my $phonenum = $detail->phonenum;
2771       next unless $phonenum;
2772
2773       my $amount = $detail->amount;
2774       next unless $amount && $amount > 0;
2775
2776       $sections{$phonenum} ||= { 'amount'      => 0,
2777                                  'calls'       => 0,
2778                                  'duration'    => 0,
2779                                  'sort_weight' => -1,
2780                                  'phonenum'    => $phonenum,
2781                                 };
2782       $sections{$phonenum}{amount} += $amount;  #subtotal
2783       $sections{$phonenum}{calls}++;
2784       $sections{$phonenum}{duration} += $detail->duration;
2785
2786       my $desc = $detail->regionname; 
2787       my $description = $desc;
2788       $description = substr($desc, 0, $maxlength). '...'
2789         if $format eq 'latex' && length($desc) > $maxlength;
2790
2791       $lines{$phonenum}{$desc} ||= {
2792         description     => &{$escape}($description),
2793         #pkgpart         => $part_pkg->pkgpart,
2794         pkgnum          => '',
2795         ref             => '',
2796         amount          => 0,
2797         calls           => 0,
2798         duration        => 0,
2799         #unit_amount     => '',
2800         quantity        => '',
2801         product_code    => 'N/A',
2802         ext_description => [],
2803       };
2804
2805       $lines{$phonenum}{$desc}{amount} += $amount;
2806       $lines{$phonenum}{$desc}{calls}++;
2807       $lines{$phonenum}{$desc}{duration} += $detail->duration;
2808
2809       my $line = $usage_class{$detail->classnum}->classname;
2810       $sections{"$phonenum $line"} ||=
2811         { 'amount' => 0,
2812           'calls' => 0,
2813           'duration' => 0,
2814           'sort_weight' => $usage_class{$detail->classnum}->weight,
2815           'phonenum' => $phonenum,
2816           'header'  => [ @header ],
2817         };
2818       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
2819       $sections{"$phonenum $line"}{calls}++;
2820       $sections{"$phonenum $line"}{duration} += $detail->duration;
2821
2822       $lines{"$phonenum $line"}{$desc} ||= {
2823         description     => &{$escape}($description),
2824         #pkgpart         => $part_pkg->pkgpart,
2825         pkgnum          => '',
2826         ref             => '',
2827         amount          => 0,
2828         calls           => 0,
2829         duration        => 0,
2830         #unit_amount     => '',
2831         quantity        => '',
2832         product_code    => 'N/A',
2833         ext_description => [],
2834       };
2835
2836       $lines{"$phonenum $line"}{$desc}{amount} += $amount;
2837       $lines{"$phonenum $line"}{$desc}{calls}++;
2838       $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
2839       push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
2840            $detail->formatted('format' => $format);
2841
2842     }
2843   }
2844
2845   my %sectionmap = ();
2846   my $simple = new FS::usage_class { format => 'simple' }; #bleh
2847   foreach ( keys %sections ) {
2848     my @header = @{ $sections{$_}{header} || [] };
2849     my $usage_simple =
2850       new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
2851     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
2852     my $usage_class = $summary ? $simple : $usage_simple;
2853     my $ending = $summary ? ' usage charges' : '';
2854     my %gen_opt = ();
2855     unless ($summary) {
2856       $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
2857     }
2858     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
2859                         'amount'    => $sections{$_}{amount},    #subtotal
2860                         'calls'       => $sections{$_}{calls},
2861                         'duration'    => $sections{$_}{duration},
2862                         'summarized'  => '',
2863                         'tax_section' => '',
2864                         'phonenum'    => $sections{$_}{phonenum},
2865                         'sort_weight' => $sections{$_}{sort_weight},
2866                         'post_total'  => $summary, #inspire pagebreak
2867                         (
2868                           ( map { $_ => $usage_class->$_($format, %gen_opt) }
2869                             qw( description_generator
2870                                 header_generator
2871                                 total_generator
2872                                 total_line_generator
2873                               )
2874                           )
2875                         ), 
2876                       };
2877   }
2878
2879   my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
2880                         $a->{sort_weight} <=> $b->{sort_weight}
2881                       }
2882                  values %sectionmap;
2883
2884   my @lines = ();
2885   foreach my $section ( keys %lines ) {
2886     foreach my $line ( keys %{$lines{$section}} ) {
2887       my $l = $lines{$section}{$line};
2888       $l->{section}     = $sectionmap{$section};
2889       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
2890       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
2891       push @lines, $l;
2892     }
2893   }
2894   
2895   if($conf->exists('phone_usage_class_summary')) { 
2896       # this only works with Latex
2897       my @newlines;
2898       my @newsections;
2899
2900       # after this, we'll have only two sections per DID:
2901       # Calls Summary and Calls Detail
2902       foreach my $section ( @sections ) {
2903         if($section->{'post_total'}) {
2904             $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
2905             $section->{'total_line_generator'} = sub { '' };
2906             $section->{'total_generator'} = sub { '' };
2907             $section->{'header_generator'} = sub { '' };
2908             $section->{'description_generator'} = '';
2909             push @newsections, $section;
2910             my %calls_detail = %$section;
2911             $calls_detail{'post_total'} = '';
2912             $calls_detail{'sort_weight'} = '';
2913             $calls_detail{'description_generator'} = sub { '' };
2914             $calls_detail{'header_generator'} = sub {
2915                 return ' & Date/Time & Called Number & Duration & Price'
2916                     if $format eq 'latex';
2917                 '';
2918             };
2919             $calls_detail{'description'} = 'Calls Detail: '
2920                                                     . $section->{'phonenum'};
2921             push @newsections, \%calls_detail;  
2922         }
2923       }
2924
2925       # after this, each usage class is collapsed/summarized into a single
2926       # line under the Calls Summary section
2927       foreach my $newsection ( @newsections ) {
2928         if($newsection->{'post_total'}) { # this means Calls Summary
2929             foreach my $section ( @sections ) {
2930                 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'} 
2931                                 && !$section->{'post_total'});
2932                 my $newdesc = $section->{'description'};
2933                 my $tn = $section->{'phonenum'};
2934                 $newdesc =~ s/$tn//g;
2935                 my $line = {  ext_description => [],
2936                               pkgnum => '',
2937                               ref => '',
2938                               quantity => '',
2939                               calls => $section->{'calls'},
2940                               section => $newsection,
2941                               duration => $section->{'duration'},
2942                               description => $newdesc,
2943                               amount => sprintf("%.2f",$section->{'amount'}),
2944                               product_code => 'N/A',
2945                             };
2946                 push @newlines, $line;
2947             }
2948         }
2949       }
2950
2951       # after this, Calls Details is populated with all CDRs
2952       foreach my $newsection ( @newsections ) {
2953         if(!$newsection->{'post_total'}) { # this means Calls Details
2954             foreach my $line ( @lines ) {
2955                 next unless (scalar(@{$line->{'ext_description'}}) &&
2956                         $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
2957                             );
2958                 my @extdesc = @{$line->{'ext_description'}};
2959                 my @newextdesc;
2960                 foreach my $extdesc ( @extdesc ) {
2961                     $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
2962                     push @newextdesc, $extdesc;
2963                 }
2964                 $line->{'ext_description'} = \@newextdesc;
2965                 $line->{'section'} = $newsection;
2966                 push @newlines, $line;
2967             }
2968         }
2969       }
2970
2971       return(\@newsections, \@newlines);
2972   }
2973
2974   return(\@sections, \@lines);
2975
2976 }
2977
2978 =sub _items_usage_class_summary OPTIONS
2979
2980 Returns a list of detail items summarizing the usage charges on this 
2981 invoice.  Each one will have 'amount', 'description' (the usage charge name),
2982 and 'usage_classnum'.
2983
2984 OPTIONS can include 'escape' (a function to escape the descriptions).
2985
2986 =cut
2987
2988 sub _items_usage_class_summary {
2989   my $self = shift;
2990   my %opt = @_;
2991
2992   my $escape = $opt{escape} || sub { $_[0] };
2993   my $invnum = $self->invnum;
2994   my @classes = qsearch({
2995       'table'     => 'usage_class',
2996       'select'    => 'classnum, classname, SUM(amount) AS amount',
2997       'addl_from' => ' LEFT JOIN cust_bill_pkg_detail USING (classnum)' .
2998                      ' LEFT JOIN cust_bill_pkg USING (billpkgnum)',
2999       'extra_sql' => " WHERE cust_bill_pkg.invnum = $invnum".
3000                      ' GROUP BY classnum, classname, weight'.
3001                      ' HAVING (usage_class.disabled IS NULL OR SUM(amount) > 0)'.
3002                      ' ORDER BY weight ASC',
3003   });
3004   my @l;
3005   my $section = {
3006     description   => &{$escape}($self->mt('Usage Summary')),
3007     no_subtotal   => 1,
3008     usage_section => 1,
3009   };
3010   foreach my $class (@classes) {
3011     push @l, {
3012       'description'     => &{$escape}($class->classname),
3013       'amount'          => sprintf('%.2f', $class->amount),
3014       'usage_classnum'  => $class->classnum,
3015       'section'         => $section,
3016     };
3017   }
3018   return @l;
3019 }
3020
3021 sub _items_previous {
3022   my $self = shift;
3023   my $conf = $self->conf;
3024   my $cust_main = $self->cust_main;
3025   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
3026   my @b = ();
3027   foreach ( @pr_cust_bill ) {
3028     my $date = $conf->exists('invoice_show_prior_due_date')
3029                ? 'due '. $_->due_date2str('short')
3030                : $self->time2str_local('short', $_->_date);
3031     push @b, {
3032       'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
3033       #'pkgpart'     => 'N/A',
3034       'pkgnum'      => 'N/A',
3035       'amount'      => sprintf("%.2f", $_->owed),
3036     };
3037   }
3038   @b;
3039
3040   #{
3041   #    'description'     => 'Previous Balance',
3042   #    #'pkgpart'         => 'N/A',
3043   #    'pkgnum'          => 'N/A',
3044   #    'amount'          => sprintf("%10.2f", $pr_total ),
3045   #    'ext_description' => [ map {
3046   #                                 "Invoice ". $_->invnum.
3047   #                                 " (". time2str("%x",$_->_date). ") ".
3048   #                                 sprintf("%10.2f", $_->owed)
3049   #                         } @pr_cust_bill ],
3050
3051   #};
3052 }
3053
3054 sub _items_credits {
3055   my( $self, %opt ) = @_;
3056   my $trim_len = $opt{'trim_len'} || 60;
3057
3058   my @b;
3059   #credits
3060   my @objects;
3061   if ( $self->conf->exists('previous_balance-payments_since') ) {
3062     if ( $opt{'template'} eq 'statement' ) {
3063       # then the current bill is a "statement" (i.e. an invoice sent as
3064       # a payment receipt)
3065       # and in that case we want to see payments on or after THIS invoice
3066       @objects = qsearch('cust_credit', {
3067           'custnum' => $self->custnum,
3068           '_date'   => {op => '>=', value => $self->_date},
3069       });
3070     } else {
3071       my $date = 0;
3072       $date = $self->previous_bill->_date if $self->previous_bill;
3073       @objects = qsearch('cust_credit', {
3074           'custnum' => $self->custnum,
3075           '_date'   => {op => '>=', value => $date},
3076       });
3077     }
3078   } else {
3079     @objects = $self->cust_credited;
3080   }
3081
3082   foreach my $obj ( @objects ) {
3083     my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
3084
3085     my $reason = substr($cust_credit->reason, 0, $trim_len);
3086     $reason .= '...' if length($reason) < length($cust_credit->reason);
3087     $reason = " ($reason) " if $reason;
3088
3089     push @b, {
3090       #'description' => 'Credit ref\#'. $_->crednum.
3091       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
3092       #                 $reason,
3093       'description' => $self->mt('Credit applied').' '.
3094                        $self->time2str_local('short', $obj->_date). $reason,
3095       'amount'      => sprintf("%.2f",$obj->amount),
3096     };
3097   }
3098
3099   @b;
3100
3101 }
3102
3103 sub _items_payments {
3104   my $self = shift;
3105   my %opt = @_;
3106
3107   my @b;
3108   my $detailed = $self->conf->exists('invoice_payment_details');
3109   my @objects;
3110   if ( $self->conf->exists('previous_balance-payments_since') ) {
3111     # then show payments dated on/after the previous bill...
3112     if ( $opt{'template'} eq 'statement' ) {
3113       # then the current bill is a "statement" (i.e. an invoice sent as
3114       # a payment receipt)
3115       # and in that case we want to see payments on or after THIS invoice
3116       @objects = qsearch('cust_pay', {
3117           'custnum' => $self->custnum,
3118           '_date'   => {op => '>=', value => $self->_date},
3119       });
3120     } else {
3121       # the normal case: payments on or after the previous invoice
3122       my $date = 0;
3123       $date = $self->previous_bill->_date if $self->previous_bill;
3124       @objects = qsearch('cust_pay', {
3125         'custnum' => $self->custnum,
3126         '_date'   => {op => '>=', value => $date},
3127       });
3128       # and before the current bill...
3129       @objects = grep { $_->_date < $self->_date } @objects;
3130     }
3131   } else {
3132     @objects = $self->cust_bill_pay;
3133   }
3134
3135   foreach my $obj (@objects) {
3136     my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
3137     my $desc = $self->mt('Payment received').' '.
3138                $self->time2str_local('short', $cust_pay->_date );
3139     $desc .= $self->mt(' via ') .
3140              $cust_pay->payby_payinfo_pretty( $self->cust_main->locale )
3141       if $detailed;
3142
3143     push @b, {
3144       'description' => $desc,
3145       'amount'      => sprintf("%.2f", $obj->amount )
3146     };
3147   }
3148
3149   @b;
3150
3151 }
3152
3153 =item call_details [ OPTION => VALUE ... ]
3154
3155 Returns an array of CSV strings representing the call details for this invoice
3156 The only option available is the boolean prepend_billed_number
3157
3158 =cut
3159
3160 sub call_details {
3161   my ($self, %opt) = @_;
3162
3163   my $format_function = sub { shift };
3164
3165   if ($opt{prepend_billed_number}) {
3166     $format_function = sub {
3167       my $detail = shift;
3168       my $row = shift;
3169
3170       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
3171       
3172     };
3173   }
3174
3175   my @details = map { $_->details( 'format_function' => $format_function,
3176                                    'escape_function' => sub{ return() },
3177                                  )
3178                     }
3179                   grep { $_->pkgnum }
3180                   $self->cust_bill_pkg;
3181   my $header = $details[0];
3182   ( $header, grep { $_ ne $header } @details );
3183 }
3184
3185
3186 =back
3187
3188 =head1 SUBROUTINES
3189
3190 =over 4
3191
3192 =item process_reprint
3193
3194 =cut
3195
3196 sub process_reprint {
3197   process_re_X('print', @_);
3198 }
3199
3200 =item process_reemail
3201
3202 =cut
3203
3204 sub process_reemail {
3205   process_re_X('email', @_);
3206 }
3207
3208 =item process_refax
3209
3210 =cut
3211
3212 sub process_refax {
3213   process_re_X('fax', @_);
3214 }
3215
3216 =item process_reftp
3217
3218 =cut
3219
3220 sub process_reftp {
3221   process_re_X('ftp', @_);
3222 }
3223
3224 =item respool
3225
3226 =cut
3227
3228 sub process_respool {
3229   process_re_X('spool', @_);
3230 }
3231
3232 use Storable qw(thaw);
3233 use Data::Dumper;
3234 use MIME::Base64;
3235 sub process_re_X {
3236   my( $method, $job ) = ( shift, shift );
3237   warn "$me process_re_X $method for job $job\n" if $DEBUG;
3238
3239   my $param = thaw(decode_base64(shift));
3240   warn Dumper($param) if $DEBUG;
3241
3242   re_X(
3243     $method,
3244     $job,
3245     %$param,
3246   );
3247
3248 }
3249
3250 sub re_X {
3251   # spool_invoice ftp_invoice fax_invoice print_invoice
3252   my($method, $job, %param ) = @_;
3253   if ( $DEBUG ) {
3254     warn "re_X $method for job $job with param:\n".
3255          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
3256   }
3257
3258   #some false laziness w/search/cust_bill.html
3259   my $distinct = '';
3260   my $orderby = 'ORDER BY cust_bill._date';
3261
3262   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
3263
3264   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
3265      
3266   my @cust_bill = qsearch( {
3267     #'select'    => "cust_bill.*",
3268     'table'     => 'cust_bill',
3269     'addl_from' => $addl_from,
3270     'hashref'   => {},
3271     'extra_sql' => $extra_sql,
3272     'order_by'  => $orderby,
3273     'debug' => 1,
3274   } );
3275
3276   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3277
3278   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3279     if $DEBUG;
3280
3281   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3282   foreach my $cust_bill ( @cust_bill ) {
3283     $cust_bill->$method();
3284
3285     if ( $job ) { #progressbar foo
3286       $num++;
3287       if ( time - $min_sec > $last ) {
3288         my $error = $job->update_statustext(
3289           int( 100 * $num / scalar(@cust_bill) )
3290         );
3291         die $error if $error;
3292         $last = time;
3293       }
3294     }
3295
3296   }
3297
3298 }
3299
3300 =back
3301
3302 =head1 CLASS METHODS
3303
3304 =over 4
3305
3306 =item owed_sql
3307
3308 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3309
3310 =cut
3311
3312 sub owed_sql {
3313   my ($class, $start, $end) = @_;
3314   'charged - '. 
3315     $class->paid_sql($start, $end). ' - '. 
3316     $class->credited_sql($start, $end);
3317 }
3318
3319 =item net_sql
3320
3321 Returns an SQL fragment to retreive the net amount (charged minus credited).
3322
3323 =cut
3324
3325 sub net_sql {
3326   my ($class, $start, $end) = @_;
3327   'charged - '. $class->credited_sql($start, $end);
3328 }
3329
3330 =item paid_sql
3331
3332 Returns an SQL fragment to retreive the amount paid against this invoice.
3333
3334 =cut
3335
3336 sub paid_sql {
3337   my ($class, $start, $end) = @_;
3338   $start &&= "AND cust_bill_pay._date <= $start";
3339   $end   &&= "AND cust_bill_pay._date > $end";
3340   $start = '' unless defined($start);
3341   $end   = '' unless defined($end);
3342   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3343        WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
3344 }
3345
3346 =item credited_sql
3347
3348 Returns an SQL fragment to retreive the amount credited against this invoice.
3349
3350 =cut
3351
3352 sub credited_sql {
3353   my ($class, $start, $end) = @_;
3354   $start &&= "AND cust_credit_bill._date <= $start";
3355   $end   &&= "AND cust_credit_bill._date >  $end";
3356   $start = '' unless defined($start);
3357   $end   = '' unless defined($end);
3358   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3359        WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
3360 }
3361
3362 =item due_date_sql
3363
3364 Returns an SQL fragment to retrieve the due date of an invoice.
3365 Currently only supported on PostgreSQL.
3366
3367 =cut
3368
3369 sub due_date_sql {
3370   my $conf = new FS::Conf;
3371 'COALESCE(
3372   SUBSTRING(
3373     COALESCE(
3374       cust_bill.invoice_terms,
3375       cust_main.invoice_terms,
3376       \''.($conf->config('invoice_default_terms') || '').'\'
3377     ), E\'Net (\\\\d+)\'
3378   )::INTEGER, 0
3379 ) * 86400 + cust_bill._date'
3380 }
3381
3382 =item search_sql_where HASHREF
3383
3384 Class method which returns an SQL WHERE fragment to search for parameters
3385 specified in HASHREF.  Valid parameters are
3386
3387 =over 4
3388
3389 =item _date
3390
3391 List reference of start date, end date, as UNIX timestamps.
3392
3393 =item invnum_min
3394
3395 =item invnum_max
3396
3397 =item agentnum
3398
3399 =item charged
3400
3401 List reference of charged limits (exclusive).
3402
3403 =item owed
3404
3405 List reference of charged limits (exclusive).
3406
3407 =item open
3408
3409 flag, return open invoices only
3410
3411 =item net
3412
3413 flag, return net invoices only
3414
3415 =item days
3416
3417 =item newest_percust
3418
3419 =item custnum
3420
3421 Return only invoices belonging to that customer.
3422
3423 =item cust_classnum
3424
3425 Limit to that customer class (single value or arrayref).
3426
3427 =item payby
3428
3429 Limit to customers with that payment method (single value or arrayref).
3430
3431 =item refnum
3432
3433 Limit to customers with that advertising source.
3434
3435 =back
3436
3437 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3438
3439 =cut
3440
3441 sub search_sql_where {
3442   my($class, $param) = @_;
3443   if ( $DEBUG ) {
3444     warn "$me search_sql_where called with params: \n".
3445          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
3446   }
3447
3448   my @search = ();
3449
3450   #agentnum
3451   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3452     push @search, "cust_main.agentnum = $1";
3453   }
3454
3455   #refnum
3456   if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
3457     push @search, "cust_main.refnum = $1";
3458   }
3459
3460   #custnum
3461   if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
3462     push @search, "cust_bill.custnum = $1";
3463   }
3464
3465   #customer classnum (false laziness w/ cust_main/Search.pm)
3466   if ( $param->{'cust_classnum'} ) {
3467
3468     my @classnum = ref( $param->{'cust_classnum'} )
3469                      ? @{ $param->{'cust_classnum'} }
3470                      :  ( $param->{'cust_classnum'} );
3471
3472     @classnum = grep /^(\d*)$/, @classnum;
3473
3474     if ( @classnum ) {
3475       push @search, '( '. join(' OR ', map {
3476                                              $_ ? "cust_main.classnum = $_"
3477                                                 : "cust_main.classnum IS NULL"
3478                                            }
3479                                            @classnum
3480                               ).
3481                     ' )';
3482     }
3483
3484   }
3485
3486   #payby
3487   if ( $param->{payby} ) {
3488     my $payby = $param->{payby};
3489     $payby = [ $payby ] unless ref $payby;
3490     my $payby_in = join(',', map {dbh->quote($_)} @$payby);
3491     push @search, "cust_main.payby IN($payby_in)" if length($payby_in);
3492   }
3493
3494   #_date
3495   if ( $param->{_date} ) {
3496     my($beginning, $ending) = @{$param->{_date}};
3497
3498     push @search, "cust_bill._date >= $beginning",
3499                   "cust_bill._date <  $ending";
3500   }
3501
3502   #invnum
3503   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3504     push @search, "cust_bill.invnum >= $1";
3505   }
3506   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3507     push @search, "cust_bill.invnum <= $1";
3508   }
3509
3510   #charged
3511   if ( $param->{charged} ) {
3512     my @charged = ref($param->{charged})
3513                     ? @{ $param->{charged} }
3514                     : ($param->{charged});
3515
3516     push @search, map { s/^charged/cust_bill.charged/; $_; }
3517                       @charged;
3518   }
3519
3520   my $owed_sql = FS::cust_bill->owed_sql;
3521
3522   #owed
3523   if ( $param->{owed} ) {
3524     my @owed = ref($param->{owed})
3525                  ? @{ $param->{owed} }
3526                  : ($param->{owed});
3527     push @search, map { s/^owed/$owed_sql/; $_; }
3528                       @owed;
3529   }
3530
3531   #open/net flags
3532   push @search, "0 != $owed_sql"
3533     if $param->{'open'};
3534   push @search, '0 != '. FS::cust_bill->net_sql
3535     if $param->{'net'};
3536
3537   #days
3538   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3539     if $param->{'days'};
3540
3541   #newest_percust
3542   if ( $param->{'newest_percust'} ) {
3543
3544     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3545     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3546
3547     my @newest_where = map { my $x = $_;
3548                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
3549                              $x;
3550                            }
3551                            grep ! /^cust_main./, @search;
3552     my $newest_where = scalar(@newest_where)
3553                          ? ' AND '. join(' AND ', @newest_where)
3554                          : '';
3555
3556
3557     push @search, "cust_bill._date = (
3558       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3559         WHERE newest_cust_bill.custnum = cust_bill.custnum
3560           $newest_where
3561     )";
3562
3563   }
3564
3565   #promised_date - also has an option to accept nulls
3566   if ( $param->{promised_date} ) {
3567     my($beginning, $ending, $null) = @{$param->{promised_date}};
3568
3569     push @search, "(( cust_bill.promised_date >= $beginning AND ".
3570                     "cust_bill.promised_date <  $ending )" .
3571                     ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
3572   }
3573
3574   #agent virtualization
3575   my $curuser = $FS::CurrentUser::CurrentUser;
3576   if ( $curuser->username eq 'fs_queue'
3577        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3578     my $username = $1;
3579     my $newuser = qsearchs('access_user', {
3580       'username' => $username,
3581       'disabled' => '',
3582     } );
3583     if ( $newuser ) {
3584       $curuser = $newuser;
3585     } else {
3586       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3587     }
3588   }
3589   push @search, $curuser->agentnums_sql;
3590
3591   join(' AND ', @search );
3592
3593 }
3594
3595 =back
3596
3597 =head1 BUGS
3598
3599 The delete method.
3600
3601 =head1 SEE ALSO
3602
3603 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3604 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
3605 documentation.
3606
3607 =cut
3608
3609 1;
3610