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