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