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