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