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