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