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