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