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