make non-package fees appear in invoice spools, #29824, #25899
[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->invoice_from_full( $self->cust_main->agentnum );
1089
1090   my @invoicing_list = $self->cust_main->invoicing_list_emailonly;
1091
1092   if ( ! @invoicing_list ) { #no recipients
1093     if ( $conf->exists('cust_bill-no_recipients-error') ) {
1094       die 'No recipients for customer #'. $self->custnum;
1095     } else {
1096       #default: better to notify this person than silence
1097       @invoicing_list = ($from);
1098     }
1099   }
1100
1101   $self->SUPER::email( {
1102     'from' => $from,
1103     'to'   => \@invoicing_list,
1104     %$opt,
1105   });
1106
1107 }
1108
1109 #this stays here for now because its explicitly used as
1110 # FS::cust_bill::queueable_email
1111 sub queueable_email {
1112   my %opt = @_;
1113
1114   my $self = qsearchs('cust_bill', { 'invnum' => $opt{invnum} } )
1115     or die "invalid invoice number: " . $opt{invnum};
1116
1117   my %args = map {$_ => $opt{$_}} 
1118              grep { $opt{$_} }
1119               qw( from notice_name no_coupon template );
1120
1121   my $error = $self->email( \%args );
1122   die $error if $error;
1123
1124 }
1125
1126 sub email_subject {
1127   my $self = shift;
1128   my $conf = $self->conf;
1129
1130   #my $template = scalar(@_) ? shift : '';
1131   #per-template?
1132
1133   my $subject = $conf->config('invoice_subject', $self->cust_main->agentnum)
1134                 || 'Invoice';
1135
1136   my $cust_main = $self->cust_main;
1137   my $name = $cust_main->name;
1138   my $name_short = $cust_main->name_short;
1139   my $invoice_number = $self->invnum;
1140   my $invoice_date = $self->_date_pretty;
1141
1142   eval qq("$subject");
1143 }
1144
1145 =item lpr_data HASHREF
1146
1147 Returns the postscript or plaintext for this invoice as an arrayref.
1148
1149 Options must be passed as a hashref.  Positional parameters are no longer 
1150 allowed.
1151
1152 I<template>, if specified, is the name of a suffix for alternate invoices.
1153
1154 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1155
1156 =cut
1157
1158 sub lpr_data {
1159   my $self = shift;
1160   my $conf = $self->conf;
1161   my $opt = shift || {};
1162   if ($opt and !ref($opt)) {
1163     # nobody does this anyway
1164     die "FS::cust_bill::lpr_data called with positional parameters";
1165   }
1166
1167   my $method = $conf->exists('invoice_latex') ? 'print_ps' : 'print_text';
1168   [ $self->$method( $opt ) ];
1169 }
1170
1171 =item print HASHREF
1172
1173 Prints this invoice.
1174
1175 Options must be passed as a hashref.
1176
1177 I<template>, if specified, is the name of a suffix for alternate invoices.
1178
1179 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1180
1181 =cut
1182
1183 sub print {
1184   my $self = shift;
1185   return if $self->hide;
1186   my $conf = $self->conf;
1187   my $opt = shift || {};
1188   if ($opt and !ref($opt)) {
1189     die "FS::cust_bill::print called with positional parameters";
1190   }
1191
1192   my $lpr = delete $opt->{lpr};
1193   if($conf->exists('invoice_print_pdf')) {
1194     # Add the invoice to the current batch.
1195     $self->batch_invoice($opt);
1196   }
1197   else {
1198     do_print(
1199       $self->lpr_data($opt),
1200       'agentnum' => $self->cust_main->agentnum,
1201       'lpr'      => $lpr,
1202     );
1203   }
1204 }
1205
1206 =item fax_invoice HASHREF
1207
1208 Faxes this invoice.
1209
1210 Options must be passed as a hashref.
1211
1212 I<template>, if specified, is the name of a suffix for alternate invoices.
1213
1214 I<notice_name>, if specified, overrides "Invoice" as the name of the sent document (templates from 10/2009 or newer required)
1215
1216 =cut
1217
1218 sub fax_invoice {
1219   my $self = shift;
1220   return if $self->hide;
1221   my $conf = $self->conf;
1222   my $opt = shift || {};
1223   if ($opt and !ref($opt)) {
1224     die "FS::cust_bill::fax_invoice called with positional parameters";
1225   }
1226
1227   die 'FAX invoice destination not (yet?) supported with plain text invoices.'
1228     unless $conf->exists('invoice_latex');
1229
1230   my $dialstring = $self->cust_main->getfield('fax');
1231   #Check $dialstring?
1232
1233   my $error = send_fax( 'docdata'    => $self->lpr_data($opt),
1234                         'dialstring' => $dialstring,
1235                       );
1236   die $error if $error;
1237
1238 }
1239
1240 =item batch_invoice [ HASHREF ]
1241
1242 Place this invoice into the open batch (see C<FS::bill_batch>).  If there 
1243 isn't an open batch, one will be created.
1244
1245 HASHREF may contain any options to be passed to C<print_pdf>.
1246
1247 =cut
1248
1249 sub batch_invoice {
1250   my ($self, $opt) = @_;
1251   my $bill_batch = $self->get_open_bill_batch;
1252   my $cust_bill_batch = FS::cust_bill_batch->new({
1253       batchnum => $bill_batch->batchnum,
1254       invnum   => $self->invnum,
1255   });
1256   return $cust_bill_batch->insert($opt);
1257 }
1258
1259 =item get_open_batch
1260
1261 Returns the currently open batch as an FS::bill_batch object, creating a new
1262 one if necessary.  (A per-agent batch if invoice_print_pdf-spoolagent is
1263 enabled)
1264
1265 =cut
1266
1267 sub get_open_bill_batch {
1268   my $self = shift;
1269   my $conf = $self->conf;
1270   my $hashref = { status => 'O' };
1271   $hashref->{'agentnum'} = $conf->exists('invoice_print_pdf-spoolagent')
1272                              ? $self->cust_main->agentnum
1273                              : '';
1274   my $batch = qsearchs('bill_batch', $hashref);
1275   return $batch if $batch;
1276   $batch = FS::bill_batch->new($hashref);
1277   my $error = $batch->insert;
1278   die $error if $error;
1279   return $batch;
1280 }
1281
1282 =item ftp_invoice [ TEMPLATENAME ] 
1283
1284 Sends this invoice data via FTP.
1285
1286 TEMPLATENAME is unused?
1287
1288 =cut
1289
1290 sub ftp_invoice {
1291   my $self = shift;
1292   my $conf = $self->conf;
1293   my $template = scalar(@_) ? shift : '';
1294
1295   $self->send_csv(
1296     'protocol'   => 'ftp',
1297     'server'     => $conf->config('cust_bill-ftpserver'),
1298     'username'   => $conf->config('cust_bill-ftpusername'),
1299     'password'   => $conf->config('cust_bill-ftppassword'),
1300     'dir'        => $conf->config('cust_bill-ftpdir'),
1301     'format'     => $conf->config('cust_bill-ftpformat'),
1302   );
1303 }
1304
1305 =item spool_invoice [ TEMPLATENAME ] 
1306
1307 Spools this invoice data (see L<FS::spool_csv>)
1308
1309 TEMPLATENAME is unused?
1310
1311 =cut
1312
1313 sub spool_invoice {
1314   my $self = shift;
1315   my $conf = $self->conf;
1316   my $template = scalar(@_) ? shift : '';
1317
1318   $self->spool_csv(
1319     'format'       => $conf->config('cust_bill-spoolformat'),
1320     'agent_spools' => $conf->exists('cust_bill-spoolagent'),
1321   );
1322 }
1323
1324 =item send_csv OPTION => VALUE, ...
1325
1326 Sends invoice as a CSV data-file to a remote host with the specified protocol.
1327
1328 Options are:
1329
1330 protocol - currently only "ftp"
1331 server
1332 username
1333 password
1334 dir
1335
1336 The file will be named "N-YYYYMMDDHHMMSS.csv" where N is the invoice number
1337 and YYMMDDHHMMSS is a timestamp.
1338
1339 See L</print_csv> for a description of the output format.
1340
1341 =cut
1342
1343 sub send_csv {
1344   my($self, %opt) = @_;
1345
1346   #create file(s)
1347
1348   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1349   mkdir $spooldir, 0700 unless -d $spooldir;
1350
1351   # don't localize dates here, they're a defined format
1352   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', time);
1353   my $file = "$spooldir/$tracctnum.csv";
1354   
1355   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum );
1356
1357   open(CSV, ">$file") or die "can't open $file: $!";
1358   print CSV $header;
1359
1360   print CSV $detail;
1361
1362   close CSV;
1363
1364   my $net;
1365   if ( $opt{protocol} eq 'ftp' ) {
1366     eval "use Net::FTP;";
1367     die $@ if $@;
1368     $net = Net::FTP->new($opt{server}) or die @$;
1369   } else {
1370     die "unknown protocol: $opt{protocol}";
1371   }
1372
1373   $net->login( $opt{username}, $opt{password} )
1374     or die "can't FTP to $opt{username}\@$opt{server}: login error: $@";
1375
1376   $net->binary or die "can't set binary mode";
1377
1378   $net->cwd($opt{dir}) or die "can't cwd to $opt{dir}";
1379
1380   $net->put($file) or die "can't put $file: $!";
1381
1382   $net->quit;
1383
1384   unlink $file;
1385
1386 }
1387
1388 =item spool_csv
1389
1390 Spools CSV invoice data.
1391
1392 Options are:
1393
1394 =over 4
1395
1396 =item format - any of FS::Misc::::Invoicing::spool_formats
1397
1398 =item dest - if set (to POST, EMAIL or FAX), only sends spools invoices if the
1399 customer has the corresponding invoice destinations set (see
1400 L<FS::cust_main_invoice>).
1401
1402 =item agent_spools - if set to a true value, will spool to per-agent files
1403 rather than a single global file
1404
1405 =item upload_targetnum - if set to a target (see L<FS::upload_target>), will
1406 append to that spool.  L<FS::Cron::upload> will then send the spool file to
1407 that destination.
1408
1409 =item balanceover - if set, only spools the invoice if the total amount owed on
1410 this invoice and all older invoices is greater than the specified amount.
1411
1412 =item time - the "current time".  Controls the printing of past due messages
1413 in the ICS format.
1414
1415 =back
1416
1417 =cut
1418
1419 sub spool_csv {
1420   my($self, %opt) = @_;
1421
1422   my $time = $opt{'time'} || time;
1423   my $cust_main = $self->cust_main;
1424
1425   if ( $opt{'dest'} ) {
1426     my %invoicing_list = map { /^(POST|FAX)$/ or 'EMAIL' =~ /^(.*)$/; $1 => 1 }
1427                              $cust_main->invoicing_list;
1428     return 'N/A' unless $invoicing_list{$opt{'dest'}}
1429                      || ! keys %invoicing_list;
1430   }
1431
1432   if ( $opt{'balanceover'} ) {
1433     return 'N/A'
1434       if $cust_main->total_owed_date($self->_date) < $opt{'balanceover'};
1435   }
1436
1437   my $spooldir = "/usr/local/etc/freeside/export.". datasrc. "/cust_bill";
1438   mkdir $spooldir, 0700 unless -d $spooldir;
1439
1440   my $tracctnum = $self->invnum. time2str('-%Y%m%d%H%M%S', $time);
1441
1442   my $file;
1443   if ( $opt{'agent_spools'} ) {
1444     $file = 'agentnum'.$cust_main->agentnum;
1445   } else {
1446     $file = 'spool';
1447   }
1448
1449   if ( $opt{'upload_targetnum'} ) {
1450     $spooldir .= '/target'.$opt{'upload_targetnum'};
1451     mkdir $spooldir, 0700 unless -d $spooldir;
1452   } # otherwise it just goes into export.xxx/cust_bill
1453
1454   if ( lc($opt{'format'}) eq 'billco' ) {
1455     $file .= '-header';
1456   }
1457
1458   $file = "$spooldir/$file.csv";
1459   
1460   my ( $header, $detail ) = $self->print_csv(%opt, 'tracctnum' => $tracctnum);
1461
1462   open(CSV, ">>$file") or die "can't open $file: $!";
1463   flock(CSV, LOCK_EX);
1464   seek(CSV, 0, 2);
1465
1466   print CSV $header;
1467
1468   if ( lc($opt{'format'}) eq 'billco' ) {
1469
1470     flock(CSV, LOCK_UN);
1471     close CSV;
1472
1473     $file =~ s/-header.csv$/-detail.csv/;
1474
1475     open(CSV,">>$file") or die "can't open $file: $!";
1476     flock(CSV, LOCK_EX);
1477     seek(CSV, 0, 2);
1478   }
1479
1480   print CSV $detail if defined($detail);
1481
1482   flock(CSV, LOCK_UN);
1483   close CSV;
1484
1485   return '';
1486
1487 }
1488
1489 =item print_csv OPTION => VALUE, ...
1490
1491 Returns CSV data for this invoice.
1492
1493 Options are:
1494
1495 format - 'default', 'billco', 'oneline', 'bridgestone'
1496
1497 Returns a list consisting of two scalars.  The first is a single line of CSV
1498 header information for this invoice.  The second is one or more lines of CSV
1499 detail information for this invoice.
1500
1501 If I<format> is not specified or "default", the fields of the CSV file are as
1502 follows:
1503
1504 record_type, invnum, custnum, _date, charged, first, last, company, address1, 
1505 address2, city, state, zip, country, pkg, setup, recur, sdate, edate
1506
1507 =over 4
1508
1509 =item record type - B<record_type> is either C<cust_bill> or C<cust_bill_pkg>
1510
1511 B<record_type> is C<cust_bill> for the initial header line only.  The
1512 last five fields (B<pkg> through B<edate>) are irrelevant, and all other
1513 fields are filled in.
1514
1515 B<record_type> is C<cust_bill_pkg> for detail lines.  Only the first two fields
1516 (B<record_type> and B<invnum>) and the last five fields (B<pkg> through B<edate>)
1517 are filled in.
1518
1519 =item invnum - invoice number
1520
1521 =item custnum - customer number
1522
1523 =item _date - invoice date
1524
1525 =item charged - total invoice amount
1526
1527 =item first - customer first name
1528
1529 =item last - customer first name
1530
1531 =item company - company name
1532
1533 =item address1 - address line 1
1534
1535 =item address2 - address line 1
1536
1537 =item city
1538
1539 =item state
1540
1541 =item zip
1542
1543 =item country
1544
1545 =item pkg - line item description
1546
1547 =item setup - line item setup fee (one or both of B<setup> and B<recur> will be defined)
1548
1549 =item recur - line item recurring fee (one or both of B<setup> and B<recur> will be defined)
1550
1551 =item sdate - start date for recurring fee
1552
1553 =item edate - end date for recurring fee
1554
1555 =back
1556
1557 If I<format> is "billco", the fields of the header CSV file are as follows:
1558
1559   +-------------------------------------------------------------------+
1560   |                        FORMAT HEADER FILE                         |
1561   |-------------------------------------------------------------------|
1562   | Field | Description                   | Name       | Type | Width |
1563   | 1     | N/A-Leave Empty               | RC         | CHAR |     2 |
1564   | 2     | N/A-Leave Empty               | CUSTID     | CHAR |    15 |
1565   | 3     | Transaction Account No        | TRACCTNUM  | CHAR |    15 |
1566   | 4     | Transaction Invoice No        | TRINVOICE  | CHAR |    15 |
1567   | 5     | Transaction Zip Code          | TRZIP      | CHAR |     5 |
1568   | 6     | Transaction Company Bill To   | TRCOMPANY  | CHAR |    30 |
1569   | 7     | Transaction Contact Bill To   | TRNAME     | CHAR |    30 |
1570   | 8     | Additional Address Unit Info  | TRADDR1    | CHAR |    30 |
1571   | 9     | Bill To Street Address        | TRADDR2    | CHAR |    30 |
1572   | 10    | Ancillary Billing Information | TRADDR3    | CHAR |    30 |
1573   | 11    | Transaction City Bill To      | TRCITY     | CHAR |    20 |
1574   | 12    | Transaction State Bill To     | TRSTATE    | CHAR |     2 |
1575   | 13    | Bill Cycle Close Date         | CLOSEDATE  | CHAR |    10 |
1576   | 14    | Bill Due Date                 | DUEDATE    | CHAR |    10 |
1577   | 15    | Previous Balance              | BALFWD     | NUM* |     9 |
1578   | 16    | Pmt/CR Applied                | CREDAPPLY  | NUM* |     9 |
1579   | 17    | Total Current Charges         | CURRENTCHG | NUM* |     9 |
1580   | 18    | Total Amt Due                 | TOTALDUE   | NUM* |     9 |
1581   | 19    | Total Amt Due                 | AMTDUE     | NUM* |     9 |
1582   | 20    | 30 Day Aging                  | AMT30      | NUM* |     9 |
1583   | 21    | 60 Day Aging                  | AMT60      | NUM* |     9 |
1584   | 22    | 90 Day Aging                  | AMT90      | NUM* |     9 |
1585   | 23    | Y/N                           | AGESWITCH  | CHAR |     1 |
1586   | 24    | Remittance automation         | SCANLINE   | CHAR |   100 |
1587   | 25    | Total Taxes & Fees            | TAXTOT     | NUM* |     9 |
1588   | 26    | Customer Reference Number     | CUSTREF    | CHAR |    15 |
1589   | 27    | Federal Tax***                | FEDTAX     | NUM* |     9 |
1590   | 28    | State Tax***                  | STATETAX   | NUM* |     9 |
1591   | 29    | Other Taxes & Fees***         | OTHERTAX   | NUM* |     9 |
1592   +-------+-------------------------------+------------+------+-------+
1593
1594 If I<format> is "billco", the fields of the detail CSV file are as follows:
1595
1596                                   FORMAT FOR DETAIL FILE
1597         |                            |           |      |
1598   Field | Description                | Name      | Type | Width
1599   1     | N/A-Leave Empty            | RC        | CHAR |     2
1600   2     | N/A-Leave Empty            | CUSTID    | CHAR |    15
1601   3     | Account Number             | TRACCTNUM | CHAR |    15
1602   4     | Invoice Number             | TRINVOICE | CHAR |    15
1603   5     | Line Sequence (sort order) | LINESEQ   | NUM  |     6
1604   6     | Transaction Detail         | DETAILS   | CHAR |   100
1605   7     | Amount                     | AMT       | NUM* |     9
1606   8     | Line Format Control**      | LNCTRL    | CHAR |     2
1607   9     | Grouping Code              | GROUP     | CHAR |     2
1608   10    | User Defined               | ACCT CODE | CHAR |    15
1609
1610 If format is 'oneline', there is no detail file.  Each invoice has a 
1611 header line only, with the fields:
1612
1613 Agent number, agent name, customer number, first name, last name, address
1614 line 1, address line 2, city, state, zip, invoice date, invoice number,
1615 amount charged, amount due, previous balance, due date.
1616
1617 and then, for each line item, three columns containing the package number,
1618 description, and amount.
1619
1620 If format is 'bridgestone', there is no detail file.  Each invoice has a 
1621 header line with the following fields in a fixed-width format:
1622
1623 Customer number (in display format), date, name (first last), company,
1624 address 1, address 2, city, state, zip.
1625
1626 This is a mailing list format, and has no per-invoice fields.  To avoid
1627 sending redundant notices, the spooling event should have a "once" or 
1628 "once_percust_every" condition.
1629
1630 =cut
1631
1632 sub print_csv {
1633   my($self, %opt) = @_;
1634   
1635   eval "use Text::CSV_XS";
1636   die $@ if $@;
1637
1638   my $cust_main = $self->cust_main;
1639
1640   my $csv = Text::CSV_XS->new({'always_quote'=>1});
1641   my $format = lc($opt{'format'});
1642
1643   my $time = $opt{'time'} || time;
1644
1645   my $tracctnum = ''; #leaking out from billco-specific sections :/
1646   if ( $format eq 'billco' ) {
1647
1648     my $account_num =
1649       $self->conf->config('billco-account_num', $cust_main->agentnum);
1650
1651     $tracctnum = $account_num eq 'display_custnum'
1652                    ? $cust_main->display_custnum
1653                    : $opt{'tracctnum'};
1654
1655     my $taxtotal = 0;
1656     $taxtotal += $_->{'amount'} foreach $self->_items_tax;
1657
1658     my $duedate = $self->due_date2str('%m/%d/%Y'); # hardcoded, NOT date_format
1659
1660     my( $previous_balance, @unused ) = $self->previous; #previous balance
1661
1662     my $pmt_cr_applied = 0;
1663     $pmt_cr_applied += $_->{'amount'}
1664       foreach ( $self->_items_payments(%opt), $self->_items_credits(%opt) ) ;
1665
1666     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1667
1668     $csv->combine(
1669       '',                         #  1 | N/A-Leave Empty               CHAR   2
1670       '',                         #  2 | N/A-Leave Empty               CHAR  15
1671       $tracctnum,                 #  3 | Transaction Account No        CHAR  15
1672       $self->invnum,              #  4 | Transaction Invoice No        CHAR  15
1673       $cust_main->zip,            #  5 | Transaction Zip Code          CHAR   5
1674       $cust_main->company,        #  6 | Transaction Company Bill To   CHAR  30
1675       #$cust_main->payname,        #  7 | Transaction Contact Bill To   CHAR  30
1676       $cust_main->contact,        #  7 | Transaction Contact Bill To   CHAR  30
1677       $cust_main->address2,       #  8 | Additional Address Unit Info  CHAR  30
1678       $cust_main->address1,       #  9 | Bill To Street Address        CHAR  30
1679       '',                         # 10 | Ancillary Billing Information CHAR  30
1680       $cust_main->city,           # 11 | Transaction City Bill To      CHAR  20
1681       $cust_main->state,          # 12 | Transaction State Bill To     CHAR   2
1682
1683       # XXX ?
1684       time2str("%m/%d/%Y", $self->_date), # 13 | Bill Cycle Close Date CHAR  10
1685
1686       # XXX ?
1687       $duedate,                   # 14 | Bill Due Date                 CHAR  10
1688
1689       $previous_balance,          # 15 | Previous Balance              NUM*   9
1690       $pmt_cr_applied,            # 16 | Pmt/CR Applied                NUM*   9
1691       sprintf("%.2f", $self->charged), # 17 | Total Current Charges    NUM*   9
1692       $totaldue,                  # 18 | Total Amt Due                 NUM*   9
1693       $totaldue,                  # 19 | Total Amt Due                 NUM*   9
1694       '',                         # 20 | 30 Day Aging                  NUM*   9
1695       '',                         # 21 | 60 Day Aging                  NUM*   9
1696       '',                         # 22 | 90 Day Aging                  NUM*   9
1697       'N',                        # 23 | Y/N                           CHAR   1
1698       '',                         # 24 | Remittance automation         CHAR 100
1699       $taxtotal,                  # 25 | Total Taxes & Fees            NUM*   9
1700       $self->custnum,             # 26 | Customer Reference Number     CHAR  15
1701       '0',                        # 27 | Federal Tax***                NUM*   9
1702       sprintf("%.2f", $taxtotal), # 28 | State Tax***                  NUM*   9
1703       '0',                        # 29 | Other Taxes & Fees***         NUM*   9
1704     );
1705
1706   } elsif ( $format eq 'oneline' ) { #name
1707   
1708     my ($previous_balance) = $self->previous; 
1709     $previous_balance = sprintf('%.2f', $previous_balance);
1710     my $totaldue = sprintf('%.2f', $self->owed + $previous_balance);
1711     my @items = map {
1712                       $_->{pkgnum},
1713                       $_->{description},
1714                       $_->{amount}
1715                     }
1716                   $self->_items_pkg, #_items_nontax?  no sections or anything
1717                                      # with this format
1718                   $self->_items_tax;
1719
1720     $csv->combine(
1721       $cust_main->agentnum,
1722       $cust_main->agent->agent,
1723       $self->custnum,
1724       $cust_main->first,
1725       $cust_main->last,
1726       $cust_main->company,
1727       $cust_main->address1,
1728       $cust_main->address2,
1729       $cust_main->city,
1730       $cust_main->state,
1731       $cust_main->zip,
1732
1733       # invoice fields
1734       time2str("%x", $self->_date),
1735       $self->invnum,
1736       $self->charged,
1737       $totaldue,
1738       $previous_balance,
1739       $self->due_date2str("%x"),
1740
1741       @items,
1742     );
1743
1744   } elsif ( $format eq 'bridgestone' ) {
1745
1746     # bypass the CSV stuff and just return this
1747     my $longdate = time2str('%B %d, %Y', $time); #current time, right?
1748     my $zip = $cust_main->zip;
1749     $zip =~ s/\D//;
1750     my $prefix = $self->conf->config('bridgestone-prefix', $cust_main->agentnum)
1751       || '';
1752     return (
1753       sprintf(
1754         "%-5s%-15s%-20s%-30s%-30s%-30s%-30s%-20s%-2s%-9s\n",
1755         $prefix,
1756         $cust_main->display_custnum,
1757         $longdate,
1758         uc(substr($cust_main->contact_firstlast,0,30)),
1759         uc(substr($cust_main->company          ,0,30)),
1760         uc(substr($cust_main->address1         ,0,30)),
1761         uc(substr($cust_main->address2         ,0,30)),
1762         uc(substr($cust_main->city             ,0,20)),
1763         uc($cust_main->state),
1764         $zip
1765       ),
1766       '' #detail
1767       );
1768
1769   } elsif ( $format eq 'ics' ) {
1770
1771     my $bill = $cust_main->bill_location;
1772     my $zip = $bill->zip;
1773     my $zip4 = '';
1774
1775     $zip =~ s/\D//;
1776     if ( $zip =~ /^(\d{5})(\d{4})$/ ) {
1777       $zip = $1;
1778       $zip4 = $2;
1779     }
1780
1781     # minor false laziness with print_generic
1782     my ($previous_balance) = $self->previous;
1783     my $balance_due = $self->owed + $previous_balance;
1784     my $payment_total = sum(0, map { $_->{'amount'} } $self->_items_payments);
1785     my $credit_total  = sum(0, map { $_->{'amount'} } $self->_items_credits);
1786
1787     my $past_due = '';
1788     if ( $self->due_date and $time >= $self->due_date ) {
1789       $past_due = sprintf('Past due:$%0.2f Due Immediately', $balance_due);
1790     }
1791
1792     # again, bypass CSV
1793     my $header = sprintf(
1794       '%-10s%-30s%-48s%-2s%-50s%-30s%-30s%-25s%-2s%-5s%-4s%-8s%-8s%-10s%-10s%-10s%-10s%-10s%-10s%-480s%-35s',
1795       $cust_main->display_custnum, #BID
1796       uc($cust_main->first), #FNAME
1797       uc($cust_main->last), #LNAME
1798       '00', #BATCH, should this ever be anything else?
1799       uc($cust_main->company), #COMP
1800       uc($bill->address1), #STREET1
1801       uc($bill->address2), #STREET2
1802       uc($bill->city), #CITY
1803       uc($bill->state), #STATE
1804       $zip,
1805       $zip4,
1806       time2str('%Y%m%d', $self->_date), #BILL_DATE
1807       $self->due_date2str('%Y%m%d'), #DUE_DATE,
1808       ( map {sprintf('%0.2f', $_)}
1809         $balance_due, #AMNT_DUE
1810         $previous_balance, #PREV_BAL
1811         $payment_total, #PYMT_RCVD
1812         $credit_total, #CREDITS
1813         $previous_balance, #BEG_BAL--is this correct?
1814         $self->charged, #NEW_CHRG
1815       ),
1816       'img01', #MRKT_MSG?
1817       $past_due, #PAST_MSG
1818     );
1819
1820     my @details;
1821     my %svc_class = ('' => ''); # maybe cache this more persistently?
1822
1823     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1824
1825       my $show_pkgnum = $cust_bill_pkg->pkgnum || '';
1826       my $cust_pkg = $cust_bill_pkg->cust_pkg if $show_pkgnum;
1827
1828       if ( $cust_pkg ) {
1829
1830         my @dates = ( $self->_date, undef );
1831         if ( my $prev = $cust_bill_pkg->previous_cust_bill_pkg ) {
1832           $dates[1] = $prev->sdate; #questionable
1833         }
1834
1835         # generate an 01 detail for each service
1836         my @svcs = $cust_pkg->h_cust_svc(@dates, 'I');
1837         foreach my $cust_svc ( @svcs ) {
1838           $show_pkgnum = ''; # hide it if we're showing svcnums
1839
1840           my $svcpart = $cust_svc->svcpart;
1841           if (!exists($svc_class{$svcpart})) {
1842             my $classnum = $cust_svc->part_svc->classnum;
1843             my $part_svc_class = FS::part_svc_class->by_key($classnum)
1844               if $classnum;
1845             $svc_class{$svcpart} = $part_svc_class ? 
1846                                    $part_svc_class->classname :
1847                                    '';
1848           }
1849
1850           my @h_label = $cust_svc->label(@dates, 'I');
1851           push @details, sprintf('01%-9s%-20s%-47s',
1852             $cust_svc->svcnum,
1853             $svc_class{$svcpart},
1854             $h_label[1],
1855           );
1856         } #foreach $cust_svc
1857       } #if $cust_pkg
1858
1859       my $desc = $cust_bill_pkg->desc; # itemdesc or part_pkg.pkg
1860       if ($cust_bill_pkg->recur > 0) {
1861         $desc .= ' '.time2str('%d-%b-%Y', $cust_bill_pkg->sdate).' to '.
1862                      time2str('%d-%b-%Y', $cust_bill_pkg->edate - 86400);
1863       }
1864       push @details, sprintf('02%-6s%-60s%-10s',
1865         $show_pkgnum,
1866         $desc,
1867         sprintf('%0.2f', $cust_bill_pkg->setup + $cust_bill_pkg->recur),
1868       );
1869     } #foreach $cust_bill_pkg
1870
1871     # Tag this row so that we know whether this is one page (1), two pages
1872     # (2), # or "big" (B).  The tag will be stripped off before uploading.
1873     if ( scalar(@details) < 12 ) {
1874       push @details, '1';
1875     } elsif ( scalar(@details) < 58 ) {
1876       push @details, '2';
1877     } else {
1878       push @details, 'B';
1879     }
1880
1881     return join('', $header, @details, "\n");
1882
1883   } else { # default
1884   
1885     $csv->combine(
1886       'cust_bill',
1887       $self->invnum,
1888       $self->custnum,
1889       time2str("%x", $self->_date),
1890       sprintf("%.2f", $self->charged),
1891       ( map { $cust_main->getfield($_) }
1892           qw( first last company address1 address2 city state zip country ) ),
1893       map { '' } (1..5),
1894     ) or die "can't create csv";
1895   }
1896
1897   my $header = $csv->string. "\n";
1898
1899   my $detail = '';
1900   if ( lc($opt{'format'}) eq 'billco' ) {
1901
1902     my $lineseq = 0;
1903     my %items_opt = ( format => 'template',
1904                       escape_function => sub { shift } );
1905     # I don't know what characters billco actually tolerates in spool entries.
1906     # Text::CSV will take care of delimiters, though.
1907
1908     my @items = ( $self->_items_pkg(%items_opt),
1909                   $self->_items_fee(%items_opt) );
1910     foreach my $item (@items) {
1911
1912       my $description = $item->{'description'};
1913       if ( $item->{'_is_discount'} and exists($item->{ext_description}[0]) ) {
1914         $description .= ': ' . $item->{ext_description}[0];
1915       }
1916
1917       $csv->combine(
1918         '',                     #  1 | N/A-Leave Empty            CHAR   2
1919         '',                     #  2 | N/A-Leave Empty            CHAR  15
1920         $tracctnum,             #  3 | Account Number             CHAR  15
1921         $self->invnum,          #  4 | Invoice Number             CHAR  15
1922         $lineseq++,             #  5 | Line Sequence (sort order) NUM    6
1923         $description,           #  6 | Transaction Detail         CHAR 100
1924         $item->{'amount'},      #  7 | Amount                     NUM*   9
1925         '',                     #  8 | Line Format Control**      CHAR   2
1926         '',                     #  9 | Grouping Code              CHAR   2
1927         '',                     # 10 | User Defined               CHAR  15
1928       );
1929
1930       $detail .= $csv->string. "\n";
1931
1932     }
1933
1934   } elsif ( lc($opt{'format'}) eq 'oneline' ) {
1935
1936     #do nothing
1937
1938   } else {
1939
1940     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
1941
1942       my($pkg, $setup, $recur, $sdate, $edate);
1943       if ( $cust_bill_pkg->pkgnum ) {
1944       
1945         ($pkg, $setup, $recur, $sdate, $edate) = (
1946           $cust_bill_pkg->part_pkg->pkg,
1947           ( $cust_bill_pkg->setup != 0
1948             ? sprintf("%.2f", $cust_bill_pkg->setup )
1949             : '' ),
1950           ( $cust_bill_pkg->recur != 0
1951             ? sprintf("%.2f", $cust_bill_pkg->recur )
1952             : '' ),
1953           ( $cust_bill_pkg->sdate 
1954             ? time2str("%x", $cust_bill_pkg->sdate)
1955             : '' ),
1956           ($cust_bill_pkg->edate 
1957             ? time2str("%x", $cust_bill_pkg->edate)
1958             : '' ),
1959         );
1960   
1961       } else { #pkgnum tax
1962         next unless $cust_bill_pkg->setup != 0;
1963         $pkg = $cust_bill_pkg->desc;
1964         $setup = sprintf('%10.2f', $cust_bill_pkg->setup );
1965         ( $sdate, $edate ) = ( '', '' );
1966       }
1967   
1968       $csv->combine(
1969         'cust_bill_pkg',
1970         $self->invnum,
1971         ( map { '' } (1..11) ),
1972         ($pkg, $setup, $recur, $sdate, $edate)
1973       ) or die "can't create csv";
1974
1975       $detail .= $csv->string. "\n";
1976
1977     }
1978
1979   }
1980
1981   ( $header, $detail );
1982
1983 }
1984
1985 =item comp
1986
1987 Pays this invoice with a compliemntary payment.  If there is an error,
1988 returns the error, otherwise returns false.
1989
1990 =cut
1991
1992 sub comp {
1993   my $self = shift;
1994   my $cust_pay = new FS::cust_pay ( {
1995     'invnum'   => $self->invnum,
1996     'paid'     => $self->owed,
1997     '_date'    => '',
1998     'payby'    => 'COMP',
1999     'payinfo'  => $self->cust_main->payinfo,
2000     'paybatch' => '',
2001   } );
2002   $cust_pay->insert;
2003 }
2004
2005 =item realtime_card
2006
2007 Attempts to pay this invoice with a credit card payment via a
2008 Business::OnlinePayment realtime gateway.  See
2009 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2010 for supported processors.
2011
2012 =cut
2013
2014 sub realtime_card {
2015   my $self = shift;
2016   $self->realtime_bop( 'CC', @_ );
2017 }
2018
2019 =item realtime_ach
2020
2021 Attempts to pay this invoice with an electronic check (ACH) payment via a
2022 Business::OnlinePayment realtime gateway.  See
2023 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2024 for supported processors.
2025
2026 =cut
2027
2028 sub realtime_ach {
2029   my $self = shift;
2030   $self->realtime_bop( 'ECHECK', @_ );
2031 }
2032
2033 =item realtime_lec
2034
2035 Attempts to pay this invoice with phone bill (LEC) payment via a
2036 Business::OnlinePayment realtime gateway.  See
2037 http://search.cpan.org/search?mode=module&query=Business%3A%3AOnlinePayment
2038 for supported processors.
2039
2040 =cut
2041
2042 sub realtime_lec {
2043   my $self = shift;
2044   $self->realtime_bop( 'LEC', @_ );
2045 }
2046
2047 sub realtime_bop {
2048   my( $self, $method ) = (shift,shift);
2049   my $conf = $self->conf;
2050   my %opt = @_;
2051
2052   my $cust_main = $self->cust_main;
2053   my $balance = $cust_main->balance;
2054   my $amount = ( $balance < $self->owed ) ? $balance : $self->owed;
2055   $amount = sprintf("%.2f", $amount);
2056   return "not run (balance $balance)" unless $amount > 0;
2057
2058   my $description = 'Internet Services';
2059   if ( $conf->exists('business-onlinepayment-description') ) {
2060     my $dtempl = $conf->config('business-onlinepayment-description');
2061
2062     my $agent_obj = $cust_main->agent
2063       or die "can't retreive agent for $cust_main (agentnum ".
2064              $cust_main->agentnum. ")";
2065     my $agent = $agent_obj->agent;
2066     my $pkgs = join(', ',
2067       map { $_->part_pkg->pkg }
2068         grep { $_->pkgnum } $self->cust_bill_pkg
2069     );
2070     $description = eval qq("$dtempl");
2071   }
2072
2073   $cust_main->realtime_bop($method, $amount,
2074     'description' => $description,
2075     'invnum'      => $self->invnum,
2076 #this didn't do what we want, it just calls apply_payments_and_credits
2077 #    'apply'       => 1,
2078     'apply_to_invoice' => 1,
2079     %opt,
2080  #what we want:
2081  #this changes application behavior: auto payments
2082                         #triggered against a specific invoice are now applied
2083                         #to that invoice instead of oldest open.
2084                         #seem okay to me...
2085   );
2086
2087 }
2088
2089 =item batch_card OPTION => VALUE...
2090
2091 Adds a payment for this invoice to the pending credit card batch (see
2092 L<FS::cust_pay_batch>), or, if the B<realtime> option is set to a true value,
2093 runs the payment using a realtime gateway.
2094
2095 =cut
2096
2097 sub batch_card {
2098   my ($self, %options) = @_;
2099   my $cust_main = $self->cust_main;
2100
2101   $options{invnum} = $self->invnum;
2102   
2103   $cust_main->batch_card(%options);
2104 }
2105
2106 sub _agent_template {
2107   my $self = shift;
2108   $self->cust_main->agent_template;
2109 }
2110
2111 sub _agent_invoice_from {
2112   my $self = shift;
2113   $self->cust_main->agent_invoice_from;
2114 }
2115
2116 =item invoice_barcode DIR_OR_FALSE
2117
2118 Generates an invoice barcode PNG. If DIR_OR_FALSE is a true value,
2119 it is taken as the temp directory where the PNG file will be generated and the
2120 PNG file name is returned. Otherwise, the PNG image itself is returned.
2121
2122 =cut
2123
2124 sub invoice_barcode {
2125     my ($self, $dir) = (shift,shift);
2126     
2127     my $gdbar = new GD::Barcode('Code39',$self->invnum);
2128         die "can't create barcode: " . $GD::Barcode::errStr unless $gdbar;
2129     my $gd = $gdbar->plot(Height => 30);
2130
2131     if($dir) {
2132         my $bh = new File::Temp( TEMPLATE => 'barcode.'. $self->invnum. '.XXXXXXXX',
2133                            DIR      => $dir,
2134                            SUFFIX   => '.png',
2135                            UNLINK   => 0,
2136                          ) or die "can't open temp file: $!\n";
2137         print $bh $gd->png or die "cannot write barcode to file: $!\n";
2138         my $png_file = $bh->filename;
2139         close $bh;
2140         return $png_file;
2141     }
2142     return $gd->png;
2143 }
2144
2145 =item invnum_date_pretty
2146
2147 Returns a string with the invoice number and date, for example:
2148 "Invoice #54 (3/20/2008)".
2149
2150 Intended for back-end context, with regard to translation and date formatting.
2151
2152 =cut
2153
2154 #note: this uses _date_pretty_unlocalized because _date_pretty is too expensive
2155 # for backend use (and also does the wrong thing, localizing for end customer
2156 # instead of backoffice configured date format)
2157 sub invnum_date_pretty {
2158   my $self = shift;
2159   #$self->mt('Invoice #').
2160   'Invoice #'. #XXX should be translated ala web UI user (not invoice customer)
2161     $self->invnum. ' ('. $self->_date_pretty_unlocalized. ')';
2162 }
2163
2164 #sub _items_extra_usage_sections {
2165 #  my $self = shift;
2166 #  my $escape = shift;
2167 #
2168 #  my %sections = ();
2169 #
2170 #  my %usage_class =  map{ $_->classname, $_ } qsearch('usage_class', {});
2171 #  foreach my $cust_bill_pkg ( $self->cust_bill_pkg )
2172 #  {
2173 #    next unless $cust_bill_pkg->pkgnum > 0;
2174 #
2175 #    foreach my $section ( keys %usage_class ) {
2176 #
2177 #      my $usage = $cust_bill_pkg->usage($section);
2178 #
2179 #      next unless $usage && $usage > 0;
2180 #
2181 #      $sections{$section} ||= 0;
2182 #      $sections{$section} += $usage;
2183 #
2184 #    }
2185 #
2186 #  }
2187 #
2188 #  map { { 'description' => &{$escape}($_),
2189 #          'subtotal'    => $sections{$_},
2190 #          'summarized'  => '',
2191 #          'tax_section' => '',
2192 #        }
2193 #      }
2194 #    sort {$usage_class{$a}->weight <=> $usage_class{$b}->weight} keys %sections;
2195 #
2196 #}
2197
2198 sub _items_extra_usage_sections {
2199   my $self = shift;
2200   my $conf = $self->conf;
2201   my $escape = shift;
2202   my $format = shift;
2203
2204   my %sections = ();
2205   my %classnums = ();
2206   my %lines = ();
2207
2208   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2209
2210   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
2211   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2212     next unless $cust_bill_pkg->pkgnum > 0;
2213
2214     foreach my $classnum ( keys %usage_class ) {
2215       my $section = $usage_class{$classnum}->classname;
2216       $classnums{$section} = $classnum;
2217
2218       foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail($classnum) ) {
2219         my $amount = $detail->amount;
2220         next unless $amount && $amount > 0;
2221  
2222         $sections{$section} ||= { 'subtotal'=>0, 'calls'=>0, 'duration'=>0 };
2223         $sections{$section}{amount} += $amount;  #subtotal
2224         $sections{$section}{calls}++;
2225         $sections{$section}{duration} += $detail->duration;
2226
2227         my $desc = $detail->regionname; 
2228         my $description = $desc;
2229         $description = substr($desc, 0, $maxlength). '...'
2230           if $format eq 'latex' && length($desc) > $maxlength;
2231
2232         $lines{$section}{$desc} ||= {
2233           description     => &{$escape}($description),
2234           #pkgpart         => $part_pkg->pkgpart,
2235           pkgnum          => $cust_bill_pkg->pkgnum,
2236           ref             => '',
2237           amount          => 0,
2238           calls           => 0,
2239           duration        => 0,
2240           #unit_amount     => $cust_bill_pkg->unitrecur,
2241           quantity        => $cust_bill_pkg->quantity,
2242           product_code    => 'N/A',
2243           ext_description => [],
2244         };
2245
2246         $lines{$section}{$desc}{amount} += $amount;
2247         $lines{$section}{$desc}{calls}++;
2248         $lines{$section}{$desc}{duration} += $detail->duration;
2249
2250       }
2251     }
2252   }
2253
2254   my %sectionmap = ();
2255   foreach (keys %sections) {
2256     my $usage_class = $usage_class{$classnums{$_}};
2257     $sectionmap{$_} = { 'description' => &{$escape}($_),
2258                         'amount'    => $sections{$_}{amount},    #subtotal
2259                         'calls'       => $sections{$_}{calls},
2260                         'duration'    => $sections{$_}{duration},
2261                         'summarized'  => '',
2262                         'tax_section' => '',
2263                         'sort_weight' => $usage_class->weight,
2264                         ( $usage_class->format
2265                           ? ( map { $_ => $usage_class->$_($format) }
2266                               qw( description_generator header_generator total_generator total_line_generator )
2267                             )
2268                           : ()
2269                         ), 
2270                       };
2271   }
2272
2273   my @sections = sort { $a->{sort_weight} <=> $b->{sort_weight} }
2274                  values %sectionmap;
2275
2276   my @lines = ();
2277   foreach my $section ( keys %lines ) {
2278     foreach my $line ( keys %{$lines{$section}} ) {
2279       my $l = $lines{$section}{$line};
2280       $l->{section}     = $sectionmap{$section};
2281       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
2282       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
2283       push @lines, $l;
2284     }
2285   }
2286
2287   return(\@sections, \@lines);
2288
2289 }
2290
2291 sub _did_summary {
2292     my $self = shift;
2293     my $end = $self->_date;
2294
2295     # start at date of previous invoice + 1 second or 0 if no previous invoice
2296     my $start = $self->scalar_sql("SELECT max(_date) FROM cust_bill WHERE custnum = ? and invnum != ?",$self->custnum,$self->invnum);
2297     $start = 0 if !$start;
2298     $start++;
2299
2300     my $cust_main = $self->cust_main;
2301     my @pkgs = $cust_main->all_pkgs;
2302     my($num_activated,$num_deactivated,$num_portedin,$num_portedout,$minutes)
2303         = (0,0,0,0,0);
2304     my @seen = ();
2305     foreach my $pkg ( @pkgs ) {
2306         my @h_cust_svc = $pkg->h_cust_svc($end);
2307         foreach my $h_cust_svc ( @h_cust_svc ) {
2308             next if grep {$_ eq $h_cust_svc->svcnum} @seen;
2309             next unless $h_cust_svc->part_svc->svcdb eq 'svc_phone';
2310
2311             my $inserted = $h_cust_svc->date_inserted;
2312             my $deleted = $h_cust_svc->date_deleted;
2313             my $phone_inserted = $h_cust_svc->h_svc_x($inserted+5);
2314             my $phone_deleted;
2315             $phone_deleted =  $h_cust_svc->h_svc_x($deleted) if $deleted;
2316             
2317 # DID either activated or ported in; cannot be both for same DID simultaneously
2318             if ($inserted >= $start && $inserted <= $end && $phone_inserted
2319                 && (!$phone_inserted->lnp_status 
2320                     || $phone_inserted->lnp_status eq ''
2321                     || $phone_inserted->lnp_status eq 'native')) {
2322                 $num_activated++;
2323             }
2324             else { # this one not so clean, should probably move to (h_)svc_phone
2325                  local($FS::Record::qsearch_qualify_columns) = 0;
2326                  my $phone_portedin = qsearchs( 'h_svc_phone',
2327                       { 'svcnum' => $h_cust_svc->svcnum, 
2328                         'lnp_status' => 'portedin' },  
2329                       FS::h_svc_phone->sql_h_searchs($end),  
2330                     );
2331                  $num_portedin++ if $phone_portedin;
2332             }
2333
2334 # DID either deactivated or ported out; cannot be both for same DID simultaneously
2335             if($deleted >= $start && $deleted <= $end && $phone_deleted
2336                 && (!$phone_deleted->lnp_status 
2337                     || $phone_deleted->lnp_status ne 'portingout')) {
2338                 $num_deactivated++;
2339             } 
2340             elsif($deleted >= $start && $deleted <= $end && $phone_deleted 
2341                 && $phone_deleted->lnp_status 
2342                 && $phone_deleted->lnp_status eq 'portingout') {
2343                 $num_portedout++;
2344             }
2345
2346             # increment usage minutes
2347         if ( $phone_inserted ) {
2348             my @cdrs = $phone_inserted->get_cdrs('begin'=>$start,'end'=>$end,'billsec_sum'=>1);
2349             $minutes = $cdrs[0]->billsec_sum if scalar(@cdrs) == 1;
2350         }
2351         else {
2352             warn "WARNING: no matching h_svc_phone insert record for insert time $inserted, svcnum " . $h_cust_svc->svcnum;
2353         }
2354
2355             # don't look at this service again
2356             push @seen, $h_cust_svc->svcnum;
2357         }
2358     }
2359
2360     $minutes = sprintf("%d", $minutes);
2361     ("Activated: $num_activated  Ported-In: $num_portedin  Deactivated: "
2362         . "$num_deactivated  Ported-Out: $num_portedout ",
2363             "Total Minutes: $minutes");
2364 }
2365
2366 sub _items_accountcode_cdr {
2367     my $self = shift;
2368     my $escape = shift;
2369     my $format = shift;
2370
2371     my $section = { 'amount'        => 0,
2372                     'calls'         => 0,
2373                     'duration'      => 0,
2374                     'sort_weight'   => '',
2375                     'phonenum'      => '',
2376                     'description'   => 'Usage by Account Code',
2377                     'post_total'    => '',
2378                     'summarized'    => '',
2379                     'header'        => '',
2380                   };
2381     my @lines;
2382     my %accountcodes = ();
2383
2384     foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2385         next unless $cust_bill_pkg->pkgnum > 0;
2386
2387         my @header = $cust_bill_pkg->details_header;
2388         next unless scalar(@header);
2389         $section->{'header'} = join(',',@header);
2390
2391         foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
2392
2393             $section->{'header'} = $detail->formatted('format' => $format)
2394                 if($detail->detail eq $section->{'header'}); 
2395       
2396             my $accountcode = $detail->accountcode;
2397             next unless $accountcode;
2398
2399             my $amount = $detail->amount;
2400             next unless $amount && $amount > 0;
2401
2402             $accountcodes{$accountcode} ||= {
2403                     description => $accountcode,
2404                     pkgnum      => '',
2405                     ref         => '',
2406                     amount      => 0,
2407                     calls       => 0,
2408                     duration    => 0,
2409                     quantity    => '',
2410                     product_code => 'N/A',
2411                     section     => $section,
2412                     ext_description => [ $section->{'header'} ],
2413                     detail_temp => [],
2414             };
2415
2416             $section->{'amount'} += $amount;
2417             $accountcodes{$accountcode}{'amount'} += $amount;
2418             $accountcodes{$accountcode}{calls}++;
2419             $accountcodes{$accountcode}{duration} += $detail->duration;
2420             push @{$accountcodes{$accountcode}{detail_temp}}, $detail;
2421         }
2422     }
2423
2424     foreach my $l ( values %accountcodes ) {
2425         $l->{amount} = sprintf( "%.2f", $l->{amount} );
2426         my @sorted_detail = sort { $a->startdate <=> $b->startdate } @{$l->{detail_temp}};
2427         foreach my $sorted_detail ( @sorted_detail ) {
2428             push @{$l->{ext_description}}, $sorted_detail->formatted('format'=>$format);
2429         }
2430         delete $l->{detail_temp};
2431         push @lines, $l;
2432     }
2433
2434     my @sorted_lines = sort { $a->{'description'} <=> $b->{'description'} } @lines;
2435
2436     return ($section,\@sorted_lines);
2437 }
2438
2439 sub _items_svc_phone_sections {
2440   my $self = shift;
2441   my $conf = $self->conf;
2442   my $escape = shift;
2443   my $format = shift;
2444
2445   my %sections = ();
2446   my %classnums = ();
2447   my %lines = ();
2448
2449   my $maxlength = $conf->config('cust_bill-latex_lineitem_maxlength') || 50;
2450
2451   my %usage_class =  map { $_->classnum => $_ } qsearch( 'usage_class', {} );
2452   $usage_class{''} ||= new FS::usage_class { 'classname' => '', 'weight' => 0 };
2453
2454   foreach my $cust_bill_pkg ( $self->cust_bill_pkg ) {
2455     next unless $cust_bill_pkg->pkgnum > 0;
2456
2457     my @header = $cust_bill_pkg->details_header;
2458     next unless scalar(@header);
2459
2460     foreach my $detail ( $cust_bill_pkg->cust_bill_pkg_detail ) {
2461
2462       my $phonenum = $detail->phonenum;
2463       next unless $phonenum;
2464
2465       my $amount = $detail->amount;
2466       next unless $amount && $amount > 0;
2467
2468       $sections{$phonenum} ||= { 'amount'      => 0,
2469                                  'calls'       => 0,
2470                                  'duration'    => 0,
2471                                  'sort_weight' => -1,
2472                                  'phonenum'    => $phonenum,
2473                                 };
2474       $sections{$phonenum}{amount} += $amount;  #subtotal
2475       $sections{$phonenum}{calls}++;
2476       $sections{$phonenum}{duration} += $detail->duration;
2477
2478       my $desc = $detail->regionname; 
2479       my $description = $desc;
2480       $description = substr($desc, 0, $maxlength). '...'
2481         if $format eq 'latex' && length($desc) > $maxlength;
2482
2483       $lines{$phonenum}{$desc} ||= {
2484         description     => &{$escape}($description),
2485         #pkgpart         => $part_pkg->pkgpart,
2486         pkgnum          => '',
2487         ref             => '',
2488         amount          => 0,
2489         calls           => 0,
2490         duration        => 0,
2491         #unit_amount     => '',
2492         quantity        => '',
2493         product_code    => 'N/A',
2494         ext_description => [],
2495       };
2496
2497       $lines{$phonenum}{$desc}{amount} += $amount;
2498       $lines{$phonenum}{$desc}{calls}++;
2499       $lines{$phonenum}{$desc}{duration} += $detail->duration;
2500
2501       my $line = $usage_class{$detail->classnum}->classname;
2502       $sections{"$phonenum $line"} ||=
2503         { 'amount' => 0,
2504           'calls' => 0,
2505           'duration' => 0,
2506           'sort_weight' => $usage_class{$detail->classnum}->weight,
2507           'phonenum' => $phonenum,
2508           'header'  => [ @header ],
2509         };
2510       $sections{"$phonenum $line"}{amount} += $amount;  #subtotal
2511       $sections{"$phonenum $line"}{calls}++;
2512       $sections{"$phonenum $line"}{duration} += $detail->duration;
2513
2514       $lines{"$phonenum $line"}{$desc} ||= {
2515         description     => &{$escape}($description),
2516         #pkgpart         => $part_pkg->pkgpart,
2517         pkgnum          => '',
2518         ref             => '',
2519         amount          => 0,
2520         calls           => 0,
2521         duration        => 0,
2522         #unit_amount     => '',
2523         quantity        => '',
2524         product_code    => 'N/A',
2525         ext_description => [],
2526       };
2527
2528       $lines{"$phonenum $line"}{$desc}{amount} += $amount;
2529       $lines{"$phonenum $line"}{$desc}{calls}++;
2530       $lines{"$phonenum $line"}{$desc}{duration} += $detail->duration;
2531       push @{$lines{"$phonenum $line"}{$desc}{ext_description}},
2532            $detail->formatted('format' => $format);
2533
2534     }
2535   }
2536
2537   my %sectionmap = ();
2538   my $simple = new FS::usage_class { format => 'simple' }; #bleh
2539   foreach ( keys %sections ) {
2540     my @header = @{ $sections{$_}{header} || [] };
2541     my $usage_simple =
2542       new FS::usage_class { format => 'usage_'. (scalar(@header) || 6). 'col' };
2543     my $summary = $sections{$_}{sort_weight} < 0 ? 1 : 0;
2544     my $usage_class = $summary ? $simple : $usage_simple;
2545     my $ending = $summary ? ' usage charges' : '';
2546     my %gen_opt = ();
2547     unless ($summary) {
2548       $gen_opt{label} = [ map{ &{$escape}($_) } @header ];
2549     }
2550     $sectionmap{$_} = { 'description' => &{$escape}($_. $ending),
2551                         'amount'    => $sections{$_}{amount},    #subtotal
2552                         'calls'       => $sections{$_}{calls},
2553                         'duration'    => $sections{$_}{duration},
2554                         'summarized'  => '',
2555                         'tax_section' => '',
2556                         'phonenum'    => $sections{$_}{phonenum},
2557                         'sort_weight' => $sections{$_}{sort_weight},
2558                         'post_total'  => $summary, #inspire pagebreak
2559                         (
2560                           ( map { $_ => $usage_class->$_($format, %gen_opt) }
2561                             qw( description_generator
2562                                 header_generator
2563                                 total_generator
2564                                 total_line_generator
2565                               )
2566                           )
2567                         ), 
2568                       };
2569   }
2570
2571   my @sections = sort { $a->{phonenum} cmp $b->{phonenum} ||
2572                         $a->{sort_weight} <=> $b->{sort_weight}
2573                       }
2574                  values %sectionmap;
2575
2576   my @lines = ();
2577   foreach my $section ( keys %lines ) {
2578     foreach my $line ( keys %{$lines{$section}} ) {
2579       my $l = $lines{$section}{$line};
2580       $l->{section}     = $sectionmap{$section};
2581       $l->{amount}      = sprintf( "%.2f", $l->{amount} );
2582       #$l->{unit_amount} = sprintf( "%.2f", $l->{unit_amount} );
2583       push @lines, $l;
2584     }
2585   }
2586   
2587   if($conf->exists('phone_usage_class_summary')) { 
2588       # this only works with Latex
2589       my @newlines;
2590       my @newsections;
2591
2592       # after this, we'll have only two sections per DID:
2593       # Calls Summary and Calls Detail
2594       foreach my $section ( @sections ) {
2595         if($section->{'post_total'}) {
2596             $section->{'description'} = 'Calls Summary: '.$section->{'phonenum'};
2597             $section->{'total_line_generator'} = sub { '' };
2598             $section->{'total_generator'} = sub { '' };
2599             $section->{'header_generator'} = sub { '' };
2600             $section->{'description_generator'} = '';
2601             push @newsections, $section;
2602             my %calls_detail = %$section;
2603             $calls_detail{'post_total'} = '';
2604             $calls_detail{'sort_weight'} = '';
2605             $calls_detail{'description_generator'} = sub { '' };
2606             $calls_detail{'header_generator'} = sub {
2607                 return ' & Date/Time & Called Number & Duration & Price'
2608                     if $format eq 'latex';
2609                 '';
2610             };
2611             $calls_detail{'description'} = 'Calls Detail: '
2612                                                     . $section->{'phonenum'};
2613             push @newsections, \%calls_detail;  
2614         }
2615       }
2616
2617       # after this, each usage class is collapsed/summarized into a single
2618       # line under the Calls Summary section
2619       foreach my $newsection ( @newsections ) {
2620         if($newsection->{'post_total'}) { # this means Calls Summary
2621             foreach my $section ( @sections ) {
2622                 next unless ($section->{'phonenum'} eq $newsection->{'phonenum'} 
2623                                 && !$section->{'post_total'});
2624                 my $newdesc = $section->{'description'};
2625                 my $tn = $section->{'phonenum'};
2626                 $newdesc =~ s/$tn//g;
2627                 my $line = {  ext_description => [],
2628                               pkgnum => '',
2629                               ref => '',
2630                               quantity => '',
2631                               calls => $section->{'calls'},
2632                               section => $newsection,
2633                               duration => $section->{'duration'},
2634                               description => $newdesc,
2635                               amount => sprintf("%.2f",$section->{'amount'}),
2636                               product_code => 'N/A',
2637                             };
2638                 push @newlines, $line;
2639             }
2640         }
2641       }
2642
2643       # after this, Calls Details is populated with all CDRs
2644       foreach my $newsection ( @newsections ) {
2645         if(!$newsection->{'post_total'}) { # this means Calls Details
2646             foreach my $line ( @lines ) {
2647                 next unless (scalar(@{$line->{'ext_description'}}) &&
2648                         $line->{'section'}->{'phonenum'} eq $newsection->{'phonenum'}
2649                             );
2650                 my @extdesc = @{$line->{'ext_description'}};
2651                 my @newextdesc;
2652                 foreach my $extdesc ( @extdesc ) {
2653                     $extdesc =~ s/scriptsize/normalsize/g if $format eq 'latex';
2654                     push @newextdesc, $extdesc;
2655                 }
2656                 $line->{'ext_description'} = \@newextdesc;
2657                 $line->{'section'} = $newsection;
2658                 push @newlines, $line;
2659             }
2660         }
2661       }
2662
2663       return(\@newsections, \@newlines);
2664   }
2665
2666   return(\@sections, \@lines);
2667
2668 }
2669
2670 =sub _items_usage_class_summary OPTIONS
2671
2672 Returns a list of detail items summarizing the usage charges on this 
2673 invoice.  Each one will have 'amount', 'description' (the usage charge name),
2674 and 'usage_classnum'.
2675
2676 OPTIONS can include 'escape' (a function to escape the descriptions).
2677
2678 =cut
2679
2680 sub _items_usage_class_summary {
2681   my $self = shift;
2682   my %opt = @_;
2683
2684   my $escape = $opt{escape} || sub { $_[0] };
2685   my $invnum = $self->invnum;
2686   my @classes = qsearch({
2687       'table'     => 'usage_class',
2688       'select'    => 'classnum, classname, SUM(amount) AS amount',
2689       'addl_from' => ' LEFT JOIN cust_bill_pkg_detail USING (classnum)' .
2690                      ' LEFT JOIN cust_bill_pkg USING (billpkgnum)',
2691       'extra_sql' => " WHERE cust_bill_pkg.invnum = $invnum".
2692                      ' GROUP BY classnum, classname, weight'.
2693                      ' HAVING (usage_class.disabled IS NULL OR SUM(amount) > 0)'.
2694                      ' ORDER BY weight ASC',
2695   });
2696   my @l;
2697   my $section = {
2698     description   => &{$escape}($self->mt('Usage Summary')),
2699     no_subtotal   => 1,
2700     usage_section => 1,
2701   };
2702   foreach my $class (@classes) {
2703     push @l, {
2704       'description'     => &{$escape}($class->classname),
2705       'amount'          => sprintf('%.2f', $class->amount),
2706       'usage_classnum'  => $class->classnum,
2707       'section'         => $section,
2708     };
2709   }
2710   return @l;
2711 }
2712
2713 sub _items_previous {
2714   my $self = shift;
2715   my $conf = $self->conf;
2716   my $cust_main = $self->cust_main;
2717   my( $pr_total, @pr_cust_bill ) = $self->previous; #previous balance
2718   my @b = ();
2719   foreach ( @pr_cust_bill ) {
2720     my $date = $conf->exists('invoice_show_prior_due_date')
2721                ? 'due '. $_->due_date2str('short')
2722                : $self->time2str_local('short', $_->_date);
2723     push @b, {
2724       'description' => $self->mt('Previous Balance, Invoice #'). $_->invnum. " ($date)",
2725       #'pkgpart'     => 'N/A',
2726       'pkgnum'      => 'N/A',
2727       'amount'      => sprintf("%.2f", $_->owed),
2728     };
2729   }
2730   @b;
2731
2732   #{
2733   #    'description'     => 'Previous Balance',
2734   #    #'pkgpart'         => 'N/A',
2735   #    'pkgnum'          => 'N/A',
2736   #    'amount'          => sprintf("%10.2f", $pr_total ),
2737   #    'ext_description' => [ map {
2738   #                                 "Invoice ". $_->invnum.
2739   #                                 " (". time2str("%x",$_->_date). ") ".
2740   #                                 sprintf("%10.2f", $_->owed)
2741   #                         } @pr_cust_bill ],
2742
2743   #};
2744 }
2745
2746 sub _items_credits {
2747   my( $self, %opt ) = @_;
2748   my $trim_len = $opt{'trim_len'} || 60;
2749
2750   my @b;
2751   #credits
2752   my @objects;
2753   if ( $self->conf->exists('previous_balance-payments_since') ) {
2754     if ( $opt{'template'} eq 'statement' ) {
2755       # then the current bill is a "statement" (i.e. an invoice sent as
2756       # a payment receipt)
2757       # and in that case we want to see payments on or after THIS invoice
2758       @objects = qsearch('cust_credit', {
2759           'custnum' => $self->custnum,
2760           '_date'   => {op => '>=', value => $self->_date},
2761       });
2762     } else {
2763       my $date = 0;
2764       $date = $self->previous_bill->_date if $self->previous_bill;
2765       @objects = qsearch('cust_credit', {
2766           'custnum' => $self->custnum,
2767           '_date'   => {op => '>=', value => $date},
2768       });
2769     }
2770   } else {
2771     @objects = $self->cust_credited;
2772   }
2773
2774   foreach my $obj ( @objects ) {
2775     my $cust_credit = $obj->isa('FS::cust_credit') ? $obj : $obj->cust_credit;
2776
2777     my $reason = substr($cust_credit->reason, 0, $trim_len);
2778     $reason .= '...' if length($reason) < length($cust_credit->reason);
2779     $reason = " ($reason) " if $reason;
2780
2781     push @b, {
2782       #'description' => 'Credit ref\#'. $_->crednum.
2783       #                 " (". time2str("%x",$_->cust_credit->_date) .")".
2784       #                 $reason,
2785       'description' => $self->mt('Credit applied').' '.
2786                        $self->time2str_local('short', $obj->_date). $reason,
2787       'amount'      => sprintf("%.2f",$obj->amount),
2788     };
2789   }
2790
2791   @b;
2792
2793 }
2794
2795 sub _items_payments {
2796   my $self = shift;
2797   my %opt = @_;
2798
2799   my @b;
2800   my $detailed = $self->conf->exists('invoice_payment_details');
2801   my @objects;
2802   if ( $self->conf->exists('previous_balance-payments_since') ) {
2803     # then show payments dated on/after the previous bill...
2804     if ( $opt{'template'} eq 'statement' ) {
2805       # then the current bill is a "statement" (i.e. an invoice sent as
2806       # a payment receipt)
2807       # and in that case we want to see payments on or after THIS invoice
2808       @objects = qsearch('cust_pay', {
2809           'custnum' => $self->custnum,
2810           '_date'   => {op => '>=', value => $self->_date},
2811       });
2812     } else {
2813       # the normal case: payments on or after the previous invoice
2814       my $date = 0;
2815       $date = $self->previous_bill->_date if $self->previous_bill;
2816       @objects = qsearch('cust_pay', {
2817         'custnum' => $self->custnum,
2818         '_date'   => {op => '>=', value => $date},
2819       });
2820       # and before the current bill...
2821       @objects = grep { $_->_date < $self->_date } @objects;
2822     }
2823   } else {
2824     @objects = $self->cust_bill_pay;
2825   }
2826
2827   foreach my $obj (@objects) {
2828     my $cust_pay = $obj->isa('FS::cust_pay') ? $obj : $obj->cust_pay;
2829     my $desc = $self->mt('Payment received').' '.
2830                $self->time2str_local('short', $cust_pay->_date );
2831     $desc .= $self->mt(' via ') .
2832              $cust_pay->payby_payinfo_pretty( $self->cust_main->locale )
2833       if $detailed;
2834
2835     push @b, {
2836       'description' => $desc,
2837       'amount'      => sprintf("%.2f", $obj->amount )
2838     };
2839   }
2840
2841   @b;
2842
2843 }
2844
2845 =item call_details [ OPTION => VALUE ... ]
2846
2847 Returns an array of CSV strings representing the call details for this invoice
2848 The only option available is the boolean prepend_billed_number
2849
2850 =cut
2851
2852 sub call_details {
2853   my ($self, %opt) = @_;
2854
2855   my $format_function = sub { shift };
2856
2857   if ($opt{prepend_billed_number}) {
2858     $format_function = sub {
2859       my $detail = shift;
2860       my $row = shift;
2861
2862       $row->amount ? $row->phonenum. ",". $detail : '"Billed number",'. $detail;
2863       
2864     };
2865   }
2866
2867   my @details = map { $_->details( 'format_function' => $format_function,
2868                                    'escape_function' => sub{ return() },
2869                                  )
2870                     }
2871                   grep { $_->pkgnum }
2872                   $self->cust_bill_pkg;
2873   my $header = $details[0];
2874   ( $header, grep { $_ ne $header } @details );
2875 }
2876
2877
2878 =back
2879
2880 =head1 SUBROUTINES
2881
2882 =over 4
2883
2884 =item process_reprint
2885
2886 =cut
2887
2888 sub process_reprint {
2889   process_re_X('print', @_);
2890 }
2891
2892 =item process_reemail
2893
2894 =cut
2895
2896 sub process_reemail {
2897   process_re_X('email', @_);
2898 }
2899
2900 =item process_refax
2901
2902 =cut
2903
2904 sub process_refax {
2905   process_re_X('fax', @_);
2906 }
2907
2908 =item process_reftp
2909
2910 =cut
2911
2912 sub process_reftp {
2913   process_re_X('ftp', @_);
2914 }
2915
2916 =item respool
2917
2918 =cut
2919
2920 sub process_respool {
2921   process_re_X('spool', @_);
2922 }
2923
2924 use Data::Dumper;
2925 sub process_re_X {
2926   my( $method, $job ) = ( shift, shift );
2927   warn "$me process_re_X $method for job $job\n" if $DEBUG;
2928
2929   my $param = shift;
2930   warn Dumper($param) if $DEBUG;
2931
2932   re_X(
2933     $method,
2934     $job,
2935     %$param,
2936   );
2937
2938 }
2939
2940 sub re_X {
2941   # spool_invoice ftp_invoice fax_invoice print_invoice
2942   my($method, $job, %param ) = @_;
2943   if ( $DEBUG ) {
2944     warn "re_X $method for job $job with param:\n".
2945          join( '', map { "  $_ => ". $param{$_}. "\n" } keys %param );
2946   }
2947
2948   #some false laziness w/search/cust_bill.html
2949   my $distinct = '';
2950   my $orderby = 'ORDER BY cust_bill._date';
2951
2952   my $extra_sql = ' WHERE '. FS::cust_bill->search_sql_where(\%param);
2953
2954   my $addl_from = 'LEFT JOIN cust_main USING ( custnum )';
2955      
2956   my @cust_bill = qsearch( {
2957     #'select'    => "cust_bill.*",
2958     'table'     => 'cust_bill',
2959     'addl_from' => $addl_from,
2960     'hashref'   => {},
2961     'extra_sql' => $extra_sql,
2962     'order_by'  => $orderby,
2963     'debug' => 1,
2964   } );
2965
2966   $method .= '_invoice' unless $method eq 'email' || $method eq 'print';
2967
2968   warn " $me re_X $method: ". scalar(@cust_bill). " invoices found\n"
2969     if $DEBUG;
2970
2971   my( $num, $last, $min_sec ) = (0, time, 5); #progresbar foo
2972   foreach my $cust_bill ( @cust_bill ) {
2973     $cust_bill->$method();
2974
2975     if ( $job ) { #progressbar foo
2976       $num++;
2977       if ( time - $min_sec > $last ) {
2978         my $error = $job->update_statustext(
2979           int( 100 * $num / scalar(@cust_bill) )
2980         );
2981         die $error if $error;
2982         $last = time;
2983       }
2984     }
2985
2986   }
2987
2988 }
2989
2990 sub API_getinfo {
2991   my $self = shift;
2992   +{ ( map { $_=>$self->$_ } $self->fields ),
2993      'owed' => $self->owed,
2994      #XXX last payment applied date
2995    };
2996 }
2997
2998 =back
2999
3000 =head1 CLASS METHODS
3001
3002 =over 4
3003
3004 =item owed_sql
3005
3006 Returns an SQL fragment to retreive the amount owed (charged minus credited and paid).
3007
3008 =cut
3009
3010 sub owed_sql {
3011   my ($class, $start, $end) = @_;
3012   'charged - '. 
3013     $class->paid_sql($start, $end). ' - '. 
3014     $class->credited_sql($start, $end);
3015 }
3016
3017 =item net_sql
3018
3019 Returns an SQL fragment to retreive the net amount (charged minus credited).
3020
3021 =cut
3022
3023 sub net_sql {
3024   my ($class, $start, $end) = @_;
3025   'charged - '. $class->credited_sql($start, $end);
3026 }
3027
3028 =item paid_sql
3029
3030 Returns an SQL fragment to retreive the amount paid against this invoice.
3031
3032 =cut
3033
3034 sub paid_sql {
3035   my ($class, $start, $end) = @_;
3036   $start &&= "AND cust_bill_pay._date <= $start";
3037   $end   &&= "AND cust_bill_pay._date > $end";
3038   $start = '' unless defined($start);
3039   $end   = '' unless defined($end);
3040   "( SELECT COALESCE(SUM(amount),0) FROM cust_bill_pay
3041        WHERE cust_bill.invnum = cust_bill_pay.invnum $start $end  )";
3042 }
3043
3044 =item credited_sql
3045
3046 Returns an SQL fragment to retreive the amount credited against this invoice.
3047
3048 =cut
3049
3050 sub credited_sql {
3051   my ($class, $start, $end) = @_;
3052   $start &&= "AND cust_credit_bill._date <= $start";
3053   $end   &&= "AND cust_credit_bill._date >  $end";
3054   $start = '' unless defined($start);
3055   $end   = '' unless defined($end);
3056   "( SELECT COALESCE(SUM(amount),0) FROM cust_credit_bill
3057        WHERE cust_bill.invnum = cust_credit_bill.invnum $start $end  )";
3058 }
3059
3060 =item due_date_sql
3061
3062 Returns an SQL fragment to retrieve the due date of an invoice.
3063 Currently only supported on PostgreSQL.
3064
3065 =cut
3066
3067 sub due_date_sql {
3068   die "don't use: doesn't account for agent-specific invoice_default_terms";
3069
3070   #we're passed a $conf but not a specific customer (that's in the query), so
3071   # to make this work we'd need an agentnum-aware "condition_sql_conf" like
3072   # "condition_sql_option" that retreives a conf value with SQL in an agent-
3073   # aware fashion
3074
3075   my $conf = new FS::Conf;
3076 'COALESCE(
3077   SUBSTRING(
3078     COALESCE(
3079       cust_bill.invoice_terms,
3080       cust_main.invoice_terms,
3081       \''.($conf->config('invoice_default_terms') || '').'\'
3082     ), E\'Net (\\\\d+)\'
3083   )::INTEGER, 0
3084 ) * 86400 + cust_bill._date'
3085 }
3086
3087 =back
3088
3089 =head1 BUGS
3090
3091 The delete method.
3092
3093 =head1 SEE ALSO
3094
3095 L<FS::Record>, L<FS::cust_main>, L<FS::cust_bill_pay>, L<FS::cust_pay>,
3096 L<FS::cust_bill_pkg>, L<FS::cust_bill_credit>, schema.html from the base
3097 documentation.
3098
3099 =cut
3100
3101 1;
3102