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