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