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