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