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