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