07c33204e84181f42210a0f939894998e6ffe166
[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                  my $phone_portedin = qsearchs( 'h_svc_phone',
2633                       { 'svcnum' => $h_cust_svc->svcnum, 
2634                         'lnp_status' => 'portedin' },  
2635                       FS::h_svc_phone->sql_h_searchs($end),  
2636                     );
2637                  $num_portedin++ if $phone_portedin;
2638             }
2639
2640 # DID either deactivated or ported out; cannot be both for same DID simultaneously
2641             if($deleted >= $start && $deleted <= $end && $phone_deleted
2642                 && (!$phone_deleted->lnp_status 
2643                     || $phone_deleted->lnp_status ne 'portingout')) {
2644                 $num_deactivated++;
2645             } 
2646             elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
2647                 && $phone_deleted->lnp_status 
2648                 && $phone_deleted->lnp_status eq 'portingout') {
2649                 $num_portedout++;
2650             }
2651
2652             # increment usage minutes
2653         if ( $phone_inserted ) {
2654             my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
2655             $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
2656         }
2657         else {
2658             warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
2659         }
2660
2661             # don't look at this service again
2662             push @seen, $h_cust_svc->svcnum;
2663         }
2664     }
2665
2666     $minutes = sprintf("%d", $minutes);
2667     ("Activated: $num_activated  Ported-In: $num_portedin  Deactivated: "
2668         . "$num_deactivated  Ported-Out: $num_portedout ",
2669             "Total Minutes: $minutes");
2670 }
2671
2672 sub _items_accountcode_cdr {
2673     my $self = shift;
2674     my $escape = shift;
2675     my $format = shift;
2676
2677     my $section = { 'amount'        => 0,
2678                     'calls'         => 0,
2679                     'duration'      => 0,
2680                     'sort_weight'   => '',
2681                     'phonenum'      => '',
2682                     'description'   => 'Usage by Account Code',
2683                     'post_total'    => '',
2684                     'summarized'    => '',
2685                     'header'        => '',
2686                   };
2687     my @lines;
2688     my %accountcodes = ();
2689
2690     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2691         next unless $cust_bill_pkg->pkgnum > 0;
2692
2693         my @header = $cust_bill_pkg->details_header;
2694         next unless scalar(@header);
2695         $section->{'header'} = join(',',@header);
2696
2697         foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
2698
2699             $section->{'header'} = $detail->formatted('format' => $format)
2700                 if($detail->detail eq $section->{'header'}); 
2701       
2702             my $accountcode = $detail->accountcode;
2703             next unless $accountcode;
2704
2705             my $amount = $detail->amount;
2706             next unless $amount && $amount > 0;
2707
2708             $accountcodes{$accountcode} ||= {
2709                     description => $accountcode,
2710                     pkgnum      => '',
2711                     ref         => '',
2712                     amount      => 0,
2713                     calls       => 0,
2714                     duration    => 0,
2715                     quantity    => '',
2716                     product_code => 'N/A',
2717                     section     => $section,
2718                     ext_description => [ $section->{'header'} ],
2719                     detail_temp => [],
2720             };
2721
2722             $section->{'amount'} += $amount;
2723             $accountcodes{$accountcode}{'amount'} += $amount;
2724             $accountcodes{$accountcode}{calls}++;
2725             $accountcodes{$accountcode}{duration} += $detail->duration;
2726             push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
2727         }
2728     }
2729
2730     foreach my $l ( values %accountcodes ) {
2731         $l->{amount} = sprintf( "%.2f", $l->{amount} );
2732         my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
2733         foreach my $sorted_detail ( @sorted_detail ) {
2734             push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
2735         }
2736         delete $l->{detail_temp};
2737         push @lines, $l;
2738     }
2739
2740     my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
2741
2742     return ($section,\@sorted_lines);
2743 }
2744
2745 sub _items_svc_phone_sections {
2746   my $self = shift;
2747   my $conf = $self->conf;
2748   my $escape = shift;
2749   my $format = shift;
2750
2751   my %sections = ();
2752   my %classnums = ();
2753   my %lines = ();
2754
2755   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2756
2757   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
2758   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
2759
2760   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2761     next unless $cust_bill_pkg->pkgnum > 0;
2762
2763     my @header = $cust_bill_pkg->details_header;
2764     next unless scalar(@header);
2765
2766     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
2767
2768       my $phonenum = $detail->phonenum;
2769       next unless $phonenum;
2770
2771       my $amount = $detail->amount;
2772       next unless $amount && $amount > 0;
2773
2774       $sections{$phonenum} ||= { 'amount'      => 0,
2775                                  'calls'       => 0,
2776                                  'duration'    => 0,
2777                                  'sort_weight' => -1,
2778                                  'phonenum'    => $phonenum,
2779                                 };
2780       $sections{$phonenum}{amount} += $amount;  #subtotal
2781       $sections{$phonenum}{calls}++;
2782       $sections{$phonenum}{duration} += $detail->duration;
2783
2784       my $desc = $detail->regionname; 
2785       my $description = $desc;
2786       $description = substr($desc, 0, $maxlength). '...'
2787         if $format eq 'latex' && length($desc) > $maxlength;
2788
2789       $lines{$phonenum}{$desc} ||= {
2790         description     => &{$escape}($description),
2791         #pkgpart         => $part_pkg->pkgpart,
2792         pkgnum          => '',
2793         ref             => '',
2794         amount          => 0,
2795         calls           => 0,
2796         duration        => 0,
2797         #unit_amount     => '',
2798         quantity        => '',
2799         product_code    => 'N/A',
2800         ext_description => [],
2801       };
2802
2803       $lines{$phonenum}{$desc}{amount} += $amount;
2804       $lines{$phonenum}{$desc}{calls}++;
2805       $lines{$phonenum}{$desc}{duration} += $detail->duration;
2806
2807       my $line = $usage_class{$detail->classnum}->classname;
2808       $sections{"$phonenum $line"} ||=
2809         { 'amount' => 0,
2810           'calls' => 0,
2811           'duration' => 0,
2812           'sort_weight' => $usage_class{$detail->classnum}->weight,
2813           'phonenum' => $phonenum,
2814           'header'  => [ @header ],
2815         };
2816       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
2817       $sections{"$phonenum $line"}{calls}++;
2818       $sections{"$phonenum $line"}{duration} += $detail->duration;
2819
2820       $lines{"$phonenum $line"}{$desc} ||= {
2821         description     => &{$escape}($description),
2822         #pkgpart         => $part_pkg->pkgpart,
2823         pkgnum          => '',
2824         ref             => '',
2825         amount          => 0,
2826         calls           => 0,
2827         duration        => 0,
2828         #unit_amount     => '',
2829         quantity        => '',
2830         product_code    => 'N/A',
2831         ext_description => [],
2832       };
2833
2834       $lines{"$phonenum $line"}{$desc}{amount} += $amount;
2835       $lines{"$phonenum $line"}{$desc}{calls}++;
2836       $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
2837       push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
2838            $detail->formatted('format' => $format);
2839
2840     }
2841   }
2842
2843   my %sectionmap = ();
2844   my $simple = new FS::usage_class { format => 'simple' }; #bleh
2845   foreach ( keys %sections ) {
2846     my @header = @{ $sections{$_}{header} || [] };
2847     my $usage_simple =
2848       new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
2849     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
2850     my $usage_class = $summary ? $simple : $usage_simple;
2851     my $ending = $summary ? ' usage charges' : '';
2852     my %gen_opt = ();
2853     unless ($summary) {
2854       $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
2855     }
2856     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
2857                         'amount'    => $sections{$_}{amount},    #subtotal
2858                         'calls'       => $sections{$_}{calls},
2859                         'duration'    => $sections{$_}{duration},
2860                         'summarized'  => '',
2861                         'tax_section' => '',
2862                         'phonenum'    => $sections{$_}{phonenum},
2863                         'sort_weight' => $sections{$_}{sort_weight},
2864                         'post_total'  => $summary, #inspire pagebreak
2865                         (
2866                           ( map { $_ => $usage_class->$_($format, %gen_opt) }
2867                             qw( description_generator
2868                                 header_generator
2869                                 total_generator
2870                                 total_line_generator
2871                               )
2872                           )
2873                         ), 
2874                       };
2875   }
2876
2877   my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
2878                         $a->{sort_weight} <=> $b->{sort_weight}
2879                       }
2880                  values %sectionmap;
2881
2882   my @lines = ();
2883   foreach my $section ( keys %lines ) {
2884     foreach my $line ( keys %{$lines{$section}} ) {
2885       my $l = $lines{$section}{$line};
2886       $l->{section}     = $sectionmap{$section};
2887       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
2888       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
2889       push @lines, $l;
2890     }
2891   }
2892   
2893   if($conf->exists('phone_usage_class_summary')) { 
2894       # this only works with Latex
2895       my @newlines;
2896       my @newsections;
2897
2898       # after this, we'll have only two sections per DID:
2899       # Calls Summary and Calls Detail
2900       foreach my $section ( @sections ) {
2901         if($section->{'post_total'}) {
2902             $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
2903             $section->{'total_line_generator'} = sub { '' };
2904             $section->{'total_generator'} = sub { '' };
2905             $section->{'header_generator'} = sub { '' };
2906             $section->{'description_generator'} = '';
2907             push @newsections, $section;
2908             my %calls_detail = %$section;
2909             $calls_detail{'post_total'} = '';
2910             $calls_detail{'sort_weight'} = '';
2911             $calls_detail{'description_generator'} = sub { '' };
2912             $calls_detail{'header_generator'} = sub {
2913                 return ' & Date/Time & Called Number & Duration & Price'
2914                     if $format eq 'latex';
2915                 '';
2916             };
2917             $calls_detail{'description'} = 'Calls Detail: '
2918                                                     . $section->{'phonenum'};
2919             push @newsections, \%calls_detail;  
2920         }
2921       }
2922
2923       # after this, each usage class is collapsed/summarized into a single
2924       # line under the Calls Summary section
2925       foreach my $newsection ( @newsections ) {
2926         if($newsection->{'post_total'}) { # this means Calls Summary
2927             foreach my $section ( @sections ) {
2928                 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'} 
2929                                 && !$section->{'post_total'});
2930                 my $newdesc = $section->{'description'};
2931                 my $tn = $section->{'phonenum'};
2932                 $newdesc =~ s/$tn//g;
2933                 my $line = {  ext_description => [],
2934                               pkgnum => '',
2935                               ref => '',
2936                               quantity => '',
2937                               calls => $section->{'calls'},
2938                               section => $newsection,
2939                               duration => $section->{'duration'},
2940                               description => $newdesc,
2941                               amount => sprintf("%.2f",$section->{'amount'}),
2942                               product_code => 'N/A',
2943                             };
2944                 push @newlines, $line;
2945             }
2946         }
2947       }
2948
2949       # after this, Calls Details is populated with all CDRs
2950       foreach my $newsection ( @newsections ) {
2951         if(!$newsection->{'post_total'}) { # this means Calls Details
2952             foreach my $line ( @lines ) {
2953                 next unless (scalar(@{$line->{'ext_description'}}) &&
2954                         $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
2955                             );
2956                 my @extdesc = @{$line->{'ext_description'}};
2957                 my @newextdesc;
2958                 foreach my $extdesc ( @extdesc ) {
2959                     $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
2960                     push @newextdesc, $extdesc;
2961                 }
2962                 $line->{'ext_description'} = \@newextdesc;
2963                 $line->{'section'} = $newsection;
2964                 push @newlines, $line;
2965             }
2966         }
2967       }
2968
2969       return(\@newsections, \@newlines);
2970   }
2971
2972   return(\@sections, \@lines);
2973
2974 }
2975
2976 =sub _items_usage_class_summary OPTIONS
2977
2978 Returns a list of detail items summarizing the usage charges on this 
2979 invoice.  Each one will have 'amount', 'description' (the usage charge name),
2980 and 'usage_classnum'.
2981
2982 OPTIONS can include 'escape' (a function to escape the descriptions).
2983
2984 =cut
2985
2986 sub _items_usage_class_summary {
2987   my $self = shift;
2988   my %opt = @_;
2989
2990   my $escape = $opt{escape} || sub { $_[0] };
2991   my $invnum = $self->invnum;
2992   my @classes = qsearch({
2993       'table'     => 'usage_class',
2994       'select'    => 'classnum, classname, SUM(amount) AS amount',
2995       'addl_from' => ' LEFT JOIN cust_bill_pkg_detail USING (classnum)' .
2996                      ' LEFT JOIN cust_bill_pkg USING (billpkgnum)',
2997       'extra_sql' => " WHERE cust_bill_pkg.invnum = $invnum".
2998                      ' GROUP BY classnum, classname, weight'.
2999                      ' HAVING (usage_class.disabled IS NULL OR SUM(amount) > 0)'.
3000                      ' ORDER BY weight ASC',
3001   });
3002   my @l;
3003   my $section = {
3004     description   => &{$escape}($self->mt('Usage Summary')),
3005     no_subtotal   => 1,
3006     usage_section => 1,
3007   };
3008   foreach my $class (@classes) {
3009     push @l, {
3010       'description'     => &{$escape}($class->classname),
3011       'amount'          => sprintf('%.2f', $class->amount),
3012       'usage_classnum'  => $class->classnum,
3013       'section'         => $section,
3014     };
3015   }
3016   return @l;
3017 }
3018
3019 sub _items_previous {
3020   my $self = shift;
3021   my $conf = $self->conf;
3022   my $cust_main = $self->cust_main;
3023   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
3024   my @b = ();
3025   foreach ( @pr_cust_bill ) {
3026     my $date = $conf->exists('invoice_show_prior_due_date')
3027                ? 'due '. $_->due_date2str('short')
3028                : $self->time2str_local('short', $_->_date);
3029     push @b, {
3030       'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
3031       #'pkgpart'     => 'N/A',
3032       'pkgnum'      => 'N/A',
3033       'amount'      => sprintf("%.2f", $_->owed),
3034     };
3035   }
3036   @b;
3037
3038   #{
3039   #    'description'     => 'Previous Balance',
3040   #    #'pkgpart'         => 'N/A',
3041   #    'pkgnum'          => 'N/A',
3042   #    'amount'          => sprintf("%10.2f", $pr_total ),
3043   #    'ext_description' => [ map {
3044   #                                 "Invoice ". $_->invnum.
3045   #                                 " (". time2str("%x",$_->_date). ") ".
3046   #                                 sprintf("%10.2f", $_->owed)
3047   #                         } @pr_cust_bill ],
3048
3049   #};
3050 }
3051
3052 sub _items_credits {
3053   my( $self, %opt ) = @_;
3054   my $trim_len = $opt{'trim_len'} || 60;
3055
3056   my @b;
3057   #credits
3058   my @objects;
3059   if ( $self->conf->exists('previous_balance-payments_since') ) {
3060     if ( $opt{'template'} eq 'statement' ) {
3061       # then the current bill is a "statement" (i.e. an invoice sent as
3062       # a payment receipt)
3063       # and in that case we want to see payments on or after THIS invoice
3064       @objects = qsearch('cust_credit', {
3065           'custnum' => $self->custnum,
3066           '_date'   => {op => '>=', value => $self->_date},
3067       });
3068     } else {
3069       my $date = 0;
3070       $date = $self->previous_bill->_date if $self->previous_bill;
3071       @objects = qsearch('cust_credit', {
3072           'custnum' => $self->custnum,
3073           '_date'   => {op => '>=', value => $date},
3074       });
3075     }
3076   } else {
3077     @objects = $self->cust_credited;
3078   }
3079
3080   foreach my $obj ( @objects ) {
3081     my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
3082
3083     my $reason = substr($cust_credit->reason, 0, $trim_len);
3084     $reason .= '...' if length($reason) < length($cust_credit->reason);
3085     $reason = " ($reason) " if $reason;
3086
3087     push @b, {
3088       #'description' => 'Credit ref\#'. $_->crednum.
3089       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
3090       #                 $reason,
3091       'description' => $self->mt('Credit applied').' '.
3092                        $self->time2str_local('short', $obj->_date). $reason,
3093       'amount'      => sprintf("%.2f",$obj->amount),
3094     };
3095   }
3096
3097   @b;
3098
3099 }
3100
3101 sub _items_payments {
3102   my $self = shift;
3103   my %opt = @_;
3104
3105   my @b;
3106   my $detailed = $self->conf->exists('invoice_payment_details');
3107   my @objects;
3108   if ( $self->conf->exists('previous_balance-payments_since') ) {
3109     # then show payments dated on/after the previous bill...
3110     if ( $opt{'template'} eq 'statement' ) {
3111       # then the current bill is a "statement" (i.e. an invoice sent as
3112       # a payment receipt)
3113       # and in that case we want to see payments on or after THIS invoice
3114       @objects = qsearch('cust_pay', {
3115           'custnum' => $self->custnum,
3116           '_date'   => {op => '>=', value => $self->_date},
3117       });
3118     } else {
3119       # the normal case: payments on or after the previous invoice
3120       my $date = 0;
3121       $date = $self->previous_bill->_date if $self->previous_bill;
3122       @objects = qsearch('cust_pay', {
3123         'custnum' => $self->custnum,
3124         '_date'   => {op => '>=', value => $date},
3125       });
3126       # and before the current bill...
3127       @objects = grep { $_->_date < $self->_date } @objects;
3128     }
3129   } else {
3130     @objects = $self->cust_bill_pay;
3131   }
3132
3133   foreach my $obj (@objects) {
3134     my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
3135     my $desc = $self->mt('Payment received').' '.
3136                $self->time2str_local('short', $cust_pay->_date );
3137     $desc .= $self->mt(' via ') .
3138              $cust_pay->payby_payinfo_pretty( $self->cust_main->locale )
3139       if $detailed;
3140
3141     push @b, {
3142       'description' => $desc,
3143       'amount'      => sprintf("%.2f", $obj->amount )
3144     };
3145   }
3146
3147   @b;
3148
3149 }
3150
3151 =item call_details [ OPTION => VALUE ... ]
3152
3153 Returns an array of CSV strings representing the call details for this invoice
3154 The only option available is the boolean prepend_billed_number
3155
3156 =cut
3157
3158 sub call_details {
3159   my ($self, %opt) = @_;
3160
3161   my $format_function = sub { shift };
3162
3163   if ($opt{prepend_billed_number}) {
3164     $format_function = sub {
3165       my $detail = shift;
3166       my $row = shift;
3167
3168       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
3169       
3170     };
3171   }
3172
3173   my @details = map { $_->details( 'format_function' => $format_function,
3174                                    'escape_function' => sub{ return() },
3175                                  )
3176                     }
3177                   grep { $_->pkgnum }
3178                   $self->cust_bill_pkg;
3179   my $header = $details[0];
3180   ( $header, grep { $_ ne $header } @details );
3181 }
3182
3183
3184 =back
3185
3186 =head1 SUBROUTINES
3187
3188 =over 4
3189
3190 =item process_reprint
3191
3192 =cut
3193
3194 sub process_reprint {
3195   process_re_X('print', @_);
3196 }
3197
3198 =item process_reemail
3199
3200 =cut
3201
3202 sub process_reemail {
3203   process_re_X('email', @_);
3204 }
3205
3206 =item process_refax
3207
3208 =cut
3209
3210 sub process_refax {
3211   process_re_X('fax', @_);
3212 }
3213
3214 =item process_reftp
3215
3216 =cut
3217
3218 sub process_reftp {
3219   process_re_X('ftp', @_);
3220 }
3221
3222 =item respool
3223
3224 =cut
3225
3226 sub process_respool {
3227   process_re_X('spool', @_);
3228 }
3229
3230 use Data::Dumper;
3231 sub process_re_X {
3232   my( $method, $job ) = ( shift, shift );
3233   warn "$me process_re_X $method for job $job\n" if $DEBUG;
3234
3235   my $param = shift;
3236   warn Dumper($param) if $DEBUG;
3237
3238   re_X(
3239     $method,
3240     $job,
3241     %$param,
3242   );
3243
3244 }
3245
3246 sub re_X {
3247   # spool_invoice ftp_invoice fax_invoice print_invoice
3248   my($method, $job, %param ) = @_;
3249   if ( $DEBUG ) {
3250     warn "re_X $method for job $job with param:\n".
3251          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
3252   }
3253
3254   #some false laziness w/search/cust_bill.html
3255   my $distinct = '';
3256   my $orderby = 'ORDER BY cust_bill._date';
3257
3258   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
3259
3260   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
3261      
3262   my @cust_bill = qsearch( {
3263     #'select'    => "cust_bill.*",
3264     'table'     => 'cust_bill',
3265     'addl_from' => $addl_from,
3266     'hashref'   => {},
3267     'extra_sql' => $extra_sql,
3268     'order_by'  => $orderby,
3269     'debug' => 1,
3270   } );
3271
3272   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3273
3274   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3275     if $DEBUG;
3276
3277   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3278   foreach my $cust_bill ( @cust_bill ) {
3279     $cust_bill->$method();
3280
3281     if ( $job ) { #progressbar foo
3282       $num++;
3283       if ( time - $min_sec > $last ) {
3284         my $error = $job->update_statustext(
3285           int( 100 * $num / scalar(@cust_bill) )
3286         );
3287         die $error if $error;
3288         $last = time;
3289       }
3290     }
3291
3292   }
3293
3294 }
3295
3296 sub API_getinfo {
3297   my $self = shift;
3298   +{ ( map { $_=>$self->$_ } $self->fields ),
3299      'owed' => $self->owed,
3300      #XXX last payment applied date
3301    };
3302 }
3303
3304 =back
3305
3306 =head1 CLASS METHODS
3307
3308 =over 4
3309
3310 =item owed_sql
3311
3312 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3313
3314 =cut
3315
3316 sub owed_sql {
3317   my ($class, $start, $end) = @_;
3318   'charged - '. 
3319     $class->paid_sql($start, $end). ' - '. 
3320     $class->credited_sql($start, $end);
3321 }
3322
3323 =item net_sql
3324
3325 Returns an SQL fragment to retreive the net amount (charged minus credited).
3326
3327 =cut
3328
3329 sub net_sql {
3330   my ($class, $start, $end) = @_;
3331   'charged - '. $class->credited_sql($start, $end);
3332 }
3333
3334 =item paid_sql
3335
3336 Returns an SQL fragment to retreive the amount paid against this invoice.
3337
3338 =cut
3339
3340 sub paid_sql {
3341   my ($class, $start, $end) = @_;
3342   $start &&= "AND cust_bill_pay._date <= $start";
3343   $end   &&= "AND cust_bill_pay._date > $end";
3344   $start = '' unless defined($start);
3345   $end   = '' unless defined($end);
3346   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3347        WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
3348 }
3349
3350 =item credited_sql
3351
3352 Returns an SQL fragment to retreive the amount credited against this invoice.
3353
3354 =cut
3355
3356 sub credited_sql {
3357   my ($class, $start, $end) = @_;
3358   $start &&= "AND cust_credit_bill._date <= $start";
3359   $end   &&= "AND cust_credit_bill._date >  $end";
3360   $start = '' unless defined($start);
3361   $end   = '' unless defined($end);
3362   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3363        WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
3364 }
3365
3366 =item due_date_sql
3367
3368 Returns an SQL fragment to retrieve the due date of an invoice.
3369 Currently only supported on PostgreSQL.
3370
3371 =cut
3372
3373 sub due_date_sql {
3374   die "don't use: doesn't account for agent-specific invoice_default_terms";
3375
3376   #we're passed a $conf but not a specific customer (that's in the query), so
3377   # to make this work we'd need an agentnum-aware "condition_sql_conf" like
3378   # "condition_sql_option" that retreives a conf value with SQL in an agent-
3379   # aware fashion
3380
3381   my $conf = new FS::Conf;
3382 'COALESCE(
3383   SUBSTRING(
3384     COALESCE(
3385       cust_bill.invoice_terms,
3386       cust_main.invoice_terms,
3387       \''.($conf->config('invoice_default_terms') || '').'\'
3388     ), E\'Net (\\\\d+)\'
3389   )::INTEGER, 0
3390 ) * 86400 + cust_bill._date'
3391 }
3392
3393 =back
3394
3395 =head1 BUGS
3396
3397 The delete method.
3398
3399 =head1 SEE ALSO
3400
3401 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3402 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
3403 documentation.
3404
3405 =cut
3406
3407 1;
3408