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