RT# 78547 - Flag to disable email/print/fax/etc during tests or reports
[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   if ( $FS::Misc::DISABLE_ALL_NOTICES ) {
1409     warn 'send_csv() disabled by $FS::Misc::DISABLE_ALL_NOTICES' if $DEBUG;
1410     return;
1411   }
1412
1413   #create file(s)
1414
1415   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1416   mkdir $spooldir, 0700 unless -d $spooldir;
1417
1418   # don't localize dates here, they're a defined format
1419   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1420   my $file = "$spooldir/$tracctnum.csv";
1421
1422   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1423
1424   open(CSV, ">$file") or die "can't open $file: $!";
1425   print CSV $header;
1426
1427   print CSV $detail;
1428
1429   close CSV;
1430
1431   my $net;
1432   if ( $opt{protocol} eq 'ftp' ) {
1433     eval "use Net::FTP;";
1434     die $@ if $@;
1435     $net = Net::FTP->new($opt{server}) or die @$;
1436   } else {
1437     die "unknown protocol: $opt{protocol}";
1438   }
1439
1440   $net->login( $opt{username}, $opt{password} )
1441     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1442
1443   $net->binary or die "can't set binary mode";
1444
1445   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1446
1447   $net->put($file) or die "can't put $file: $!";
1448
1449   $net->quit;
1450
1451   unlink $file;
1452
1453 }
1454
1455 =item spool_csv
1456
1457 Spools CSV invoice data.
1458
1459 Options are:
1460
1461 =over 4
1462
1463 =item format - any of FS::Misc::::Invoicing::spool_formats
1464
1465 =item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the
1466 customer has the corresponding invoice destinations set (see
1467 L<FS::cust_main_invoice>).
1468
1469 =item agent_spools - if set to a true value, will spool to per-agent files
1470 rather than a single global file
1471
1472 =item upload_targetnum - if set to a target (see L<FS::upload_target>), will
1473 append to that spool.  L<FS::Cron::upload> will then send the spool file to
1474 that destination.
1475
1476 =item balanceover - if set, only spools the invoice if the total amount owed on
1477 this invoice and all older invoices is greater than the specified amount.
1478
1479 =item time - the "current time".  Controls the printing of past due messages
1480 in the ICS format.
1481
1482 =back
1483
1484 =cut
1485
1486 sub spool_csv {
1487   my($self, %opt) = @_;
1488
1489   if ( $FS::Misc::DISABLE_ALL_NOTICES ) {
1490     warn 'spool_csv() disabled by $FS::Misc::DISABLE_ALL_NOTICES' if $DEBUG;
1491     return;
1492   }
1493
1494   my $time = $opt{'time'} || time;
1495   my $cust_main = $self->cust_main;
1496
1497   if ( $opt{'dest'} ) {
1498     my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1499                              $cust_main->invoicing_list;
1500     return 'N/A' unless $invoicing_list{$opt{'dest'}}
1501                      || ! keys %invoicing_list;
1502   }
1503
1504   if ( $opt{'balanceover'} ) {
1505     return 'N/A'
1506       if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1507   }
1508
1509   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1510   mkdir $spooldir, 0700 unless -d $spooldir;
1511
1512   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', $time);
1513
1514   my $file;
1515   if ( $opt{'agent_spools'} ) {
1516     $file = 'agentnum'.$cust_main->agentnum;
1517   } else {
1518     $file = 'spool';
1519   }
1520
1521   if ( $opt{'upload_targetnum'} ) {
1522     $spooldir .= '/target'.$opt{'upload_targetnum'};
1523     mkdir $spooldir, 0700 unless -d $spooldir;
1524   } # otherwise it just goes into export.xxx/cust_bill
1525
1526   if ( lc($opt{'format'}) eq 'billco' ) {
1527     $file .= '-header';
1528   }
1529
1530   $file = "$spooldir/$file.csv";
1531
1532   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum);
1533
1534   open(CSV, ">>$file") or die "can't open $file: $!";
1535   flock(CSV, LOCK_EX);
1536   seek(CSV, 0, 2);
1537
1538   print CSV $header;
1539
1540   if ( lc($opt{'format'}) eq 'billco' ) {
1541
1542     flock(CSV, LOCK_UN);
1543     close CSV;
1544
1545     $file =~ s/-header.csv$/-detail.csv/;
1546
1547     open(CSV,">>$file") or die "can't open $file: $!";
1548     flock(CSV, LOCK_EX);
1549     seek(CSV, 0, 2);
1550   }
1551
1552   print CSV $detail if defined($detail);
1553
1554   flock(CSV, LOCK_UN);
1555   close CSV;
1556
1557   return '';
1558
1559 }
1560
1561 =item print_csv OPTION => VALUE, ...
1562
1563 Returns CSV data for this invoice.
1564
1565 Options are:
1566
1567 format - 'default', 'billco', 'oneline', 'bridgestone'
1568
1569 Returns a list consisting of two scalars.  The first is a single line of CSV
1570 header information for this invoice.  The second is one or more lines of CSV
1571 detail information for this invoice.
1572
1573 If I<format> is not specified or "default", the fields of the CSV file are as
1574 follows:
1575
1576 record_type, invnum, custnum, _date, charged, first, last, company, address1,
1577 address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1578
1579 =over 4
1580
1581 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1582
1583 B<record_type> is C<cust_bill> for the initial header line only.  The
1584 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1585 fields are filled in.
1586
1587 B<record_type> is C<cust_bill_pkg> for detail lines.  Only the first two fields
1588 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1589 are filled in.
1590
1591 =item invnum - invoice number
1592
1593 =item custnum - customer number
1594
1595 =item _date - invoice date
1596
1597 =item charged - total invoice amount
1598
1599 =item first - customer first name
1600
1601 =item last - customer first name
1602
1603 =item company - company name
1604
1605 =item address1 - address line 1
1606
1607 =item address2 - address line 1
1608
1609 =item city
1610
1611 =item state
1612
1613 =item zip
1614
1615 =item country
1616
1617 =item pkg - line item description
1618
1619 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1620
1621 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1622
1623 =item sdate - start date for recurring fee
1624
1625 =item edate - end date for recurring fee
1626
1627 =back
1628
1629 If I<format> is "billco", the fields of the header CSV file are as follows:
1630
1631   +-------------------------------------------------------------------+
1632   |                        FORMAT HEADER FILE                         |
1633   |-------------------------------------------------------------------|
1634   | Field | Description                   | Name       | Type | Width |
1635   | 1     | N/A-Leave Empty               | RC         | CHAR |     2 |
1636   | 2     | N/A-Leave Empty               | CUSTID     | CHAR |    15 |
1637   | 3     | Transaction Account No        | TRACCTNUM  | CHAR |    15 |
1638   | 4     | Transaction Invoice No        | TRINVOICE  | CHAR |    15 |
1639   | 5     | Transaction Zip Code          | TRZIP      | CHAR |     5 |
1640   | 6     | Transaction Company Bill To   | TRCOMPANY  | CHAR |    30 |
1641   | 7     | Transaction Contact Bill To   | TRNAME     | CHAR |    30 |
1642   | 8     | Additional Address Unit Info  | TRADDR1    | CHAR |    30 |
1643   | 9     | Bill To Street Address        | TRADDR2    | CHAR |    30 |
1644   | 10    | Ancillary Billing Information | TRADDR3    | CHAR |    30 |
1645   | 11    | Transaction City Bill To      | TRCITY     | CHAR |    20 |
1646   | 12    | Transaction State Bill To     | TRSTATE    | CHAR |     2 |
1647   | 13    | Bill Cycle Close Date         | CLOSEDATE  | CHAR |    10 |
1648   | 14    | Bill Due Date                 | DUEDATE    | CHAR |    10 |
1649   | 15    | Previous Balance              | BALFWD     | NUM* |     9 |
1650   | 16    | Pmt/CR Applied                | CREDAPPLY  | NUM* |     9 |
1651   | 17    | Total Current Charges         | CURRENTCHG | NUM* |     9 |
1652   | 18    | Total Amt Due                 | TOTALDUE   | NUM* |     9 |
1653   | 19    | Total Amt Due                 | AMTDUE     | NUM* |     9 |
1654   | 20    | 30 Day Aging                  | AMT30      | NUM* |     9 |
1655   | 21    | 60 Day Aging                  | AMT60      | NUM* |     9 |
1656   | 22    | 90 Day Aging                  | AMT90      | NUM* |     9 |
1657   | 23    | Y/N                           | AGESWITCH  | CHAR |     1 |
1658   | 24    | Remittance automation         | SCANLINE   | CHAR |   100 |
1659   | 25    | Total Taxes & Fees            | TAXTOT     | NUM* |     9 |
1660   | 26    | Customer Reference Number     | CUSTREF    | CHAR |    15 |
1661   | 27    | Federal Tax***                | FEDTAX     | NUM* |     9 |
1662   | 28    | State Tax***                  | STATETAX   | NUM* |     9 |
1663   | 29    | Other Taxes & Fees***         | OTHERTAX   | NUM* |     9 |
1664   +-------+-------------------------------+------------+------+-------+
1665
1666 If I<format> is "billco", the fields of the detail CSV file are as follows:
1667
1668                                   FORMAT FOR DETAIL FILE
1669         |                            |           |      |
1670   Field | Description                | Name      | Type | Width
1671   1     | N/A-Leave Empty            | RC        | CHAR |     2
1672   2     | N/A-Leave Empty            | CUSTID    | CHAR |    15
1673   3     | Account Number             | TRACCTNUM | CHAR |    15
1674   4     | Invoice Number             | TRINVOICE | CHAR |    15
1675   5     | Line Sequence (sort order) | LINESEQ   | NUM  |     6
1676   6     | Transaction Detail         | DETAILS   | CHAR |   100
1677   7     | Amount                     | AMT       | NUM* |     9
1678   8     | Line Format Control**      | LNCTRL    | CHAR |     2
1679   9     | Grouping Code              | GROUP     | CHAR |     2
1680   10    | User Defined               | ACCT CODE | CHAR |    15
1681
1682 If format is 'oneline', there is no detail file.  Each invoice has a
1683 header line only, with the fields:
1684
1685 Agent number, agent name, customer number, first name, last name, address
1686 line 1, address line 2, city, state, zip, invoice date, invoice number,
1687 amount charged, amount due, previous balance, due date.
1688
1689 and then, for each line item, three columns containing the package number,
1690 description, and amount.
1691
1692 If format is 'bridgestone', there is no detail file.  Each invoice has a
1693 header line with the following fields in a fixed-width format:
1694
1695 Customer number (in display format), date, name (first last), company,
1696 address 1, address 2, city, state, zip.
1697
1698 This is a mailing list format, and has no per-invoice fields.  To avoid
1699 sending redundant notices, the spooling event should have a "once" or
1700 "once_percust_every" condition.
1701
1702 =cut
1703
1704 sub print_csv {
1705   my($self, %opt) = @_;
1706
1707   eval "use Text::CSV_XS";
1708   die $@ if $@;
1709
1710   my $cust_main = $self->cust_main;
1711
1712   my $csv = Text::CSV_XS->new({'always_quote'=>1});
1713   my $format = lc($opt{'format'});
1714
1715   my $time = $opt{'time'} || time;
1716
1717   $self->set('_template', $opt{template})
1718     if exists $opt{template};
1719
1720   my $tracctnum = ''; #leaking out from billco-specific sections :/
1721   if ( $format eq 'billco' ) {
1722
1723     my $account_num =
1724       $self->conf->config('billco-account_num', $cust_main->agentnum);
1725
1726     $tracctnum = $account_num eq 'display_custnum'
1727                    ? $cust_main->display_custnum
1728                    : $opt{'tracctnum'};
1729
1730     my $taxtotal = 0;
1731     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1732
1733     my $duedate = $self->due_date2str('%m/%d/%Y'); # hardcoded, NOT date_format
1734
1735     my( $previous_balance, @unused ) = $self->previous; #previous balance
1736
1737     my $pmt_cr_applied = 0;
1738     $pmt_cr_applied += $_->{'amount'}
1739       foreach ( $self->_items_payments(%opt), $self->_items_credits(%opt) ) ;
1740
1741     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1742
1743     $csv->combine(
1744       '',                         #  1 | N/A-Leave Empty               CHAR   2
1745       '',                         #  2 | N/A-Leave Empty               CHAR  15
1746       $tracctnum,                 #  3 | Transaction Account No        CHAR  15
1747       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
1748       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
1749       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
1750       #$cust_main->payname,        #  7 | Transaction Contact Bill To   CHAR  30
1751       $cust_main->contact,        #  7 | Transaction Contact Bill To   CHAR  30
1752       $cust_main->address2,       #  8 | Additional Address Unit Info  CHAR  30
1753       $cust_main->address1,       #  9 | Bill To Street Address        CHAR  30
1754       '',                         # 10 | Ancillary Billing Information CHAR  30
1755       $cust_main->city,           # 11 | Transaction City Bill To      CHAR  20
1756       $cust_main->state,          # 12 | Transaction State Bill To     CHAR   2
1757
1758       # XXX ?
1759       time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR  10
1760
1761       # XXX ?
1762       $duedate,                   # 14 | Bill Due Date                 CHAR  10
1763
1764       $previous_balance,          # 15 | Previous Balance              NUM*   9
1765       $pmt_cr_applied,            # 16 | Pmt/CR Applied                NUM*   9
1766       sprintf("%.2f", $self->charged), # 17 | Total Current Charges    NUM*   9
1767       $totaldue,                  # 18 | Total Amt Due                 NUM*   9
1768       $totaldue,                  # 19 | Total Amt Due                 NUM*   9
1769       '',                         # 20 | 30 Day Aging                  NUM*   9
1770       '',                         # 21 | 60 Day Aging                  NUM*   9
1771       '',                         # 22 | 90 Day Aging                  NUM*   9
1772       'N',                        # 23 | Y/N                           CHAR   1
1773       '',                         # 24 | Remittance automation         CHAR 100
1774       $taxtotal,                  # 25 | Total Taxes & Fees            NUM*   9
1775       $self->custnum,             # 26 | Customer Reference Number     CHAR  15
1776       '0',                        # 27 | Federal Tax***                NUM*   9
1777       sprintf("%.2f", $taxtotal), # 28 | State Tax***                  NUM*   9
1778       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
1779     );
1780
1781   } elsif ( $format eq 'oneline' ) { #name
1782
1783     my ($previous_balance) = $self->previous;
1784     $previous_balance = sprintf('%.2f', $previous_balance);
1785     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1786     my @items = map {
1787                       $_->{pkgnum},
1788                       $_->{description},
1789                       $_->{amount}
1790                     }
1791                   $self->_items_pkg, #_items_nontax?  no sections or anything
1792                                      # with this format
1793                   $self->_items_tax;
1794
1795     $csv->combine(
1796       $cust_main->agentnum,
1797       $cust_main->agent->agent,
1798       $self->custnum,
1799       $cust_main->first,
1800       $cust_main->last,
1801       $cust_main->company,
1802       $cust_main->address1,
1803       $cust_main->address2,
1804       $cust_main->city,
1805       $cust_main->state,
1806       $cust_main->zip,
1807
1808       # invoice fields
1809       time2str("%x", $self->_date),
1810       $self->invnum,
1811       $self->charged,
1812       $totaldue,
1813       $previous_balance,
1814       $self->due_date2str("%x"),
1815
1816       @items,
1817     );
1818
1819   } elsif ( $format eq 'bridgestone' ) {
1820
1821     # bypass the CSV stuff and just return this
1822     my $longdate = time2str('%B %d, %Y', $time); #current time, right?
1823     my $zip = $cust_main->zip;
1824     $zip =~ s/\D//;
1825     my $prefix = $self->conf->config('bridgestone-prefix', $cust_main->agentnum)
1826       || '';
1827     return (
1828       sprintf(
1829         "%-5s%-15s%-20s%-30s%-30s%-30s%-30s%-20s%-2s%-9s\n",
1830         $prefix,
1831         $cust_main->display_custnum,
1832         $longdate,
1833         uc(substr($cust_main->contact_firstlast,0,30)),
1834         uc(substr($cust_main->company          ,0,30)),
1835         uc(substr($cust_main->address1         ,0,30)),
1836         uc(substr($cust_main->address2         ,0,30)),
1837         uc(substr($cust_main->city             ,0,20)),
1838         uc($cust_main->state),
1839         $zip
1840       ),
1841       '' #detail
1842       );
1843
1844   } elsif ( $format eq 'ics' ) {
1845
1846     my $bill = $cust_main->bill_location;
1847     my $zip = $bill->zip;
1848     my $zip4 = '';
1849
1850     $zip =~ s/\D//;
1851     if ( $zip =~ /^(\d{5})(\d{4})$/ ) {
1852       $zip = $1;
1853       $zip4 = $2;
1854     }
1855
1856     # minor false laziness with print_generic
1857     my ($previous_balance) = $self->previous;
1858     my $balance_due = $self->owed + $previous_balance;
1859     my $payment_total = sum(0, map { $_->{'amount'} } $self->_items_payments);
1860     my $credit_total  = sum(0, map { $_->{'amount'} } $self->_items_credits);
1861
1862     my $past_due = '';
1863     if ( $self->due_date and $time >= $self->due_date ) {
1864       $past_due = sprintf('Past due:$%0.2f Due Immediately', $balance_due);
1865     }
1866
1867     # again, bypass CSV
1868     my $header = sprintf(
1869       '%-10s%-30s%-48s%-2s%-50s%-30s%-30s%-25s%-2s%-5s%-4s%-8s%-8s%-10s%-10s%-10s%-10s%-10s%-10s%-480s%-35s',
1870       $cust_main->display_custnum, #BID
1871       uc($cust_main->first), #FNAME
1872       uc($cust_main->last), #LNAME
1873       '00', #BATCH, should this ever be anything else?
1874       uc($cust_main->company), #COMP
1875       uc($bill->address1), #STREET1
1876       uc($bill->address2), #STREET2
1877       uc($bill->city), #CITY
1878       uc($bill->state), #STATE
1879       $zip,
1880       $zip4,
1881       time2str('%Y%m%d', $self->_date), #BILL_DATE
1882       $self->due_date2str('%Y%m%d'), #DUE_DATE,
1883       ( map {sprintf('%0.2f', $_)}
1884         $balance_due, #AMNT_DUE
1885         $previous_balance, #PREV_BAL
1886         $payment_total, #PYMT_RCVD
1887         $credit_total, #CREDITS
1888         $previous_balance, #BEG_BAL--is this correct?
1889         $self->charged, #NEW_CHRG
1890       ),
1891       'img01', #MRKT_MSG?
1892       $past_due, #PAST_MSG
1893     );
1894
1895     my @details;
1896     my %svc_class = ('' => ''); # maybe cache this more persistently?
1897
1898     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1899
1900       my $show_pkgnum = $cust_bill_pkg->pkgnum || '';
1901       my $cust_pkg = $cust_bill_pkg->cust_pkg if $show_pkgnum;
1902
1903       if ( $cust_pkg ) {
1904
1905         my @dates = ( $self->_date, undef );
1906         if ( my $prev = $cust_bill_pkg->previous_cust_bill_pkg ) {
1907           $dates[1] = $prev->sdate; #questionable
1908         }
1909
1910         # generate an 01 detail for each service
1911         my @svcs = $cust_pkg->h_cust_svc(@dates, 'I');
1912         foreach my $cust_svc ( @svcs ) {
1913           $show_pkgnum = ''; # hide it if we're showing svcnums
1914
1915           my $svcpart = $cust_svc->svcpart;
1916           if (!exists($svc_class{$svcpart})) {
1917             my $classnum = $cust_svc->part_svc->classnum;
1918             my $part_svc_class = FS::part_svc_class->by_key($classnum)
1919               if $classnum;
1920             $svc_class{$svcpart} = $part_svc_class ?
1921                                    $part_svc_class->classname :
1922                                    '';
1923           }
1924
1925           my @h_label = $cust_svc->label(@dates, 'I');
1926           push @details, sprintf('01%-9s%-20s%-47s',
1927             $cust_svc->svcnum,
1928             $svc_class{$svcpart},
1929             $h_label[1],
1930           );
1931         } #foreach $cust_svc
1932       } #if $cust_pkg
1933
1934       my $desc = $cust_bill_pkg->desc; # itemdesc or part_pkg.pkg
1935       if ($cust_bill_pkg->recur > 0) {
1936         $desc .= ' '.time2str('%d-%b-%Y', $cust_bill_pkg->sdate).' to '.
1937                      time2str('%d-%b-%Y', $cust_bill_pkg->edate - 86400);
1938       }
1939       push @details, sprintf('02%-6s%-60s%-10s',
1940         $show_pkgnum,
1941         $desc,
1942         sprintf('%0.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
1943       );
1944     } #foreach $cust_bill_pkg
1945
1946     # Tag this row so that we know whether this is one page (1), two pages
1947     # (2), # or "big" (B).  The tag will be stripped off before uploading.
1948     if ( scalar(@details) < 12 ) {
1949       push @details, '1';
1950     } elsif ( scalar(@details) < 58 ) {
1951       push @details, '2';
1952     } else {
1953       push @details, 'B';
1954     }
1955
1956     return join('', $header, @details, "\n");
1957
1958   } else { # default
1959
1960     $csv->combine(
1961       'cust_bill',
1962       $self->invnum,
1963       $self->custnum,
1964       time2str("%x", $self->_date),
1965       sprintf("%.2f", $self->charged),
1966       ( map { $cust_main->getfield($_) }
1967           qw( first last company address1 address2 city state zip country ) ),
1968       map { '' } (1..5),
1969     ) or die "can't create csv";
1970   }
1971
1972   my $header = $csv->string. "\n";
1973
1974   my $detail = '';
1975   if ( lc($opt{'format'}) eq 'billco' ) {
1976
1977     my $lineseq = 0;
1978     my %items_opt = ( format => 'template',
1979                       escape_function => sub { shift } );
1980     # I don't know what characters billco actually tolerates in spool entries.
1981     # Text::CSV will take care of delimiters, though.
1982
1983     my @items = ( $self->_items_pkg(%items_opt),
1984                   $self->_items_fee(%items_opt) );
1985     foreach my $item (@items) {
1986
1987       my $description = $item->{'description'};
1988       if ( $item->{'_is_discount'} and exists($item->{ext_description}[0]) ) {
1989         $description .= ': ' . $item->{ext_description}[0];
1990       }
1991
1992       $csv->combine(
1993         '',                     #  1 | N/A-Leave Empty            CHAR   2
1994         '',                     #  2 | N/A-Leave Empty            CHAR  15
1995         $tracctnum,             #  3 | Account Number             CHAR  15
1996         $self->invnum,          #  4 | Invoice Number             CHAR  15
1997         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
1998         $description,           #  6 | Transaction Detail         CHAR 100
1999         $item->{'amount'},      #  7 | Amount                     NUM*   9
2000         '',                     #  8 | Line Format Control**      CHAR   2
2001         '',                     #  9 | Grouping Code              CHAR   2
2002         '',                     # 10 | User Defined               CHAR  15
2003       );
2004
2005       $detail .= $csv->string. "\n";
2006
2007     }
2008
2009   } elsif ( lc($opt{'format'}) eq 'oneline' ) {
2010
2011     #do nothing
2012
2013   } else {
2014
2015     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2016
2017       my($pkg, $setup, $recur, $sdate, $edate);
2018       if ( $cust_bill_pkg->pkgnum ) {
2019
2020         ($pkg, $setup, $recur, $sdate, $edate) = (
2021           $cust_bill_pkg->part_pkg->pkg,
2022           ( $cust_bill_pkg->setup != 0
2023             ? sprintf("%.2f", $cust_bill_pkg->setup )
2024             : '' ),
2025           ( $cust_bill_pkg->recur != 0
2026             ? sprintf("%.2f", $cust_bill_pkg->recur )
2027             : '' ),
2028           ( $cust_bill_pkg->sdate
2029             ? time2str("%x", $cust_bill_pkg->sdate)
2030             : '' ),
2031           ($cust_bill_pkg->edate
2032             ? time2str("%x", $cust_bill_pkg->edate)
2033             : '' ),
2034         );
2035
2036       } else { #pkgnum tax
2037         next unless $cust_bill_pkg->setup != 0;
2038         $pkg = $cust_bill_pkg->desc;
2039         $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
2040         ( $sdate, $edate ) = ( '', '' );
2041       }
2042
2043       $csv->combine(
2044         'cust_bill_pkg',
2045         $self->invnum,
2046         ( map { '' } (1..11) ),
2047         ($pkg, $setup, $recur, $sdate, $edate)
2048       ) or die "can't create csv";
2049
2050       $detail .= $csv->string. "\n";
2051
2052     }
2053
2054   }
2055
2056   ( $header, $detail );
2057
2058 }
2059
2060 sub comp {
2061   croak 'cust_bill->comp is deprecated (COMP payments are deprecated)';
2062 }
2063
2064 =item realtime_card
2065
2066 Attempts to pay this invoice with a credit card payment via a
2067 Business::OnlinePayment realtime gateway.  See
2068 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2069 for supported processors.
2070
2071 =cut
2072
2073 sub realtime_card {
2074   my $self = shift;
2075   $self->realtime_bop( 'CC', @_ );
2076 }
2077
2078 =item realtime_ach
2079
2080 Attempts to pay this invoice with an electronic check (ACH) payment via a
2081 Business::OnlinePayment realtime gateway.  See
2082 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2083 for supported processors.
2084
2085 =cut
2086
2087 sub realtime_ach {
2088   my $self = shift;
2089   $self->realtime_bop( 'ECHECK', @_ );
2090 }
2091
2092 =item realtime_lec
2093
2094 Attempts to pay this invoice with phone bill (LEC) payment via a
2095 Business::OnlinePayment realtime gateway.  See
2096 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2097 for supported processors.
2098
2099 =cut
2100
2101 sub realtime_lec {
2102   my $self = shift;
2103   $self->realtime_bop( 'LEC', @_ );
2104 }
2105
2106 sub realtime_bop {
2107   my( $self, $method ) = (shift,shift);
2108   my $conf = $self->conf;
2109   my %opt = @_;
2110
2111   my $cust_main = $self->cust_main;
2112   my $balance = $cust_main->balance;
2113   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2114   $amount = sprintf("%.2f", $amount);
2115   return "not run (balance $balance)" unless $amount > 0;
2116
2117   my $description = 'Internet Services';
2118   if ( $conf->exists('business-onlinepayment-description') ) {
2119     my $dtempl = $conf->config('business-onlinepayment-description');
2120
2121     my $agent_obj = $cust_main->agent
2122       or die "can't retreive agent for $cust_main (agentnum ".
2123              $cust_main->agentnum. ")";
2124     my $agent = $agent_obj->agent;
2125     my $pkgs = join(', ',
2126       map { $_->part_pkg->pkg }
2127         grep { $_->pkgnum } $self->cust_bill_pkg
2128     );
2129     $description = eval qq("$dtempl");
2130   }
2131
2132   $cust_main->realtime_bop($method, $amount,
2133     'description' => $description,
2134     'invnum'      => $self->invnum,
2135 #this didn't do what we want, it just calls apply_payments_and_credits
2136 #    'apply'       => 1,
2137     'apply_to_invoice' => 1,
2138     %opt,
2139  #what we want:
2140  #this changes application behavior: auto payments
2141                         #triggered against a specific invoice are now applied
2142                         #to that invoice instead of oldest open.
2143                         #seem okay to me...
2144   );
2145
2146 }
2147
2148 =item batch_card OPTION => VALUE...
2149
2150 Adds a payment for this invoice to the pending credit card batch (see
2151 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2152 runs the payment using a realtime gateway.
2153
2154 =cut
2155
2156 sub batch_card {
2157   my ($self, %options) = @_;
2158   my $cust_main = $self->cust_main;
2159
2160   $options{invnum} = $self->invnum;
2161
2162   $cust_main->batch_card(%options);
2163 }
2164
2165 sub _agent_template {
2166   my $self = shift;
2167   $self->cust_main->agent_template;
2168 }
2169
2170 sub _agent_invoice_from {
2171   my $self = shift;
2172   $self->cust_main->agent_invoice_from;
2173 }
2174
2175 =item invoice_barcode DIR_OR_FALSE
2176
2177 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2178 it is taken as the temp directory where the PNG file will be generated and the
2179 PNG file name is returned. Otherwise, the PNG image itself is returned.
2180
2181 =cut
2182
2183 sub invoice_barcode {
2184     my ($self, $dir) = (shift,shift);
2185
2186     my $gdbar = new GD::Barcode('Code39',$self->invnum);
2187         die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2188     my $gd = $gdbar->plot(Height => 30);
2189
2190     if($dir) {
2191         my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2192                            DIR      => $dir,
2193                            SUFFIX   => '.png',
2194                            UNLINK   => 0,
2195                          ) or die "can't open temp file: $!\n";
2196         print $bh $gd->png or die "cannot write barcode to file: $!\n";
2197         my $png_file = $bh->filename;
2198         close $bh;
2199         return $png_file;
2200     }
2201     return $gd->png;
2202 }
2203
2204 =item invnum_date_pretty
2205
2206 Returns a string with the invoice number and date, for example:
2207 "Invoice #54 (3/20/2008)".
2208
2209 Intended for back-end context, with regard to translation and date formatting.
2210
2211 =cut
2212
2213 #note: this uses _date_pretty_unlocalized because _date_pretty is too expensive
2214 # for backend use (and also does the wrong thing, localizing for end customer
2215 # instead of backoffice configured date format)
2216 sub invnum_date_pretty {
2217   my $self = shift;
2218   #$self->mt('Invoice #').
2219   'Invoice #'. #XXX should be translated ala web UI user (not invoice customer)
2220     $self->invnum. ' ('. $self->_date_pretty_unlocalized. ')';
2221 }
2222
2223 #sub _items_extra_usage_sections {
2224 #  my $self = shift;
2225 #  my $escape = shift;
2226 #
2227 #  my %sections = ();
2228 #
2229 #  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
2230 #  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2231 #  {
2232 #    next unless $cust_bill_pkg->pkgnum > 0;
2233 #
2234 #    foreach my $section ( keys %usage_class ) {
2235 #
2236 #      my $usage = $cust_bill_pkg->usage($section);
2237 #
2238 #      next unless $usage && $usage > 0;
2239 #
2240 #      $sections{$section} ||= 0;
2241 #      $sections{$section} += $usage;
2242 #
2243 #    }
2244 #
2245 #  }
2246 #
2247 #  map { { 'description' => &{$escape}($_),
2248 #          'subtotal'    => $sections{$_},
2249 #          'summarized'  => '',
2250 #          'tax_section' => '',
2251 #        }
2252 #      }
2253 #    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
2254 #
2255 #}
2256
2257 sub _items_extra_usage_sections {
2258   my $self = shift;
2259   my $conf = $self->conf;
2260   my $escape = shift;
2261   my $format = shift;
2262
2263   my %sections = ();
2264   my %classnums = ();
2265   my %lines = ();
2266
2267   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
2268
2269   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
2270   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2271     next unless $cust_bill_pkg->pkgnum > 0;
2272
2273     foreach my $classnum ( keys %usage_class ) {
2274       my $section = $usage_class{$classnum}->classname;
2275       $classnums{$section} = $classnum;
2276
2277       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
2278         my $amount = $detail->amount;
2279         next unless $amount && $amount > 0;
2280
2281         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
2282         $sections{$section}{amount} += $amount;  #subtotal
2283         $sections{$section}{calls}++;
2284         $sections{$section}{duration} += $detail->duration;
2285
2286         my $desc = $detail->regionname;
2287         my $description = $desc;
2288         $description = substr($desc, 0, $maxlength). '...'
2289           if $format eq 'latex' && length($desc) > $maxlength;
2290
2291         $lines{$section}{$desc} ||= {
2292           description     => &{$escape}($description),
2293           #pkgpart         => $part_pkg->pkgpart,
2294           pkgnum          => $cust_bill_pkg->pkgnum,
2295           ref             => '',
2296           amount          => 0,
2297           calls           => 0,
2298           duration        => 0,
2299           #unit_amount     => $cust_bill_pkg->unitrecur,
2300           quantity        => $cust_bill_pkg->quantity,
2301           product_code    => 'N/A',
2302           ext_description => [],
2303         };
2304
2305         $lines{$section}{$desc}{amount} += $amount;
2306         $lines{$section}{$desc}{calls}++;
2307         $lines{$section}{$desc}{duration} += $detail->duration;
2308
2309       }
2310     }
2311   }
2312
2313   my %sectionmap = ();
2314   foreach (keys %sections) {
2315     my $usage_class = $usage_class{$classnums{$_}};
2316     $sectionmap{$_} = { 'description' => &{$escape}($_),
2317                         'amount'    => $sections{$_}{amount},    #subtotal
2318                         'calls'       => $sections{$_}{calls},
2319                         'duration'    => $sections{$_}{duration},
2320                         'summarized'  => '',
2321                         'tax_section' => '',
2322                         'sort_weight' => $usage_class->weight,
2323                         ( $usage_class->format
2324                           ? ( map { $_ => $usage_class->$_($format) }
2325                               qw( description_generator header_generator total_generator total_line_generator )
2326                             )
2327                           : ()
2328                         ),
2329                       };
2330   }
2331
2332   my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
2333                  values %sectionmap;
2334
2335   my @lines = ();
2336   foreach my $section ( keys %lines ) {
2337     foreach my $line ( keys %{$lines{$section}} ) {
2338       my $l = $lines{$section}{$line};
2339       $l->{section}     = $sectionmap{$section};
2340       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
2341       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
2342       push @lines, $l;
2343     }
2344   }
2345
2346   return(\@sections, \@lines);
2347
2348 }
2349
2350 sub _did_summary {
2351     my $self = shift;
2352     my $end = $self->_date;
2353
2354     # start at date of previous invoice + 1 second or 0 if no previous invoice
2355     my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
2356     $start = 0 if !$start;
2357     $start++;
2358
2359     my $cust_main = $self->cust_main;
2360     my @pkgs = $cust_main->all_pkgs;
2361     my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
2362         = (0,0,0,0,0);
2363     my @seen = ();
2364     foreach my $pkg ( @pkgs ) {
2365         my @h_cust_svc = $pkg->h_cust_svc($end);
2366         foreach my $h_cust_svc ( @h_cust_svc ) {
2367             next if grep {$_ eq $h_cust_svc->svcnum} @seen;
2368             next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
2369
2370             my $inserted = $h_cust_svc->date_inserted;
2371             my $deleted = $h_cust_svc->date_deleted;
2372             my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
2373             my $phone_deleted;
2374             $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
2375
2376 # DID either activated or ported in; cannot be both for same DID simultaneously
2377             if ($inserted >= $start && $inserted <= $end && $phone_inserted
2378                 && (!$phone_inserted->lnp_status
2379                     || $phone_inserted->lnp_status eq ''
2380                     || $phone_inserted->lnp_status eq 'native')) {
2381                 $num_activated++;
2382             }
2383             else { # this one not so clean, should probably move to (h_)svc_phone
2384                  local($FS::Record::qsearch_qualify_columns) = 0;
2385                  my $phone_portedin = qsearchs( 'h_svc_phone',
2386                       { 'svcnum' => $h_cust_svc->svcnum,
2387                         'lnp_status' => 'portedin' },
2388                       FS::h_svc_phone->sql_h_searchs($end),
2389                     );
2390                  $num_portedin++ if $phone_portedin;
2391             }
2392
2393 # DID either deactivated or ported out; cannot be both for same DID simultaneously
2394             if($deleted >= $start && $deleted <= $end && $phone_deleted
2395                 && (!$phone_deleted->lnp_status
2396                     || $phone_deleted->lnp_status ne 'portingout')) {
2397                 $num_deactivated++;
2398             }
2399             elsif($deleted >= $start && $deleted <= $end && $phone_deleted
2400                 && $phone_deleted->lnp_status
2401                 && $phone_deleted->lnp_status eq 'portingout') {
2402                 $num_portedout++;
2403             }
2404
2405             # increment usage minutes
2406         if ( $phone_inserted ) {
2407             my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
2408             $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
2409         }
2410         else {
2411             warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
2412         }
2413
2414             # don't look at this service again
2415             push @seen, $h_cust_svc->svcnum;
2416         }
2417     }
2418
2419     $minutes = sprintf("%d", $minutes);
2420     ("Activated: $num_activated  Ported-In: $num_portedin  Deactivated: "
2421         . "$num_deactivated  Ported-Out: $num_portedout ",
2422             "Total Minutes: $minutes");
2423 }
2424
2425 sub _items_accountcode_cdr {
2426     my $self = shift;
2427     my $escape = shift;
2428     my $format = shift;
2429
2430     my $section = { 'amount'        => 0,
2431                     'calls'         => 0,
2432                     'duration'      => 0,
2433                     'sort_weight'   => '',
2434                     'phonenum'      => '',
2435                     'description'   => 'Usage by Account Code',
2436                     'post_total'    => '',
2437                     'summarized'    => '',
2438                     'header'        => '',
2439                   };
2440     my @lines;
2441     my %accountcodes = ();
2442
2443     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2444         next unless $cust_bill_pkg->pkgnum > 0;
2445
2446         my @header = $cust_bill_pkg->details_header;
2447         next unless scalar(@header);
2448         $section->{'header'} = join(',',@header);
2449
2450         foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
2451
2452             $section->{'header'} = $detail->formatted('format' => $format)
2453                 if($detail->detail eq $section->{'header'});
2454
2455             my $accountcode = $detail->accountcode;
2456             next unless $accountcode;
2457
2458             my $amount = $detail->amount;
2459             next unless $amount && $amount > 0;
2460
2461             $accountcodes{$accountcode} ||= {
2462                     description => $accountcode,
2463                     pkgnum      => '',
2464                     ref         => '',
2465                     amount      => 0,
2466                     calls       => 0,
2467                     duration    => 0,
2468                     quantity    => '',
2469                     product_code => 'N/A',
2470                     section     => $section,
2471                     ext_description => [ $section->{'header'} ],
2472                     detail_temp => [],
2473             };
2474
2475             $section->{'amount'} += $amount;
2476             $accountcodes{$accountcode}{'amount'} += $amount;
2477             $accountcodes{$accountcode}{calls}++;
2478             $accountcodes{$accountcode}{duration} += $detail->duration;
2479             push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
2480         }
2481     }
2482
2483     foreach my $l ( values %accountcodes ) {
2484         $l->{amount} = sprintf( "%.2f", $l->{amount} );
2485         my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
2486         foreach my $sorted_detail ( @sorted_detail ) {
2487             push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
2488         }
2489         delete $l->{detail_temp};
2490         push @lines, $l;
2491     }
2492
2493     my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
2494
2495     return ($section,\@sorted_lines);
2496 }
2497
2498 sub _items_svc_phone_sections {
2499   my $self = shift;
2500   my $conf = $self->conf;
2501   my $escape = shift;
2502   my $format = shift;
2503
2504   my %sections = ();
2505   my %classnums = ();
2506   my %lines = ();
2507
2508   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 40;
2509
2510   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
2511   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
2512
2513   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2514     next unless $cust_bill_pkg->pkgnum > 0;
2515
2516     my @header = $cust_bill_pkg->details_header;
2517     next unless scalar(@header);
2518
2519     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
2520
2521       my $phonenum = $detail->phonenum;
2522       next unless $phonenum;
2523
2524       my $amount = $detail->amount;
2525       next unless $amount && $amount > 0;
2526
2527       $sections{$phonenum} ||= { 'amount'      => 0,
2528                                  'calls'       => 0,
2529                                  'duration'    => 0,
2530                                  'sort_weight' => -1,
2531                                  'phonenum'    => $phonenum,
2532                                 };
2533       $sections{$phonenum}{amount} += $amount;  #subtotal
2534       $sections{$phonenum}{calls}++;
2535       $sections{$phonenum}{duration} += $detail->duration;
2536
2537       my $desc = $detail->regionname;
2538       my $description = $desc;
2539       $description = substr($desc, 0, $maxlength). '...'
2540         if $format eq 'latex' && length($desc) > $maxlength;
2541
2542       $lines{$phonenum}{$desc} ||= {
2543         description     => &{$escape}($description),
2544         #pkgpart         => $part_pkg->pkgpart,
2545         pkgnum          => '',
2546         ref             => '',
2547         amount          => 0,
2548         calls           => 0,
2549         duration        => 0,
2550         #unit_amount     => '',
2551         quantity        => '',
2552         product_code    => 'N/A',
2553         ext_description => [],
2554       };
2555
2556       $lines{$phonenum}{$desc}{amount} += $amount;
2557       $lines{$phonenum}{$desc}{calls}++;
2558       $lines{$phonenum}{$desc}{duration} += $detail->duration;
2559
2560       my $line = $usage_class{$detail->classnum}->classname;
2561       $sections{"$phonenum $line"} ||=
2562         { 'amount' => 0,
2563           'calls' => 0,
2564           'duration' => 0,
2565           'sort_weight' => $usage_class{$detail->classnum}->weight,
2566           'phonenum' => $phonenum,
2567           'header'  => [ @header ],
2568         };
2569       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
2570       $sections{"$phonenum $line"}{calls}++;
2571       $sections{"$phonenum $line"}{duration} += $detail->duration;
2572
2573       $lines{"$phonenum $line"}{$desc} ||= {
2574         description     => &{$escape}($description),
2575         #pkgpart         => $part_pkg->pkgpart,
2576         pkgnum          => '',
2577         ref             => '',
2578         amount          => 0,
2579         calls           => 0,
2580         duration        => 0,
2581         #unit_amount     => '',
2582         quantity        => '',
2583         product_code    => 'N/A',
2584         ext_description => [],
2585       };
2586
2587       $lines{"$phonenum $line"}{$desc}{amount} += $amount;
2588       $lines{"$phonenum $line"}{$desc}{calls}++;
2589       $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
2590       push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
2591            $detail->formatted('format' => $format);
2592
2593     }
2594   }
2595
2596   my %sectionmap = ();
2597   my $simple = new FS::usage_class { format => 'simple' }; #bleh
2598   foreach ( keys %sections ) {
2599     my @header = @{ $sections{$_}{header} || [] };
2600     my $usage_simple =
2601       new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
2602     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
2603     my $usage_class = $summary ? $simple : $usage_simple;
2604     my $ending = $summary ? ' usage charges' : '';
2605     my %gen_opt = ();
2606     unless ($summary) {
2607       $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
2608     }
2609     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
2610                         'amount'    => $sections{$_}{amount},    #subtotal
2611                         'calls'       => $sections{$_}{calls},
2612                         'duration'    => $sections{$_}{duration},
2613                         'summarized'  => '',
2614                         'tax_section' => '',
2615                         'phonenum'    => $sections{$_}{phonenum},
2616                         'sort_weight' => $sections{$_}{sort_weight},
2617                         'post_total'  => $summary, #inspire pagebreak
2618                         (
2619                           ( map { $_ => $usage_class->$_($format, %gen_opt) }
2620                             qw( description_generator
2621                                 header_generator
2622                                 total_generator
2623                                 total_line_generator
2624                               )
2625                           )
2626                         ),
2627                       };
2628   }
2629
2630   my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
2631                         $a->{sort_weight} <=> $b->{sort_weight}
2632                       }
2633                  values %sectionmap;
2634
2635   my @lines = ();
2636   foreach my $section ( keys %lines ) {
2637     foreach my $line ( keys %{$lines{$section}} ) {
2638       my $l = $lines{$section}{$line};
2639       $l->{section}     = $sectionmap{$section};
2640       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
2641       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
2642       push @lines, $l;
2643     }
2644   }
2645
2646   if($conf->exists('phone_usage_class_summary')) {
2647       # this only works with Latex
2648       my @newlines;
2649       my @newsections;
2650
2651       # after this, we'll have only two sections per DID:
2652       # Calls Summary and Calls Detail
2653       foreach my $section ( @sections ) {
2654         if($section->{'post_total'}) {
2655             $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
2656             $section->{'total_line_generator'} = sub { '' };
2657             $section->{'total_generator'} = sub { '' };
2658             $section->{'header_generator'} = sub { '' };
2659             $section->{'description_generator'} = '';
2660             push @newsections, $section;
2661             my %calls_detail = %$section;
2662             $calls_detail{'post_total'} = '';
2663             $calls_detail{'sort_weight'} = '';
2664             $calls_detail{'description_generator'} = sub { '' };
2665             $calls_detail{'header_generator'} = sub {
2666                 return ' & Date/Time & Called Number & Duration & Price'
2667                     if $format eq 'latex';
2668                 '';
2669             };
2670             $calls_detail{'description'} = 'Calls Detail: '
2671                                                     . $section->{'phonenum'};
2672             push @newsections, \%calls_detail;
2673         }
2674       }
2675
2676       # after this, each usage class is collapsed/summarized into a single
2677       # line under the Calls Summary section
2678       foreach my $newsection ( @newsections ) {
2679         if($newsection->{'post_total'}) { # this means Calls Summary
2680             foreach my $section ( @sections ) {
2681                 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'}
2682                                 && !$section->{'post_total'});
2683                 my $newdesc = $section->{'description'};
2684                 my $tn = $section->{'phonenum'};
2685                 $newdesc =~ s/$tn//g;
2686                 my $line = {  ext_description => [],
2687                               pkgnum => '',
2688                               ref => '',
2689                               quantity => '',
2690                               calls => $section->{'calls'},
2691                               section => $newsection,
2692                               duration => $section->{'duration'},
2693                               description => $newdesc,
2694                               amount => sprintf("%.2f",$section->{'amount'}),
2695                               product_code => 'N/A',
2696                             };
2697                 push @newlines, $line;
2698             }
2699         }
2700       }
2701
2702       # after this, Calls Details is populated with all CDRs
2703       foreach my $newsection ( @newsections ) {
2704         if(!$newsection->{'post_total'}) { # this means Calls Details
2705             foreach my $line ( @lines ) {
2706                 next unless (scalar(@{$line->{'ext_description'}}) &&
2707                         $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
2708                             );
2709                 my @extdesc = @{$line->{'ext_description'}};
2710                 my @newextdesc;
2711                 foreach my $extdesc ( @extdesc ) {
2712                     $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
2713                     push @newextdesc, $extdesc;
2714                 }
2715                 $line->{'ext_description'} = \@newextdesc;
2716                 $line->{'section'} = $newsection;
2717                 push @newlines, $line;
2718             }
2719         }
2720       }
2721
2722       return(\@newsections, \@newlines);
2723   }
2724
2725   return(\@sections, \@lines);
2726
2727 }
2728
2729 =item _items_usage_class_summary OPTIONS
2730
2731 Returns a list of detail items summarizing the usage charges on this
2732 invoice.  Each one will have 'amount', 'description' (the usage charge name),
2733 and 'usage_classnum'.
2734
2735 OPTIONS can include 'escape' (a function to escape the descriptions).
2736
2737 =cut
2738
2739 sub _items_usage_class_summary {
2740   my $self = shift;
2741   my %opt = @_;
2742
2743   my $escape = $opt{escape} || sub { $_[0] };
2744   my $money_char = $opt{money_char};
2745   my $invnum = $self->invnum;
2746   my @classes = qsearch({
2747       'table'     => 'usage_class',
2748       'select'    => 'classnum, classname, SUM(amount) AS amount,'.
2749                      ' COUNT(*) AS calls, SUM(duration) AS duration',
2750       'addl_from' => ' LEFT JOIN cust_bill_pkg_detail USING (classnum)' .
2751                      ' LEFT JOIN cust_bill_pkg USING (billpkgnum)',
2752       'extra_sql' => " WHERE cust_bill_pkg.invnum = $invnum".
2753                      ' GROUP BY classnum, classname, weight'.
2754                      ' HAVING (usage_class.disabled IS NULL OR SUM(amount) > 0)'.
2755                      ' ORDER BY weight ASC',
2756   });
2757   my @l;
2758   my $section = {
2759     description   => &{$escape}($self->mt('Usage Summary')),
2760     usage_section => 1,
2761     subtotal      => 0,
2762   };
2763   foreach my $class (@classes) {
2764     $section->{subtotal} += $class->get('amount');
2765     push @l, {
2766       'description'     => &{$escape}($class->classname),
2767       'amount'          => $money_char.sprintf('%.2f', $class->get('amount')),
2768       'quantity'        => $class->get('calls'),
2769       'duration'        => $class->get('duration'),
2770       'usage_classnum'  => $class->classnum,
2771       'section'         => $section,
2772     };
2773   }
2774   $section->{subtotal} = $money_char.sprintf('%.2f', $section->{subtotal});
2775   return @l;
2776 }
2777
2778 =item _items_previous()
2779
2780   Returns an array of hashrefs, each hashref representing a line-item on
2781   the current bill for previous unpaid invoices.
2782
2783   keys for each previous_item:
2784   - amount (see notes)
2785   - pkgnum
2786   - description
2787   - invnum
2788   - _date
2789
2790   Payments and credits shown on this invoice may vary based on configuraiton.
2791
2792   when conf flag previous_balance-payments_since is set:
2793     This method works backwards to rebuild the invoice as a snapshot in time.
2794     The invoice displayed will have the balances owed, and payments made,
2795     reflecting the state of the account at the time of invoice generation.
2796
2797 =cut
2798
2799 sub _items_previous {
2800
2801   my $self = shift;
2802
2803   # simple memoize
2804   if ($self->get('_items_previous')) {
2805     return sort { $a->{_date} <=> $b->{_date} }
2806          values %{ $self->get('_items_previous') };
2807   }
2808
2809   # Gets the customer's current balance and outstanding invoices.
2810   my ($prev_balance, @open_invoices) = $self->previous;
2811
2812   my %invoices = map {
2813     $_->invnum => $self->__items_previous_map_invoice($_)
2814   } @open_invoices;
2815
2816   # Which credits and payments displayed on the bill will vary based on
2817   # conf flag previous_balance-payments_since.
2818   my @credits  = $self->_items_credits();
2819   my @payments = $self->_items_payments();
2820
2821
2822   if ($self->conf->exists('previous_balance-payments_since')) {
2823     # For each credit or payment, determine which invoices it was applied to.
2824     # Manipulate data displayed so the invoice displayed appears as a
2825     # snapshot in time... with previous balances and balance owed displayed
2826     # as they were at the time of invoice creation.
2827
2828     my @credits_postbill = $self->_items_credits_postbill();
2829     my @payments_postbill = $self->_items_payments_postbill();
2830
2831     my %pmnt_dupechk;
2832     my %cred_dupechk;
2833
2834     # Each section below follows this pattern on a payment/credit
2835     #
2836     # - Dupe check, avoid adjusting for the same item twice
2837     # - If invoice being adjusted for isn't in our list, add it
2838     # - Adjust the invoice balance to refelct balnace without the
2839     #   credit or payment applied
2840     #
2841
2842     # Working with payments displayed on this bill
2843     for my $pmt_hash (@payments) {
2844       my $pmt_obj = qsearchs('cust_pay', {paynum => $pmt_hash->{paynum}});
2845       for my $cust_bill_pay ($pmt_obj->cust_bill_pay) {
2846         next if exists $pmnt_dupechk{$cust_bill_pay->billpaynum};
2847         $pmnt_dupechk{$cust_bill_pay->billpaynum} = 1;
2848
2849         my $invnum = $cust_bill_pay->invnum;
2850
2851         $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
2852           unless exists $invoices{$invnum};
2853
2854         $invoices{$invnum}->{amount} += $cust_bill_pay->amount;
2855       }
2856     }
2857
2858     # Working with credits displayed on this bill
2859     for my $cred_hash (@credits) {
2860       my $cred_obj = qsearchs('cust_credit', {crednum => $cred_hash->{crednum}});
2861       for my $cust_credit_bill ($cred_obj->cust_credit_bill) {
2862         next if exists $cred_dupechk{$cust_credit_bill->creditbillnum};
2863         $cred_dupechk{$cust_credit_bill->creditbillnum} = 1;
2864
2865         my $invnum = $cust_credit_bill->invnum;
2866
2867         $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
2868           unless exists $invoices{$invnum};
2869
2870         $invoices{$invnum}->{amount} += $cust_credit_bill->amount;
2871       }
2872     }
2873
2874     # Working with both credits and payments which are not displayed
2875     # on this bill, but which have affected this bill's balances
2876     for my $postbill (@payments_postbill, @credits_postbill) {
2877
2878       if ($postbill->{billpaynum}) {
2879         next if exists $pmnt_dupechk{$postbill->{billpaynum}};
2880         $pmnt_dupechk{$postbill->{billpaynum}} = 1;
2881       } elsif ($postbill->{creditbillnum}) {
2882         next if exists $cred_dupechk{$postbill->{creditbillnum}};
2883         $cred_dupechk{$postbill->{creditbillnum}} = 1;
2884       } else {
2885         die "Missing creditbillnum or billpaynum";
2886       }
2887
2888       my $invnum = $postbill->{invnum};
2889
2890       $invoices{$invnum} = $self->__items_previous_get_invoice($invnum)
2891         unless exists $invoices{$invnum};
2892
2893       $invoices{$invnum}->{amount} += $postbill->{amount};
2894     }
2895
2896     # Make sure current invoice doesn't appear in previous items
2897     delete $invoices{$self->invnum}
2898       if exists $invoices{$self->invnum};
2899
2900   }
2901
2902   # Make sure amount is formatted as a dollar string
2903   # (Formatting should happen on the template side, but is not?)
2904   $invoices{$_}->{amount} = sprintf('%.2f',$invoices{$_}->{amount})
2905     for keys %invoices;
2906
2907   $self->set('_items_previous', \%invoices);
2908   return sort { $a->{_date} <=> $b->{_date} } values %invoices;
2909
2910 }
2911
2912 =item _items_previous_total
2913
2914   Return sum of amounts from all items returned by _items_previous
2915   Results will vary based on invoicing conf flags
2916
2917 =cut
2918
2919 sub _items_previous_total {
2920   my $self = shift;
2921   my $tot = 0;
2922   $tot += $_->{amount} for $self->_items_previous();
2923   return $tot;
2924 }
2925
2926 sub __items_previous_get_invoice {
2927   # Helper function for _items_previous
2928   #
2929   # Read a record from cust_bill, return a hash of it's information
2930   my ($self, $invnum) = @_;
2931   die "Incorrect usage of __items_previous_get_invoice()" unless $invnum;
2932
2933   my $cust_bill = qsearchs('cust_bill', {invnum => $invnum});
2934   return $self->__items_previous_map_invoice($cust_bill);
2935 }
2936
2937 sub __items_previous_map_invoice {
2938   # Helper function for _items_previous
2939   #
2940   # Transform a cust_bill object into a simple hash reference of the type
2941   # required by _items_previous
2942   my ($self, $cust_bill) = @_;
2943   die "Incorrect usage of __items_previous_map_invoice" unless ref $cust_bill;
2944
2945   my $date = $self->conf->exists('invoice_show_prior_due_date')
2946            ? 'due '.$cust_bill->due_date2str('short')
2947            : $self->time2str_local('short', $cust_bill->_date);
2948
2949   return {
2950     invnum => $cust_bill->invnum,
2951     amount => $cust_bill->owed,
2952     pkgnum => 'N/A',
2953     _date  => $cust_bill->_date,
2954     description => join(' ',
2955       $self->mt('Previous Balance, Invoice #'),
2956       $cust_bill->invnum,
2957       "($date)"
2958     ),
2959   }
2960 }
2961
2962 =item _items_credits()
2963
2964   Return array of hashrefs containing credits to be shown as line-items
2965   when rendering this bill.
2966
2967   keys for each credit item:
2968   - crednum: id of payment
2969   - amount: payment amount
2970   - description: line item to be displayed on the bill
2971
2972   This method has three ways it selects which credits to display on
2973   this bill:
2974
2975   1) Default Case: No Conf flag for 'previous_balance-payments_since'
2976
2977      Returns credits that have been applied to this bill only
2978
2979   2) Case:
2980        Conf flag set for 'previous_balance-payments_since'
2981
2982      List all credits that have been recorded during the time period
2983      between the timestamps of the last invoice and this invoice
2984
2985   3) Case:
2986         Conf flag set for 'previous_balance-payments_since'
2987         $opt{'template'} eq 'statement'
2988
2989     List all payments that have been recorded between the timestamps
2990     of the previous invoice and the following invoice.
2991
2992      This is used to give the customer a receipt for a payment
2993      in the form of their last bill with the payment amended.
2994
2995      I am concerned with this implementation, but leaving in place as is
2996      If this option is selected, while viewing an older bill, the old bill
2997      will show ALL future credits for future bills, but no charges for
2998      future bills.  Somebody could be misled into believing they have a
2999      large account credit when they don't.  Also, interrupts the chain of
3000      invoices as an account history... the customer could have two invoices
3001      in their fileing cabinet, for two different dates, both with a line item
3002      for the same duplicate credit.  The accounting is technically accurate,
3003      but somebody could easily become confused and think two credits were
3004      made, when really those two line items on two different bills represent
3005      only a single credit
3006
3007 =cut
3008
3009 sub _items_credits {
3010
3011   my $self= shift;
3012
3013   # Simple memoize
3014   return @{$self->get('_items_credits')} if $self->get('_items_credits');
3015
3016   my %opt = @_;
3017   my $template = $opt{template} || $self->get('_template');
3018   my $trim_len = $opt{template} || $self->get('trim_len') || 40;
3019
3020   my @return;
3021   my @cust_credit_objs;
3022
3023   if ($self->conf->exists('previous_balance-payments_since')) {
3024     if ($template eq 'statement') {
3025       # Case 3 (see above)
3026       # Return credits timestamped between the previous and following bills
3027
3028       my $previous_bill  = $self->previous_bill;
3029       my $following_bill = $self->following_bill;
3030
3031       my $date_start = ref $previous_bill  ? $previous_bill->_date  : 0;
3032       my $date_end   = ref $following_bill ? $following_bill->_date : undef;
3033
3034       my %query = (
3035         table => 'cust_credit',
3036         hashref => {
3037           custnum => $self->custnum,
3038           _date => { op => '>=', value => $date_start },
3039         },
3040       );
3041       $query{extra_sql} = " AND _date <= $date_end " if $date_end;
3042
3043       @cust_credit_objs = qsearch(\%query);
3044
3045     } else {
3046       # Case 2 (see above)
3047       # Return credits timestamps between this and the previous bills
3048
3049       my $date_start = 0;
3050       my $date_end = $self->_date;
3051
3052       my $previous_bill = $self->previous_bill;
3053       if (ref $previous_bill) {
3054         $date_start = $previous_bill->_date;
3055       }
3056
3057       @cust_credit_objs = qsearch({
3058         table => 'cust_credit',
3059         hashref => {
3060           custnum => $self->custnum,
3061           _date => {op => '>=', value => $date_start},
3062         },
3063         extra_sql => " AND _date <= $date_end ",
3064       });
3065     }
3066
3067   } else {
3068     # Case 1 (see above)
3069     # Return only credits that have been applied to this bill
3070
3071     @cust_credit_objs = $self->cust_credited;
3072
3073   }
3074
3075   # Translate objects into hashrefs
3076   foreach my $obj ( @cust_credit_objs ) {
3077     my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
3078     my %r_obj = (
3079       amount       => sprintf('%.2f',$cust_credit->amount),
3080       crednum      => $cust_credit->crednum,
3081       _date        => $cust_credit->_date,
3082       creditreason => $cust_credit->reason,
3083     );
3084
3085     my $reason = substr($cust_credit->reason, 0, $trim_len);
3086     $reason .= '...' if length($reason) < length($cust_credit->reason);
3087     $reason = "($reason)" if $reason;
3088
3089     $r_obj{description} = join(' ',
3090       $self->mt('Credit applied'),
3091       $self->time2str_local('short', $cust_credit->_date),
3092       $reason,
3093     );
3094
3095     push @return, \%r_obj;
3096   }
3097   $self->set('_items_credits',\@return);
3098   @return;
3099 }
3100
3101 =item _items_credits_total
3102
3103   Return the total of al items from _items_credits
3104   Will vary based on invoice display conf flag
3105
3106 =cut
3107
3108 sub _items_credits_total {
3109   my $self = shift;
3110   my $tot = 0;
3111   $tot += $_->{amount} for $self->_items_credits();
3112   return $tot;
3113 }
3114
3115
3116
3117 =item _items_credits_postbill()
3118
3119   Returns an array of hashrefs for credits where
3120   - Credit issued after this invoice
3121   - Credit applied to an invoice before this invoice
3122
3123   Returned hashrefs are of the format returned by _items_credits()
3124
3125 =cut
3126
3127 sub _items_credits_postbill {
3128   my $self = shift;
3129
3130   my @cust_credit_bill = qsearch({
3131     table   => 'cust_credit_bill',
3132     select  => join(', ',qw(
3133       cust_credit_bill.creditbillnum
3134       cust_credit_bill._date
3135       cust_credit_bill.invnum
3136       cust_credit_bill.amount
3137     )),
3138     addl_from => ' LEFT JOIN cust_credit'.
3139                  ' ON (cust_credit_bill.crednum = cust_credit.crednum) ',
3140     extra_sql => ' WHERE cust_credit.custnum     = '.$self->custnum.
3141                  ' AND   cust_credit_bill._date  > '.$self->_date.
3142                  ' AND   cust_credit_bill.invnum < '.$self->invnum.' ',
3143 #! did not investigate why hashref doesn't work for this join query
3144 #    hashref => {
3145 #      'cust_credit.custnum'     => {op => '=', value => $self->custnum},
3146 #      'cust_credit_bill._date'  => {op => '>', value => $self->_date},
3147 #      'cust_credit_bill.invnum' => {op => '<', value => $self->invnum},
3148 #    },
3149   });
3150
3151   return map {{
3152     _date         => $_->_date,
3153     invnum        => $_->invnum,
3154     amount        => $_->amount,
3155     creditbillnum => $_->creditbillnum,
3156   }} @cust_credit_bill;
3157 }
3158
3159 =item _items_payments_postbill()
3160
3161   Returns an array of hashrefs for payments where
3162   - Payment occured after this invoice
3163   - Payment applied to an invoice before this invoice
3164
3165   Returned hashrefs are of the format returned by _items_payments()
3166
3167 =cut
3168
3169 sub _items_payments_postbill {
3170   my $self = shift;
3171
3172   my @cust_bill_pay = qsearch({
3173     table    => 'cust_bill_pay',
3174     select => join(', ',qw(
3175       cust_bill_pay.billpaynum
3176       cust_bill_pay._date
3177       cust_bill_pay.invnum
3178       cust_bill_pay.amount
3179     )),
3180     addl_from => ' LEFT JOIN cust_bill'.
3181                  ' ON (cust_bill_pay.invnum = cust_bill.invnum) ',
3182     extra_sql => ' WHERE cust_bill.custnum     = '.$self->custnum.
3183                  ' AND   cust_bill_pay._date   > '.$self->_date.
3184                  ' AND   cust_bill_pay.invnum  < '.$self->invnum.' ',
3185   });
3186
3187   return map {{
3188     _date      => $_->_date,
3189     invnum     => $_->invnum,
3190     amount     => $_->amount,
3191     billpaynum => $_->billpaynum,
3192   }} @cust_bill_pay;
3193 }
3194
3195 =item _items_payments()
3196
3197   Return array of hashrefs containing payments to be shown as line-items
3198   when rendering this bill.
3199
3200   keys for each payment item:
3201   - paynum: id of payment
3202   - amount: payment amount
3203   - description: line item to be displayed on the bill
3204
3205   This method has three ways it selects which payments to display on
3206   this bill:
3207
3208   1) Default Case: No Conf flag for 'previous_balance-payments_since'
3209
3210      Returns payments that have been applied to this bill only
3211
3212   2) Case:
3213        Conf flag set for 'previous_balance-payments_since'
3214
3215      List all payments that have been recorded between the timestamps
3216      of the previous invoice and this invoice
3217
3218   3) Case:
3219         Conf flag set for 'previous_balance-payments_since'
3220         $opt{'template'} eq 'statement'
3221
3222      List all payments that have been recorded between the timestamps
3223      of the previous invoice and the following invoice.
3224
3225      I am concerned with this implementation, but leaving in place as is
3226      If this option is selected, while viewing an older bill, the old bill
3227      will show ALL future payments for future bills, but no charges for
3228      future bills.  Somebody could be misled into believing they have a
3229      large account credit when they don't.  Also, interrupts the chain of
3230      invoices as an account history... the customer could have two invoices
3231      in their fileing cabinet, for two different dates, both with a line item
3232      for the same duplicate payment.  The accounting is technically accurate,
3233      but somebody could easily become confused and think two payments were
3234      made, when really those two line items on two different bills represent
3235      only a single payment.
3236
3237 =cut
3238
3239 sub _items_payments {
3240
3241   my $self = shift;
3242
3243   # Simple memoize
3244   return @{$self->get('_items_payments')} if $self->get('_items_payments');
3245
3246   my %opt = @_;
3247   my $template = $opt{template} || $self->get('_template');
3248
3249   my @return;
3250   my @cust_pay_objs;
3251
3252   my $c_invoice_payment_details = $self->conf->exists('invoice_payment_details');
3253
3254   if ($self->conf->exists('previous_balance-payments_since')) {
3255     if ($template eq 'statement') {
3256       # Case 3 (see above)
3257       # Return payments timestamped between the previous and following bills
3258
3259       my $previous_bill  = $self->previous_bill;
3260       my $following_bill = $self->following_bill;
3261
3262       my $date_start = ref $previous_bill  ? $previous_bill->_date  : 0;
3263       my $date_end   = ref $following_bill ? $following_bill->_date : undef;
3264
3265       my %query = (
3266         table => 'cust_pay',
3267         hashref => {
3268           custnum => $self->custnum,
3269           _date => { op => '>=', value => $date_start },
3270         },
3271       );
3272       $query{extra_sql} = " AND _date <= $date_end " if $date_end;
3273
3274       @cust_pay_objs = qsearch(\%query);
3275
3276     } else {
3277       # Case 2 (see above)
3278       # Return payments timestamped between this and the previous bill
3279
3280       my $date_start = 0;
3281       my $date_end = $self->_date;
3282
3283       my $previous_bill = $self->previous_bill;
3284       if (ref $previous_bill) {
3285         $date_start = $previous_bill->_date;
3286       }
3287
3288       @cust_pay_objs = qsearch({
3289         table => 'cust_pay',
3290         hashref => {
3291           custnum => $self->custnum,
3292           _date => {op => '>=', value => $date_start},
3293         },
3294         extra_sql => " AND _date <= $date_end ",
3295       });
3296     }
3297
3298   } else {
3299     # Case 1 (see above)
3300     # Return payments applied only to this bill
3301
3302     @cust_pay_objs = $self->cust_bill_pay;
3303
3304   }
3305
3306   $self->set(
3307     '_items_payments',
3308     [ $self->__items_payments_make_hashref(@cust_pay_objs) ]
3309   );
3310   return @{ $self->get('_items_payments') };
3311 }
3312
3313 =item _items_payments_total
3314
3315   Return a total of all records returned by _items_payments
3316   Results vary based on invoicing conf flags
3317
3318 =cut
3319
3320 sub _items_payments_total {
3321   my $self = shift;
3322   my $tot = 0;
3323   $tot += $_->{amount} for $self->_items_payments();
3324   return $tot;
3325 }
3326
3327 sub __items_payments_make_hashref {
3328   # Transform a FS::cust_pay object into a simple hashref for invoice
3329   my ($self, @cust_pay_objs) = @_;
3330   my $c_invoice_payment_details = $self->conf->exists('invoice_payment_details');
3331   my @return;
3332
3333   for my $obj (@cust_pay_objs) {
3334
3335     # In case we're passed FS::cust_bill_pay (or something else?)
3336     # Below, we use $obj to render amount rather than $cust_apy.
3337     #   If we were passed cust_bill_pay objs, then:
3338     #   $obj->amount represents the amount applied to THIS invoice
3339     #   $cust_pay->amount represents the total payment, which may have
3340     #       been applied accross several invoices.
3341     # If we were passed cust_bill_pay objects, then the conf flag
3342     # previous_balance-payments_since is NOT set, so we should not
3343     # present any payments not applied to this invoice.
3344     my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
3345
3346     my %r_obj = (
3347       _date   => $cust_pay->_date,
3348       amount  => sprintf("%.2f", $obj->amount),
3349       paynum  => $cust_pay->paynum,
3350       payinfo => $cust_pay->payby_payinfo_pretty(),
3351       description => join(' ',
3352         $self->mt('Payment received'),
3353         $self->time2str_local('short', $cust_pay->_date),
3354       ),
3355     );
3356
3357     if ($c_invoice_payment_details) {
3358       $r_obj{description} = join(' ',
3359         $r_obj{description},
3360         $self->mt('via'),
3361         $cust_pay->payby_payinfo_pretty($self->cust_main->locale),
3362       );
3363     }
3364
3365     push @return, \%r_obj;
3366   }
3367   return @return;
3368 }
3369
3370 =item _items_total()
3371
3372   Generate the line-items to be shown on the bill in the "Totals" section
3373
3374   Returns a list of hashrefs, each with the keys:
3375   - total_item: description field
3376   - total_amount: dollar-formatted number amount
3377
3378   Information presented by this method varies based on Conf
3379
3380   Conf previous_balance-payments_due
3381   - default, flag not set
3382       Only transactions that were applied to this bill bill be
3383       displayed and calculated intothe total.  If items exist in
3384       the past-due section, those items will disappear from this
3385       invoice if they have been paid off.
3386
3387   - previous_balance-payments_due flag is set
3388       Transactions occuring after the timestsamp of this
3389       invoice are not reflected on invoice line items
3390
3391       Only payments/credits applied between the previous invoice
3392       and this one are displayed and calculated into the total
3393
3394   - previous_balance-payments_due && $opt{template} eq 'statement'
3395       Same as above, except payments/credits occuring before the date
3396       of the following invoice are also displayed and calculated into
3397       the total
3398
3399   Conf previous_balance-exclude_from_total
3400   - default, flag not set
3401       The "Totals" section contains a single line item.
3402       The dollar amount of this line items is a sum of old and new charges
3403   - previous_balance-exclude_from_total flag is set
3404       The "Totals" section contains two line items.
3405       One for previous balance, one for new charges
3406   !NOTE: Avent virtualization flag 'disable_previous_balance' can
3407       override the global conf flag previous_balance-exclude_from_total
3408
3409   Conf invoice_show_prior_due_date
3410   - default, flag not set
3411     Total line item in the "Totals" section does not mention due date
3412   - invoice_show_prior_due_date flag is set
3413     Total line item in the "Totals" section includes either the due
3414     date of the invoice, or the specified invoice terms
3415     ? Not sure why this is called "Prior" due date, since we seem to be
3416       displaying THIS due date...
3417 =cut
3418
3419 sub _items_total {
3420   my $self = shift;
3421   my $conf = $self->conf;
3422
3423   my $c_multi_line_total = 0;
3424   $c_multi_line_total    = 1
3425     if $conf->exists('previous_balance-exclude_from_total')
3426     && $self->enable_previous();
3427
3428   my @line_items;
3429   my $invoice_charges  = $self->charged();
3430
3431   # _items_previous() is aware of conf flags
3432   my $previous_balance = 0;
3433   $previous_balance += $_->{amount} for $self->_items_previous();
3434
3435   my $total_charges;
3436   my $total_descr;
3437
3438   if ( $previous_balance && $c_multi_line_total ) {
3439     # previous balance, new charges on separate lines
3440
3441     push @line_items, {
3442       total_amount => sprintf('%.2f',$previous_balance),
3443       total_item   => $self->mt(
3444         $conf->config('previous_balance-text') || 'Previous Balance'
3445       ),
3446     };
3447
3448     $total_charges = $invoice_charges;
3449     $total_descr   = $self->mt(
3450       $conf->config('previous_balance-text-total_new_charges')
3451       || 'Total New Charges'
3452     );
3453
3454   } else {
3455     # previous balance and new charges combined into a single total line
3456     $total_charges = $invoice_charges + $previous_balance;
3457     $total_descr = $self->mt('Total Charges');
3458   }
3459
3460   if ( $conf->exists('invoice_show_prior_due_date') && !$conf->exists('invoice_omit_due_date') ) {
3461     # then the due date should be shown with Total New Charges,
3462     # and should NOT be shown with the Balance Due message.
3463
3464     if ( $self->due_date ) {
3465       $total_descr .= $self->invoice_pay_by_msg;
3466     } elsif ( $self->terms ) {
3467       $total_descr = join(' ',
3468         $total_descr,
3469         '-',
3470         $self->mt($self->terms)
3471       );
3472     }
3473   }
3474
3475   push @line_items, {
3476     total_amount => sprintf('%.2f', $total_charges),
3477     total_item   => $total_descr,
3478   };
3479
3480   return @line_items;
3481 }
3482
3483 =item _items_aging_balances
3484
3485   Returns an array of aged balance amounts from a given epoch timestamp.
3486
3487   The time of day is ignored for this calculation, so that slight differences
3488   on the generation time of an invoice doesn't determine which column an
3489   aged balance falls into.
3490
3491   Will not include any balances dated after the given timestamp in
3492   the calculated totals
3493
3494   usage:
3495   @aged_balances = $b->_items_aging_balances( $b->_date )
3496
3497   @aged_balances = (
3498     under30d,
3499     30d-60d,
3500     60d-90d,
3501     over90d
3502   )
3503
3504 =cut
3505
3506 sub _items_aging_balances {
3507   my ($self, $basetime) = @_;
3508   die "Incorrect usage of _items_aging_balances()" unless ref $self;
3509
3510   $basetime = $self->_date unless $basetime;
3511   my @aging_balances = (0, 0, 0, 0);
3512   my @open_invoices = $self->_items_previous();
3513   my $d30 = 2592000; # 60 * 60 * 24 * 30,
3514   my $d60 = 5184000; # 60 * 60 * 24 * 60,
3515   my $d90 = 7776000; # 60 * 60 * 24 * 90
3516
3517   # Move the clock back on our given day to 12:00:01 AM
3518   my $dt_basetime = DateTime->from_epoch(epoch => $basetime);
3519   my $dt_12am = DateTime->new(
3520     year   => $dt_basetime->year,
3521     month  => $dt_basetime->month,
3522     day    => $dt_basetime->day,
3523     hour   => 0,
3524     minute => 0,
3525     second => 1,
3526   )->epoch();
3527
3528   # set our epoch breakpoints
3529   $_ = $dt_12am - $_ for $d30, $d60, $d90;
3530
3531   # grep the aged balances
3532   for my $oinv (@open_invoices) {
3533     if ($oinv->{_date} <= $basetime && $oinv->{_date} > $d30) {
3534       # If post invoice dated less than 30days ago
3535       $aging_balances[0] += $oinv->{amount};
3536     } elsif ($oinv->{_date} <= $d30 && $oinv->{_date} > $d60) {
3537       # If past invoice dated between 30-60 days ago
3538       $aging_balances[1] += $oinv->{amount};
3539     } elsif ($oinv->{_date} <= $d60 && $oinv->{_date} > $d90) {
3540       # If past invoice dated between 60-90 days ago
3541       $aging_balances[2] += $oinv->{amount};
3542     } else {
3543       # If past invoice dated 90+ days ago
3544       $aging_balances[3] += $oinv->{amount};
3545     }
3546   }
3547
3548   return map{ sprintf('%.2f',$_) } @aging_balances;
3549 }
3550
3551 =item has_call_details
3552
3553 Returns true if this invoice has call details.
3554
3555 =cut
3556
3557 sub has_call_details {
3558   my $self = shift;
3559   $self->scalar_sql("
3560     SELECT 1 FROM cust_bill_pkg_detail
3561              LEFT JOIN cust_bill_pkg USING (billpkgnum)
3562       WHERE cust_bill_pkg_detail.format = 'C'
3563         AND cust_bill_pkg.invnum = ?
3564       LIMIT 1
3565   ", $self->invnum);
3566 }
3567
3568 =item call_details [ OPTION => VALUE ... ]
3569
3570 Returns an array of CSV strings representing the call details for this invoice
3571 The only option available is the boolean prepend_billed_number
3572
3573 =cut
3574
3575 sub call_details {
3576   my ($self, %opt) = @_;
3577
3578   my $format_function = sub { shift };
3579
3580   if ($opt{prepend_billed_number}) {
3581     $format_function = sub {
3582       my $detail = shift;
3583       my $row = shift;
3584
3585       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
3586
3587     };
3588   }
3589
3590   my @details = map { $_->details( 'format_function' => $format_function,
3591                                    'escape_function' => sub{ return() },
3592                                  )
3593                     }
3594                   grep { $_->pkgnum }
3595                   $self->cust_bill_pkg;
3596   my $header = $details[0];
3597   ( $header, grep { $_ ne $header } @details );
3598 }
3599
3600 =item cust_pay_batch
3601
3602 Returns all L<FS::cust_pay_batch> records linked to this invoice. Deprecated,
3603 will be removed.
3604
3605 =cut
3606
3607 sub cust_pay_batch {
3608   carp "FS::cust_bill->cust_pay_batch is deprecated";
3609   my $self = shift;
3610   qsearch('cust_pay_batch', { 'invnum' => $self->invnum });
3611 }
3612
3613 =back
3614
3615 =head1 SUBROUTINES
3616
3617 =over 4
3618
3619 =item process_reprint
3620
3621 =cut
3622
3623 sub process_reprint {
3624   process_re_X('print', @_);
3625 }
3626
3627 =item process_reemail
3628
3629 =cut
3630
3631 sub process_reemail {
3632   process_re_X('email', @_);
3633 }
3634
3635 =item process_refax
3636
3637 =cut
3638
3639 sub process_refax {
3640   process_re_X('fax', @_);
3641 }
3642
3643 =item process_reftp
3644
3645 =cut
3646
3647 sub process_reftp {
3648   process_re_X('ftp', @_);
3649 }
3650
3651 =item respool
3652
3653 =cut
3654
3655 sub process_respool {
3656   process_re_X('spool', @_);
3657 }
3658
3659 use Data::Dumper;
3660 sub process_re_X {
3661   my( $method, $job ) = ( shift, shift );
3662   warn "$me process_re_X $method for job $job\n" if $DEBUG;
3663
3664   my $param = shift;
3665   warn Dumper($param) if $DEBUG;
3666
3667   re_X(
3668     $method,
3669     $job,
3670     %$param,
3671   );
3672
3673 }
3674
3675 # this is called from search/cust_bill.html and given all its search
3676 # parameters, so it needs to perform the same search.
3677
3678 sub re_X {
3679   # spool_invoice ftp_invoice fax_invoice print_invoice
3680   my($method, $job, %param ) = @_;
3681   if ( $DEBUG ) {
3682     warn "re_X $method for job $job with param:\n".
3683          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
3684   }
3685
3686   #some false laziness w/search/cust_bill.html
3687   $param{'order_by'} = 'cust_bill._date';
3688
3689   my $query = FS::cust_bill->search(\%param);
3690   delete $query->{'count_query'};
3691   delete $query->{'count_addl'};
3692
3693   $query->{debug} = 1; # was in here before, is obviously useful
3694
3695   my @cust_bill = qsearch( $query );
3696
3697   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
3698
3699   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
3700     if $DEBUG;
3701
3702   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
3703   foreach my $cust_bill ( @cust_bill ) {
3704     $cust_bill->$method();
3705
3706     if ( $job ) { #progressbar foo
3707       $num++;
3708       if ( time - $min_sec > $last ) {
3709         my $error = $job->update_statustext(
3710           int( 100 * $num / scalar(@cust_bill) )
3711         );
3712         die $error if $error;
3713         $last = time;
3714       }
3715     }
3716
3717   }
3718
3719 }
3720
3721 sub API_getinfo {
3722   my $self = shift;
3723   +{ ( map { $_=>$self->$_ } $self->fields ),
3724      'owed' => $self->owed,
3725      #XXX last payment applied date
3726    };
3727 }
3728
3729 =back
3730
3731 =head1 CLASS METHODS
3732
3733 =over 4
3734
3735 =item owed_sql
3736
3737 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3738
3739 =cut
3740
3741 sub owed_sql {
3742   my ($class, $start, $end) = @_;
3743   'charged - '.
3744     $class->paid_sql($start, $end). ' - '.
3745     $class->credited_sql($start, $end);
3746 }
3747
3748 =item net_sql
3749
3750 Returns an SQL fragment to retreive the net amount (charged minus credited).
3751
3752 =cut
3753
3754 sub net_sql {
3755   my ($class, $start, $end) = @_;
3756   'charged - '. $class->credited_sql($start, $end);
3757 }
3758
3759 =item paid_sql
3760
3761 Returns an SQL fragment to retreive the amount paid against this invoice.
3762
3763 =cut
3764
3765 sub paid_sql {
3766   my ($class, $start, $end) = @_;
3767   $start &&= "AND cust_bill_pay._date <= $start";
3768   $end   &&= "AND cust_bill_pay._date > $end";
3769   $start = '' unless defined($start);
3770   $end   = '' unless defined($end);
3771   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3772        WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
3773 }
3774
3775 =item credited_sql
3776
3777 Returns an SQL fragment to retreive the amount credited against this invoice.
3778
3779 =cut
3780
3781 sub credited_sql {
3782   my ($class, $start, $end) = @_;
3783   $start &&= "AND cust_credit_bill._date <= $start";
3784   $end   &&= "AND cust_credit_bill._date >  $end";
3785   $start = '' unless defined($start);
3786   $end   = '' unless defined($end);
3787   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3788        WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
3789 }
3790
3791 =item due_date_sql
3792
3793 Returns an SQL fragment to retrieve the due date of an invoice.
3794 Currently only supported on PostgreSQL.
3795
3796 =cut
3797
3798 sub due_date_sql {
3799   die "don't use: doesn't account for agent-specific invoice_default_terms";
3800
3801   #we're passed a $conf but not a specific customer (that's in the query), so
3802   # to make this work we'd need an agentnum-aware "condition_sql_conf" like
3803   # "condition_sql_option" that retreives a conf value with SQL in an agent-
3804   # aware fashion
3805
3806   my $conf = new FS::Conf;
3807 'COALESCE(
3808   SUBSTRING(
3809     COALESCE(
3810       cust_bill.invoice_terms,
3811       cust_main.invoice_terms,
3812       \''.($conf->config('invoice_default_terms') || '').'\'
3813     ), E\'Net (\\\\d+)\'
3814   )::INTEGER, 0
3815 ) * 86400 + cust_bill._date'
3816 }
3817
3818 =back
3819
3820 =head1 BUGS
3821
3822 The delete method.
3823
3824 =head1 SEE ALSO
3825
3826 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3827 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
3828 documentation.
3829
3830 =cut
3831
3832 1;