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