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