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