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