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