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