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