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