NG auth: autocreate records for external users, RT#21563
[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                     }
2137                   $self->_items_pkg, #_items_nontax?  no sections or anything
2138                                      # with this format
2139                   $self->_items_tax;
2140
2141     $csv->combine(
2142       $cust_main->agentnum,
2143       $cust_main->agent->agent,
2144       $self->custnum,
2145       $cust_main->first,
2146       $cust_main->last,
2147       $cust_main->address1,
2148       $cust_main->address2,
2149       $cust_main->city,
2150       $cust_main->state,
2151       $cust_main->zip,
2152
2153       # invoice fields
2154       time2str("%x", $self->_date),
2155       $self->invnum,
2156       $self->charged,
2157       $totaldue,
2158       $previous_balance,
2159       $self->due_date2str("%x"),
2160
2161       @items,
2162     );
2163
2164   } elsif ( $format eq 'bridgestone' ) {
2165
2166     # bypass the CSV stuff and just return this
2167     my $longdate = time2str('%B %d, %Y', $time); #current time, right?
2168     my $zip = $cust_main->zip;
2169     $zip =~ s/\D//;
2170     my $prefix = $self->conf->config('bridgestone-prefix', $cust_main->agentnum)
2171       || '';
2172     return (
2173       sprintf(
2174         "%-5s%-15s%-20s%-30s%-30s%-30s%-30s%-20s%-2s%-9s\n",
2175         $prefix,
2176         $cust_main->display_custnum,
2177         $longdate,
2178         uc(substr($cust_main->contact_firstlast,0,30)),
2179         uc(substr($cust_main->company          ,0,30)),
2180         uc(substr($cust_main->address1         ,0,30)),
2181         uc(substr($cust_main->address2         ,0,30)),
2182         uc(substr($cust_main->city             ,0,20)),
2183         uc($cust_main->state),
2184         $zip
2185       ),
2186       '' #detail
2187       );
2188
2189   } elsif ( $format eq 'ics' ) {
2190
2191     my $bill = $cust_main->bill_location;
2192     my $zip = $bill->zip;
2193     my $zip4 = '';
2194
2195     $zip =~ s/\D//;
2196     if ( $zip =~ /^(\d{5})(\d{4})$/ ) {
2197       $zip = $1;
2198       $zip4 = $2;
2199     }
2200
2201     # minor false laziness with print_generic
2202     my ($previous_balance) = $self->previous;
2203     my $balance_due = $self->owed + $previous_balance;
2204     my $payment_total = sum(0, map { $_->{'amount'} } $self->_items_payments);
2205     my $credit_total  = sum(0, map { $_->{'amount'} } $self->_items_credits);
2206
2207     my $past_due = '';
2208     if ( $self->due_date and $time >= $self->due_date ) {
2209       $past_due = sprintf('Past due:$%0.2f Due Immediately', $balance_due);
2210     }
2211
2212     # again, bypass CSV
2213     my $header = sprintf(
2214       '%-10s%-30s%-48s%-2s%-50s%-30s%-30s%-25s%-2s%-5s%-4s%-8s%-8s%-10s%-10s%-10s%-10s%-10s%-10s%-480s%-35s',
2215       $cust_main->display_custnum, #BID
2216       uc($cust_main->first), #FNAME
2217       uc($cust_main->last), #LNAME
2218       '00', #BATCH, should this ever be anything else?
2219       uc($cust_main->company), #COMP
2220       uc($bill->address1), #STREET1
2221       uc($bill->address2), #STREET2
2222       uc($bill->city), #CITY
2223       uc($bill->state), #STATE
2224       $zip,
2225       $zip4,
2226       time2str('%Y%m%d', $self->_date), #BILL_DATE
2227       $self->due_date2str('%Y%m%d'), #DUE_DATE,
2228       ( map {sprintf('%0.2f', $_)}
2229         $balance_due, #AMNT_DUE
2230         $previous_balance, #PREV_BAL
2231         $payment_total, #PYMT_RCVD
2232         $credit_total, #CREDITS
2233         $previous_balance, #BEG_BAL--is this correct?
2234         $self->charged, #NEW_CHRG
2235       ),
2236       'img01', #MRKT_MSG?
2237       $past_due, #PAST_MSG
2238     );
2239
2240     my @details;
2241     my %svc_class = ('' => ''); # maybe cache this more persistently?
2242
2243     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2244
2245       my $show_pkgnum = $cust_bill_pkg->pkgnum || '';
2246       my $cust_pkg = $cust_bill_pkg->cust_pkg if $show_pkgnum;
2247
2248       if ( $cust_pkg ) {
2249
2250         my @dates = ( $self->_date, undef );
2251         if ( my $prev = $cust_bill_pkg->previous_cust_bill_pkg ) {
2252           $dates[1] = $prev->sdate; #questionable
2253         }
2254
2255         # generate an 01 detail for each service
2256         my @svcs = $cust_pkg->h_cust_svc(@dates, 'I');
2257         foreach my $cust_svc ( @svcs ) {
2258           $show_pkgnum = ''; # hide it if we're showing svcnums
2259
2260           my $svcpart = $cust_svc->svcpart;
2261           if (!exists($svc_class{$svcpart})) {
2262             my $classnum = $cust_svc->part_svc->classnum;
2263             my $part_svc_class = FS::part_svc_class->by_key($classnum)
2264               if $classnum;
2265             $svc_class{$svcpart} = $part_svc_class ? 
2266                                    $part_svc_class->classname :
2267                                    '';
2268           }
2269
2270           my @h_label = $cust_svc->label(@dates, 'I');
2271           push @details, sprintf('01%-9s%-20s%-47s',
2272             $cust_svc->svcnum,
2273             $svc_class{$svcpart},
2274             $h_label[1],
2275           );
2276         } #foreach $cust_svc
2277       } #if $cust_pkg
2278
2279       my $desc = $cust_bill_pkg->desc; # itemdesc or part_pkg.pkg
2280       if ($cust_bill_pkg->recur > 0) {
2281         $desc .= ' '.time2str('%d-%b-%Y', $cust_bill_pkg->sdate).' to '.
2282                      time2str('%d-%b-%Y', $cust_bill_pkg->edate - 86400);
2283       }
2284       push @details, sprintf('02%-6s%-60s%-10s',
2285         $show_pkgnum,
2286         $desc,
2287         sprintf('%0.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
2288       );
2289     } #foreach $cust_bill_pkg
2290
2291     # Tag this row so that we know whether this is one page (1), two pages
2292     # (2), # or "big" (B).  The tag will be stripped off before uploading.
2293     if ( scalar(@details) < 12 ) {
2294       push @details, '1';
2295     } elsif ( scalar(@details) < 58 ) {
2296       push @details, '2';
2297     } else {
2298       push @details, 'B';
2299     }
2300
2301     return join('', $header, @details, "\n");
2302
2303   } else { # default
2304   
2305     $csv->combine(
2306       'cust_bill',
2307       $self->invnum,
2308       $self->custnum,
2309       time2str("%x", $self->_date),
2310       sprintf("%.2f", $self->charged),
2311       ( map { $cust_main->getfield($_) }
2312           qw( first last company address1 address2 city state zip country ) ),
2313       map { '' } (1..5),
2314     ) or die "can't create csv";
2315   }
2316
2317   my $header = $csv->string. "\n";
2318
2319   my $detail = '';
2320   if ( lc($opt{'format'}) eq 'billco' ) {
2321
2322     my $lineseq = 0;
2323     foreach my $item ( $self->_items_pkg ) {
2324
2325       $csv->combine(
2326         '',                     #  1 | N/A-Leave Empty            CHAR   2
2327         '',                     #  2 | N/A-Leave Empty            CHAR  15
2328         $opt{'tracctnum'},      #  3 | Account Number             CHAR  15
2329         $self->invnum,          #  4 | Invoice Number             CHAR  15
2330         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
2331         $item->{'description'}, #  6 | Transaction Detail         CHAR 100
2332         $item->{'amount'},      #  7 | Amount                     NUM*   9
2333         '',                     #  8 | Line Format Control**      CHAR   2
2334         '',                     #  9 | Grouping Code              CHAR   2
2335         '',                     # 10 | User Defined               CHAR  15
2336       );
2337
2338       $detail .= $csv->string. "\n";
2339
2340     }
2341
2342   } elsif ( lc($opt{'format'}) eq 'oneline' ) {
2343
2344     #do nothing
2345
2346   } else {
2347
2348     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2349
2350       my($pkg, $setup, $recur, $sdate, $edate);
2351       if ( $cust_bill_pkg->pkgnum ) {
2352       
2353         ($pkg, $setup, $recur, $sdate, $edate) = (
2354           $cust_bill_pkg->part_pkg->pkg,
2355           ( $cust_bill_pkg->setup != 0
2356             ? sprintf("%.2f", $cust_bill_pkg->setup )
2357             : '' ),
2358           ( $cust_bill_pkg->recur != 0
2359             ? sprintf("%.2f", $cust_bill_pkg->recur )
2360             : '' ),
2361           ( $cust_bill_pkg->sdate 
2362             ? time2str("%x", $cust_bill_pkg->sdate)
2363             : '' ),
2364           ($cust_bill_pkg->edate 
2365             ?time2str("%x", $cust_bill_pkg->edate)
2366             : '' ),
2367         );
2368   
2369       } else { #pkgnum tax
2370         next unless $cust_bill_pkg->setup != 0;
2371         $pkg = $cust_bill_pkg->desc;
2372         $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2373         ( $sdate, $edate ) = ( '', '' );
2374       }
2375   
2376       $csv->combine(
2377         'cust_bill_pkg',
2378         $self->invnum,
2379         ( map { '' } (1..11) ),
2380         ($pkg, $setup, $recur, $sdate, $edate)
2381       ) or die "can't create csv";
2382
2383       $detail .= $csv->string. "\n";
2384
2385     }
2386
2387   }
2388
2389   ( $header, $detail );
2390
2391 }
2392
2393 =item comp
2394
2395 Pays this invoice with a compliemntary payment.  If there is an error,
2396 returns the error, otherwise returns false.
2397
2398 =cut
2399
2400 sub comp {
2401   my $self = shift;
2402   my $cust_pay = new FS::cust_pay ( {
2403     'invnum'   => $self->invnum,
2404     'paid'     => $self->owed,
2405     '_date'    => '',
2406     'payby'    => 'COMP',
2407     'payinfo'  => $self->cust_main->payinfo,
2408     'paybatch' => '',
2409   } );
2410   $cust_pay->insert;
2411 }
2412
2413 =item realtime_card
2414
2415 Attempts to pay this invoice with a credit card payment via a
2416 Business::OnlinePayment realtime gateway.  See
2417 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2418 for supported processors.
2419
2420 =cut
2421
2422 sub realtime_card {
2423   my $self = shift;
2424   $self->realtime_bop( 'CC', @_ );
2425 }
2426
2427 =item realtime_ach
2428
2429 Attempts to pay this invoice with an electronic check (ACH) payment via a
2430 Business::OnlinePayment realtime gateway.  See
2431 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2432 for supported processors.
2433
2434 =cut
2435
2436 sub realtime_ach {
2437   my $self = shift;
2438   $self->realtime_bop( 'ECHECK', @_ );
2439 }
2440
2441 =item realtime_lec
2442
2443 Attempts to pay this invoice with phone bill (LEC) payment via a
2444 Business::OnlinePayment realtime gateway.  See
2445 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2446 for supported processors.
2447
2448 =cut
2449
2450 sub realtime_lec {
2451   my $self = shift;
2452   $self->realtime_bop( 'LEC', @_ );
2453 }
2454
2455 sub realtime_bop {
2456   my( $self, $method ) = (shift,shift);
2457   my $conf = $self->conf;
2458   my %opt = @_;
2459
2460   my $cust_main = $self->cust_main;
2461   my $balance = $cust_main->balance;
2462   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2463   $amount = sprintf("%.2f", $amount);
2464   return "not run (balance $balance)" unless $amount > 0;
2465
2466   my $description = 'Internet Services';
2467   if ( $conf->exists('business-onlinepayment-description') ) {
2468     my $dtempl = $conf->config('business-onlinepayment-description');
2469
2470     my $agent_obj = $cust_main->agent
2471       or die "can't retreive agent for $cust_main (agentnum ".
2472              $cust_main->agentnum. ")";
2473     my $agent = $agent_obj->agent;
2474     my $pkgs = join(', ',
2475       map { $_->part_pkg->pkg }
2476         grep { $_->pkgnum } $self->cust_bill_pkg
2477     );
2478     $description = eval qq("$dtempl");
2479   }
2480
2481   $cust_main->realtime_bop($method, $amount,
2482     'description' => $description,
2483     'invnum'      => $self->invnum,
2484 #this didn't do what we want, it just calls apply_payments_and_credits
2485 #    'apply'       => 1,
2486     'apply_to_invoice' => 1,
2487     %opt,
2488  #what we want:
2489  #this changes application behavior: auto payments
2490                         #triggered against a specific invoice are now applied
2491                         #to that invoice instead of oldest open.
2492                         #seem okay to me...
2493   );
2494
2495 }
2496
2497 =item batch_card OPTION => VALUE...
2498
2499 Adds a payment for this invoice to the pending credit card batch (see
2500 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2501 runs the payment using a realtime gateway.
2502
2503 =cut
2504
2505 sub batch_card {
2506   my ($self, %options) = @_;
2507   my $cust_main = $self->cust_main;
2508
2509   $options{invnum} = $self->invnum;
2510   
2511   $cust_main->batch_card(%options);
2512 }
2513
2514 sub _agent_template {
2515   my $self = shift;
2516   $self->cust_main->agent_template;
2517 }
2518
2519 sub _agent_invoice_from {
2520   my $self = shift;
2521   $self->cust_main->agent_invoice_from;
2522 }
2523
2524 =item invoice_barcode DIR_OR_FALSE
2525
2526 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2527 it is taken as the temp directory where the PNG file will be generated and the
2528 PNG file name is returned. Otherwise, the PNG image itself is returned.
2529
2530 =cut
2531
2532 sub invoice_barcode {
2533     my ($self, $dir) = (shift,shift);
2534     
2535     my $gdbar = new GD::Barcode('Code39',$self->invnum);
2536         die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2537     my $gd = $gdbar->plot(Height => 30);
2538
2539     if($dir) {
2540         my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2541                            DIR      => $dir,
2542                            SUFFIX   => '.png',
2543                            UNLINK   => 0,
2544                          ) or die "can't open temp file: $!\n";
2545         print $bh $gd->png or die "cannot write barcode to file: $!\n";
2546         my $png_file = $bh->filename;
2547         close $bh;
2548         return $png_file;
2549     }
2550     return $gd->png;
2551 }
2552
2553 =item invnum_date_pretty
2554
2555 Returns a string with the invoice number and date, for example:
2556 "Invoice #54 (3/20/2008)"
2557
2558 =cut
2559
2560 sub invnum_date_pretty {
2561   my $self = shift;
2562   $self->mt('Invoice #'). $self->invnum. ' ('. $self->_date_pretty. ')';
2563 }
2564
2565 #sub _items_extra_usage_sections {
2566 #  my $self = shift;
2567 #  my $escape = shift;
2568 #
2569 #  my %sections = ();
2570 #
2571 #  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
2572 #  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2573 #  {
2574 #    next unless $cust_bill_pkg->pkgnum > 0;
2575 #
2576 #    foreach my $section ( keys %usage_class ) {
2577 #
2578 #      my $usage = $cust_bill_pkg->usage($section);
2579 #
2580 #      next unless $usage && $usage > 0;
2581 #
2582 #      $sections{$section} ||= 0;
2583 #      $sections{$section} += $usage;
2584 #
2585 #    }
2586 #
2587 #  }
2588 #
2589 #  map { { 'description' => &{$escape}($_),
2590 #          'subtotal'    => $sections{$_},
2591 #          'summarized'  => '',
2592 #          'tax_section' => '',
2593 #        }
2594 #      }
2595 #    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
2596 #
2597 #}
2598
2599 sub _items_extra_usage_sections {
2600   my $self = shift;
2601   my $conf = $self->conf;
2602   my $escape = shift;
2603   my $format = shift;
2604
2605   my %sections = ();
2606   my %classnums = ();
2607   my %lines = ();
2608
2609   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2610
2611   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
2612   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2613     next unless $cust_bill_pkg->pkgnum > 0;
2614
2615     foreach my $classnum ( keys %usage_class ) {
2616       my $section = $usage_class{$classnum}->classname;
2617       $classnums{$section} = $classnum;
2618
2619       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
2620         my $amount = $detail->amount;
2621         next unless $amount && $amount > 0;
2622  
2623         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
2624         $sections{$section}{amount} += $amount;  #subtotal
2625         $sections{$section}{calls}++;
2626         $sections{$section}{duration} += $detail->duration;
2627
2628         my $desc = $detail->regionname; 
2629         my $description = $desc;
2630         $description = substr($desc, 0, $maxlength). '...'
2631           if $format eq 'latex' && length($desc) > $maxlength;
2632
2633         $lines{$section}{$desc} ||= {
2634           description     => &{$escape}($description),
2635           #pkgpart         => $part_pkg->pkgpart,
2636           pkgnum          => $cust_bill_pkg->pkgnum,
2637           ref             => '',
2638           amount          => 0,
2639           calls           => 0,
2640           duration        => 0,
2641           #unit_amount     => $cust_bill_pkg->unitrecur,
2642           quantity        => $cust_bill_pkg->quantity,
2643           product_code    => 'N/A',
2644           ext_description => [],
2645         };
2646
2647         $lines{$section}{$desc}{amount} += $amount;
2648         $lines{$section}{$desc}{calls}++;
2649         $lines{$section}{$desc}{duration} += $detail->duration;
2650
2651       }
2652     }
2653   }
2654
2655   my %sectionmap = ();
2656   foreach (keys %sections) {
2657     my $usage_class = $usage_class{$classnums{$_}};
2658     $sectionmap{$_} = { 'description' => &{$escape}($_),
2659                         'amount'    => $sections{$_}{amount},    #subtotal
2660                         'calls'       => $sections{$_}{calls},
2661                         'duration'    => $sections{$_}{duration},
2662                         'summarized'  => '',
2663                         'tax_section' => '',
2664                         'sort_weight' => $usage_class->weight,
2665                         ( $usage_class->format
2666                           ? ( map { $_ => $usage_class->$_($format) }
2667                               qw( description_generator header_generator total_generator total_line_generator )
2668                             )
2669                           : ()
2670                         ), 
2671                       };
2672   }
2673
2674   my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
2675                  values %sectionmap;
2676
2677   my @lines = ();
2678   foreach my $section ( keys %lines ) {
2679     foreach my $line ( keys %{$lines{$section}} ) {
2680       my $l = $lines{$section}{$line};
2681       $l->{section}     = $sectionmap{$section};
2682       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
2683       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
2684       push @lines, $l;
2685     }
2686   }
2687
2688   return(\@sections, \@lines);
2689
2690 }
2691
2692 sub _did_summary {
2693     my $self = shift;
2694     my $end = $self->_date;
2695
2696     # start at date of previous invoice + 1 second or 0 if no previous invoice
2697     my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
2698     $start = 0 if !$start;
2699     $start++;
2700
2701     my $cust_main = $self->cust_main;
2702     my @pkgs = $cust_main->all_pkgs;
2703     my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
2704         = (0,0,0,0,0);
2705     my @seen = ();
2706     foreach my $pkg ( @pkgs ) {
2707         my @h_cust_svc = $pkg->h_cust_svc($end);
2708         foreach my $h_cust_svc ( @h_cust_svc ) {
2709             next if grep {$_ eq $h_cust_svc->svcnum} @seen;
2710             next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
2711
2712             my $inserted = $h_cust_svc->date_inserted;
2713             my $deleted = $h_cust_svc->date_deleted;
2714             my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
2715             my $phone_deleted;
2716             $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
2717             
2718 # DID either activated or ported in; cannot be both for same DID simultaneously
2719             if ($inserted >= $start && $inserted <= $end && $phone_inserted
2720                 && (!$phone_inserted->lnp_status 
2721                     || $phone_inserted->lnp_status eq ''
2722                     || $phone_inserted->lnp_status eq 'native')) {
2723                 $num_activated++;
2724             }
2725             else { # this one not so clean, should probably move to (h_)svc_phone
2726                  my $phone_portedin = qsearchs( 'h_svc_phone',
2727                       { 'svcnum' => $h_cust_svc->svcnum, 
2728                         'lnp_status' => 'portedin' },  
2729                       FS::h_svc_phone->sql_h_searchs($end),  
2730                     );
2731                  $num_portedin++ if $phone_portedin;
2732             }
2733
2734 # DID either deactivated or ported out; cannot be both for same DID simultaneously
2735             if($deleted >= $start && $deleted <= $end && $phone_deleted
2736                 && (!$phone_deleted->lnp_status 
2737                     || $phone_deleted->lnp_status ne 'portingout')) {
2738                 $num_deactivated++;
2739             } 
2740             elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
2741                 && $phone_deleted->lnp_status 
2742                 && $phone_deleted->lnp_status eq 'portingout') {
2743                 $num_portedout++;
2744             }
2745
2746             # increment usage minutes
2747         if ( $phone_inserted ) {
2748             my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
2749             $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
2750         }
2751         else {
2752             warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
2753         }
2754
2755             # don't look at this service again
2756             push @seen, $h_cust_svc->svcnum;
2757         }
2758     }
2759
2760     $minutes = sprintf("%d", $minutes);
2761     ("Activated: $num_activated  Ported-In: $num_portedin  Deactivated: "
2762         . "$num_deactivated  Ported-Out: $num_portedout ",
2763             "Total Minutes: $minutes");
2764 }
2765
2766 sub _items_accountcode_cdr {
2767     my $self = shift;
2768     my $escape = shift;
2769     my $format = shift;
2770
2771     my $section = { 'amount'        => 0,
2772                     'calls'         => 0,
2773                     'duration'      => 0,
2774                     'sort_weight'   => '',
2775                     'phonenum'      => '',
2776                     'description'   => 'Usage by Account Code',
2777                     'post_total'    => '',
2778                     'summarized'    => '',
2779                     'header'        => '',
2780                   };
2781     my @lines;
2782     my %accountcodes = ();
2783
2784     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2785         next unless $cust_bill_pkg->pkgnum > 0;
2786
2787         my @header = $cust_bill_pkg->details_header;
2788         next unless scalar(@header);
2789         $section->{'header'} = join(',',@header);
2790
2791         foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
2792
2793             $section->{'header'} = $detail->formatted('format' => $format)
2794                 if($detail->detail eq $section->{'header'}); 
2795       
2796             my $accountcode = $detail->accountcode;
2797             next unless $accountcode;
2798
2799             my $amount = $detail->amount;
2800             next unless $amount && $amount > 0;
2801
2802             $accountcodes{$accountcode} ||= {
2803                     description => $accountcode,
2804                     pkgnum      => '',
2805                     ref         => '',
2806                     amount      => 0,
2807                     calls       => 0,
2808                     duration    => 0,
2809                     quantity    => '',
2810                     product_code => 'N/A',
2811                     section     => $section,
2812                     ext_description => [ $section->{'header'} ],
2813                     detail_temp => [],
2814             };
2815
2816             $section->{'amount'} += $amount;
2817             $accountcodes{$accountcode}{'amount'} += $amount;
2818             $accountcodes{$accountcode}{calls}++;
2819             $accountcodes{$accountcode}{duration} += $detail->duration;
2820             push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
2821         }
2822     }
2823
2824     foreach my $l ( values %accountcodes ) {
2825         $l->{amount} = sprintf( "%.2f", $l->{amount} );
2826         my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
2827         foreach my $sorted_detail ( @sorted_detail ) {
2828             push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
2829         }
2830         delete $l->{detail_temp};
2831         push @lines, $l;
2832     }
2833
2834     my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
2835
2836     return ($section,\@sorted_lines);
2837 }
2838
2839 sub _items_svc_phone_sections {
2840   my $self = shift;
2841   my $conf = $self->conf;
2842   my $escape = shift;
2843   my $format = shift;
2844
2845   my %sections = ();
2846   my %classnums = ();
2847   my %lines = ();
2848
2849   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2850
2851   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
2852   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
2853
2854   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2855     next unless $cust_bill_pkg->pkgnum > 0;
2856
2857     my @header = $cust_bill_pkg->details_header;
2858     next unless scalar(@header);
2859
2860     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
2861
2862       my $phonenum = $detail->phonenum;
2863       next unless $phonenum;
2864
2865       my $amount = $detail->amount;
2866       next unless $amount && $amount > 0;
2867
2868       $sections{$phonenum} ||= { 'amount'      => 0,
2869                                  'calls'       => 0,
2870                                  'duration'    => 0,
2871                                  'sort_weight' => -1,
2872                                  'phonenum'    => $phonenum,
2873                                 };
2874       $sections{$phonenum}{amount} += $amount;  #subtotal
2875       $sections{$phonenum}{calls}++;
2876       $sections{$phonenum}{duration} += $detail->duration;
2877
2878       my $desc = $detail->regionname; 
2879       my $description = $desc;
2880       $description = substr($desc, 0, $maxlength). '...'
2881         if $format eq 'latex' && length($desc) > $maxlength;
2882
2883       $lines{$phonenum}{$desc} ||= {
2884         description     => &{$escape}($description),
2885         #pkgpart         => $part_pkg->pkgpart,
2886         pkgnum          => '',
2887         ref             => '',
2888         amount          => 0,
2889         calls           => 0,
2890         duration        => 0,
2891         #unit_amount     => '',
2892         quantity        => '',
2893         product_code    => 'N/A',
2894         ext_description => [],
2895       };
2896
2897       $lines{$phonenum}{$desc}{amount} += $amount;
2898       $lines{$phonenum}{$desc}{calls}++;
2899       $lines{$phonenum}{$desc}{duration} += $detail->duration;
2900
2901       my $line = $usage_class{$detail->classnum}->classname;
2902       $sections{"$phonenum $line"} ||=
2903         { 'amount' => 0,
2904           'calls' => 0,
2905           'duration' => 0,
2906           'sort_weight' => $usage_class{$detail->classnum}->weight,
2907           'phonenum' => $phonenum,
2908           'header'  => [ @header ],
2909         };
2910       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
2911       $sections{"$phonenum $line"}{calls}++;
2912       $sections{"$phonenum $line"}{duration} += $detail->duration;
2913
2914       $lines{"$phonenum $line"}{$desc} ||= {
2915         description     => &{$escape}($description),
2916         #pkgpart         => $part_pkg->pkgpart,
2917         pkgnum          => '',
2918         ref             => '',
2919         amount          => 0,
2920         calls           => 0,
2921         duration        => 0,
2922         #unit_amount     => '',
2923         quantity        => '',
2924         product_code    => 'N/A',
2925         ext_description => [],
2926       };
2927
2928       $lines{"$phonenum $line"}{$desc}{amount} += $amount;
2929       $lines{"$phonenum $line"}{$desc}{calls}++;
2930       $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
2931       push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
2932            $detail->formatted('format' => $format);
2933
2934     }
2935   }
2936
2937   my %sectionmap = ();
2938   my $simple = new FS::usage_class { format => 'simple' }; #bleh
2939   foreach ( keys %sections ) {
2940     my @header = @{ $sections{$_}{header} || [] };
2941     my $usage_simple =
2942       new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
2943     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
2944     my $usage_class = $summary ? $simple : $usage_simple;
2945     my $ending = $summary ? ' usage charges' : '';
2946     my %gen_opt = ();
2947     unless ($summary) {
2948       $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
2949     }
2950     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
2951                         'amount'    => $sections{$_}{amount},    #subtotal
2952                         'calls'       => $sections{$_}{calls},
2953                         'duration'    => $sections{$_}{duration},
2954                         'summarized'  => '',
2955                         'tax_section' => '',
2956                         'phonenum'    => $sections{$_}{phonenum},
2957                         'sort_weight' => $sections{$_}{sort_weight},
2958                         'post_total'  => $summary, #inspire pagebreak
2959                         (
2960                           ( map { $_ => $usage_class->$_($format, %gen_opt) }
2961                             qw( description_generator
2962                                 header_generator
2963                                 total_generator
2964                                 total_line_generator
2965                               )
2966                           )
2967                         ), 
2968                       };
2969   }
2970
2971   my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
2972                         $a->{sort_weight} <=> $b->{sort_weight}
2973                       }
2974                  values %sectionmap;
2975
2976   my @lines = ();
2977   foreach my $section ( keys %lines ) {
2978     foreach my $line ( keys %{$lines{$section}} ) {
2979       my $l = $lines{$section}{$line};
2980       $l->{section}     = $sectionmap{$section};
2981       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
2982       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
2983       push @lines, $l;
2984     }
2985   }
2986   
2987   if($conf->exists('phone_usage_class_summary')) { 
2988       # this only works with Latex
2989       my @newlines;
2990       my @newsections;
2991
2992       # after this, we'll have only two sections per DID:
2993       # Calls Summary and Calls Detail
2994       foreach my $section ( @sections ) {
2995         if($section->{'post_total'}) {
2996             $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
2997             $section->{'total_line_generator'} = sub { '' };
2998             $section->{'total_generator'} = sub { '' };
2999             $section->{'header_generator'} = sub { '' };
3000             $section->{'description_generator'} = '';
3001             push @newsections, $section;
3002             my %calls_detail = %$section;
3003             $calls_detail{'post_total'} = '';
3004             $calls_detail{'sort_weight'} = '';
3005             $calls_detail{'description_generator'} = sub { '' };
3006             $calls_detail{'header_generator'} = sub {
3007                 return ' & Date/Time & Called Number & Duration & Price'
3008                     if $format eq 'latex';
3009                 '';
3010             };
3011             $calls_detail{'description'} = 'Calls Detail: '
3012                                                     . $section->{'phonenum'};
3013             push @newsections, \%calls_detail;  
3014         }
3015       }
3016
3017       # after this, each usage class is collapsed/summarized into a single
3018       # line under the Calls Summary section
3019       foreach my $newsection ( @newsections ) {
3020         if($newsection->{'post_total'}) { # this means Calls Summary
3021             foreach my $section ( @sections ) {
3022                 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'} 
3023                                 && !$section->{'post_total'});
3024                 my $newdesc = $section->{'description'};
3025                 my $tn = $section->{'phonenum'};
3026                 $newdesc =~ s/$tn//g;
3027                 my $line = {  ext_description => [],
3028                               pkgnum => '',
3029                               ref => '',
3030                               quantity => '',
3031                               calls => $section->{'calls'},
3032                               section => $newsection,
3033                               duration => $section->{'duration'},
3034                               description => $newdesc,
3035                               amount => sprintf("%.2f",$section->{'amount'}),
3036                               product_code => 'N/A',
3037                             };
3038                 push @newlines, $line;
3039             }
3040         }
3041       }
3042
3043       # after this, Calls Details is populated with all CDRs
3044       foreach my $newsection ( @newsections ) {
3045         if(!$newsection->{'post_total'}) { # this means Calls Details
3046             foreach my $line ( @lines ) {
3047                 next unless (scalar(@{$line->{'ext_description'}}) &&
3048                         $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
3049                             );
3050                 my @extdesc = @{$line->{'ext_description'}};
3051                 my @newextdesc;
3052                 foreach my $extdesc ( @extdesc ) {
3053                     $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
3054                     push @newextdesc, $extdesc;
3055                 }
3056                 $line->{'ext_description'} = \@newextdesc;
3057                 $line->{'section'} = $newsection;
3058                 push @newlines, $line;
3059             }
3060         }
3061       }
3062
3063       return(\@newsections, \@newlines);
3064   }
3065
3066   return(\@sections, \@lines);
3067
3068 }
3069
3070 sub _items_previous {
3071   my $self = shift;
3072   my $conf = $self->conf;
3073   my $cust_main = $self->cust_main;
3074   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
3075   my @b = ();
3076   foreach ( @pr_cust_bill ) {
3077     my $date = $conf->exists('invoice_show_prior_due_date')
3078                ? 'due '. $_->due_date2str($date_format)
3079                : time2str($date_format, $_->_date);
3080     push @b, {
3081       'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
3082       #'pkgpart'     => 'N/A',
3083       'pkgnum'      => 'N/A',
3084       'amount'      => sprintf("%.2f", $_->owed),
3085     };
3086   }
3087   @b;
3088
3089   #{
3090   #    'description'     => 'Previous Balance',
3091   #    #'pkgpart'         => 'N/A',
3092   #    'pkgnum'          => 'N/A',
3093   #    'amount'          => sprintf("%10.2f", $pr_total ),
3094   #    'ext_description' => [ map {
3095   #                                 "Invoice ". $_->invnum.
3096   #                                 " (". time2str("%x",$_->_date). ") ".
3097   #                                 sprintf("%10.2f", $_->owed)
3098   #                         } @pr_cust_bill ],
3099
3100   #};
3101 }
3102
3103 sub _items_credits {
3104   my( $self, %opt ) = @_;
3105   my $trim_len = $opt{'trim_len'} || 60;
3106
3107   my @b;
3108   #credits
3109   foreach ( $self->cust_credited ) {
3110
3111     #something more elaborate if $_->amount ne $_->cust_credit->credited ?
3112
3113     my $reason = substr($_->cust_credit->reason, 0, $trim_len);
3114     $reason .= '...' if length($reason) < length($_->cust_credit->reason);
3115     $reason = " ($reason) " if $reason;
3116
3117     push @b, {
3118       #'description' => 'Credit ref\#'. $_->crednum.
3119       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
3120       #                 $reason,
3121       'description' => $self->mt('Credit applied').' '.
3122                        time2str($date_format,$_->cust_credit->_date). $reason,
3123       'amount'      => sprintf("%.2f",$_->amount),
3124     };
3125   }
3126
3127   @b;
3128
3129 }
3130
3131 sub _items_payments {
3132   my $self = shift;
3133
3134   my @b;
3135   #get & print payments
3136   foreach ( $self->cust_bill_pay ) {
3137
3138     #something more elaborate if $_->amount ne ->cust_pay->paid ?
3139
3140     push @b, {
3141       'description' => $self->mt('Payment received').' '.
3142                        time2str($date_format,$_->cust_pay->_date ),
3143       'amount'      => sprintf("%.2f", $_->amount )
3144     };
3145   }
3146
3147   @b;
3148
3149 }
3150
3151 =item call_details [ OPTION => VALUE ... ]
3152
3153 Returns an array of CSV strings representing the call details for this invoice
3154 The only option available is the boolean prepend_billed_number
3155
3156 =cut
3157
3158 sub call_details {
3159   my ($self, %opt) = @_;
3160
3161   my $format_function = sub { shift };
3162
3163   if ($opt{prepend_billed_number}) {
3164     $format_function = sub {
3165       my $detail = shift;
3166       my $row = shift;
3167
3168       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
3169       
3170     };
3171   }
3172
3173   my @details = map { $_->details( 'format_function' => $format_function,
3174                                    'escape_function' => sub{ return() },
3175                                  )
3176                     }
3177                   grep { $_->pkgnum }
3178                   $self->cust_bill_pkg;
3179   my $header = $details[0];
3180   ( $header, grep { $_ ne $header } @details );
3181 }
3182
3183
3184 =back
3185
3186 =head1 SUBROUTINES
3187
3188 =over 4
3189
3190 =item process_reprint
3191
3192 =cut
3193
3194 sub process_reprint {
3195   process_re_X('print', @_);
3196 }
3197
3198 =item process_reemail
3199
3200 =cut
3201
3202 sub process_reemail {
3203   process_re_X('email', @_);
3204 }
3205
3206 =item process_refax
3207
3208 =cut
3209
3210 sub process_refax {
3211   process_re_X('fax', @_);
3212 }
3213
3214 =item process_reftp
3215
3216 =cut
3217
3218 sub process_reftp {
3219   process_re_X('ftp', @_);
3220 }
3221
3222 =item respool
3223
3224 =cut
3225
3226 sub process_respool {
3227   process_re_X('spool', @_);
3228 }
3229
3230 use Storable qw(thaw);
3231 use Data::Dumper;
3232 use MIME::Base64;
3233 sub process_re_X {
3234   my( $method, $job ) = ( shift, shift );
3235   warn "$me process_re_X $method for job $job\n" if $DEBUG;
3236
3237   my $param = thaw(decode_base64(shift));
3238   warn Dumper($param) if $DEBUG;
3239
3240   re_X(
3241     $method,
3242     $job,
3243     %$param,
3244   );
3245
3246 }
3247
3248 sub re_X {
3249   # spool_invoice ftp_invoice fax_invoice print_invoice
3250   my($method, $job, %param ) = @_;
3251   if ( $DEBUG ) {
3252     warn "re_X $method for job $job with param:\n".
3253          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
3254   }
3255
3256   #some false laziness w/search/cust_bill.html
3257   my $distinct = '';
3258   my $orderby = 'ORDER BY cust_bill._date';
3259
3260   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
3261
3262   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
3263      
3264   my @cust_bill = qsearch( {
3265     #'select'    => "cust_bill.*",
3266     'table'     => 'cust_bill',
3267     'addl_from' => $addl_from,
3268     'hashref'   => {},
3269     'extra_sql' => $extra_sql,
3270     'order_by'  => $orderby,
3271     'debug' => 1,
3272   } );
3273
3274   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3275
3276   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3277     if $DEBUG;
3278
3279   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3280   foreach my $cust_bill ( @cust_bill ) {
3281     $cust_bill->$method();
3282
3283     if ( $job ) { #progressbar foo
3284       $num++;
3285       if ( time - $min_sec > $last ) {
3286         my $error = $job->update_statustext(
3287           int( 100 * $num / scalar(@cust_bill) )
3288         );
3289         die $error if $error;
3290         $last = time;
3291       }
3292     }
3293
3294   }
3295
3296 }
3297
3298 =back
3299
3300 =head1 CLASS METHODS
3301
3302 =over 4
3303
3304 =item owed_sql
3305
3306 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3307
3308 =cut
3309
3310 sub owed_sql {
3311   my ($class, $start, $end) = @_;
3312   'charged - '. 
3313     $class->paid_sql($start, $end). ' - '. 
3314     $class->credited_sql($start, $end);
3315 }
3316
3317 =item net_sql
3318
3319 Returns an SQL fragment to retreive the net amount (charged minus credited).
3320
3321 =cut
3322
3323 sub net_sql {
3324   my ($class, $start, $end) = @_;
3325   'charged - '. $class->credited_sql($start, $end);
3326 }
3327
3328 =item paid_sql
3329
3330 Returns an SQL fragment to retreive the amount paid against this invoice.
3331
3332 =cut
3333
3334 sub paid_sql {
3335   my ($class, $start, $end) = @_;
3336   $start &&= "AND cust_bill_pay._date <= $start";
3337   $end   &&= "AND cust_bill_pay._date > $end";
3338   $start = '' unless defined($start);
3339   $end   = '' unless defined($end);
3340   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3341        WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
3342 }
3343
3344 =item credited_sql
3345
3346 Returns an SQL fragment to retreive the amount credited against this invoice.
3347
3348 =cut
3349
3350 sub credited_sql {
3351   my ($class, $start, $end) = @_;
3352   $start &&= "AND cust_credit_bill._date <= $start";
3353   $end   &&= "AND cust_credit_bill._date >  $end";
3354   $start = '' unless defined($start);
3355   $end   = '' unless defined($end);
3356   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3357        WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
3358 }
3359
3360 =item due_date_sql
3361
3362 Returns an SQL fragment to retrieve the due date of an invoice.
3363 Currently only supported on PostgreSQL.
3364
3365 =cut
3366
3367 sub due_date_sql {
3368   my $conf = new FS::Conf;
3369 'COALESCE(
3370   SUBSTRING(
3371     COALESCE(
3372       cust_bill.invoice_terms,
3373       cust_main.invoice_terms,
3374       \''.($conf->config('invoice_default_terms') || '').'\'
3375     ), E\'Net (\\\\d+)\'
3376   )::INTEGER, 0
3377 ) * 86400 + cust_bill._date'
3378 }
3379
3380 =item search_sql_where HASHREF
3381
3382 Class method which returns an SQL WHERE fragment to search for parameters
3383 specified in HASHREF.  Valid parameters are
3384
3385 =over 4
3386
3387 =item _date
3388
3389 List reference of start date, end date, as UNIX timestamps.
3390
3391 =item invnum_min
3392
3393 =item invnum_max
3394
3395 =item agentnum
3396
3397 =item charged
3398
3399 List reference of charged limits (exclusive).
3400
3401 =item owed
3402
3403 List reference of charged limits (exclusive).
3404
3405 =item open
3406
3407 flag, return open invoices only
3408
3409 =item net
3410
3411 flag, return net invoices only
3412
3413 =item days
3414
3415 =item newest_percust
3416
3417 =back
3418
3419 Note: validates all passed-in data; i.e. safe to use with unchecked CGI params.
3420
3421 =cut
3422
3423 sub search_sql_where {
3424   my($class, $param) = @_;
3425   if ( $DEBUG ) {
3426     warn "$me search_sql_where called with params: \n".
3427          join("\n", map { "  $_: ". $param->{$_} } keys %$param ). "\n";
3428   }
3429
3430   my @search = ();
3431
3432   #agentnum
3433   if ( $param->{'agentnum'} =~ /^(\d+)$/ ) {
3434     push @search, "cust_main.agentnum = $1";
3435   }
3436
3437   #refnum
3438   if ( $param->{'refnum'} =~ /^(\d+)$/ ) {
3439     push @search, "cust_main.refnum = $1";
3440   }
3441
3442   #custnum
3443   if ( $param->{'custnum'} =~ /^(\d+)$/ ) {
3444     push @search, "cust_bill.custnum = $1";
3445   }
3446
3447   #customer classnum
3448   if ( $param->{'cust_classnum'} ) {
3449     my $classnums = $param->{'cust_classnum'};
3450     $classnums = [ $classnums ] if !ref($classnums);
3451     $classnums = [ grep /^\d+$/, @$classnums ];
3452     push @search, 'cust_main.classnum in ('.join(',',@$classnums).')'
3453       if @$classnums;
3454   }
3455
3456   #_date
3457   if ( $param->{_date} ) {
3458     my($beginning, $ending) = @{$param->{_date}};
3459
3460     push @search, "cust_bill._date >= $beginning",
3461                   "cust_bill._date <  $ending";
3462   }
3463
3464   #invnum
3465   if ( $param->{'invnum_min'} =~ /^(\d+)$/ ) {
3466     push @search, "cust_bill.invnum >= $1";
3467   }
3468   if ( $param->{'invnum_max'} =~ /^(\d+)$/ ) {
3469     push @search, "cust_bill.invnum <= $1";
3470   }
3471
3472   #charged
3473   if ( $param->{charged} ) {
3474     my @charged = ref($param->{charged})
3475                     ? @{ $param->{charged} }
3476                     : ($param->{charged});
3477
3478     push @search, map { s/^charged/cust_bill.charged/; $_; }
3479                       @charged;
3480   }
3481
3482   my $owed_sql = FS::cust_bill->owed_sql;
3483
3484   #owed
3485   if ( $param->{owed} ) {
3486     my @owed = ref($param->{owed})
3487                  ? @{ $param->{owed} }
3488                  : ($param->{owed});
3489     push @search, map { s/^owed/$owed_sql/; $_; }
3490                       @owed;
3491   }
3492
3493   #open/net flags
3494   push @search, "0 != $owed_sql"
3495     if $param->{'open'};
3496   push @search, '0 != '. FS::cust_bill->net_sql
3497     if $param->{'net'};
3498
3499   #days
3500   push @search, "cust_bill._date < ". (time-86400*$param->{'days'})
3501     if $param->{'days'};
3502
3503   #newest_percust
3504   if ( $param->{'newest_percust'} ) {
3505
3506     #$distinct = 'DISTINCT ON ( cust_bill.custnum )';
3507     #$orderby = 'ORDER BY cust_bill.custnum ASC, cust_bill._date DESC';
3508
3509     my @newest_where = map { my $x = $_;
3510                              $x =~ s/\bcust_bill\./newest_cust_bill./g;
3511                              $x;
3512                            }
3513                            grep ! /^cust_main./, @search;
3514     my $newest_where = scalar(@newest_where)
3515                          ? ' AND '. join(' AND ', @newest_where)
3516                          : '';
3517
3518
3519     push @search, "cust_bill._date = (
3520       SELECT(MAX(newest_cust_bill._date)) FROM cust_bill AS newest_cust_bill
3521         WHERE newest_cust_bill.custnum = cust_bill.custnum
3522           $newest_where
3523     )";
3524
3525   }
3526
3527   #promised_date - also has an option to accept nulls
3528   if ( $param->{promised_date} ) {
3529     my($beginning, $ending, $null) = @{$param->{promised_date}};
3530
3531     push @search, "(( cust_bill.promised_date >= $beginning AND ".
3532                     "cust_bill.promised_date <  $ending )" .
3533                     ($null ? ' OR cust_bill.promised_date IS NULL ) ' : ')');
3534   }
3535
3536   #agent virtualization
3537   my $curuser = $FS::CurrentUser::CurrentUser;
3538   if ( $curuser->username eq 'fs_queue'
3539        && $param->{'CurrentUser'} =~ /^(\w+)$/ ) {
3540     my $username = $1;
3541     my $newuser = qsearchs('access_user', {
3542       'username' => $username,
3543       'disabled' => '',
3544     } );
3545     if ( $newuser ) {
3546       $curuser = $newuser;
3547     } else {
3548       warn "$me WARNING: (fs_queue) can't find CurrentUser $username\n";
3549     }
3550   }
3551   push @search, $curuser->agentnums_sql;
3552
3553   join(' AND ', @search );
3554
3555 }
3556
3557 =back
3558
3559 =head1 BUGS
3560
3561 The delete method.
3562
3563 =head1 SEE ALSO
3564
3565 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3566 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
3567 documentation.
3568
3569 =cut
3570
3571 1;
3572