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