80077aa8b9659db7591f03a00014a0102cd25c44
[freeside.git] / FS / FS / cust_bill_pkg.pm
1 package FS::cust_bill_pkg;
2
3 use strict;
4 use vars qw( @ISA $DEBUG $me );
5 use Carp;
6 use FS::Record qw( qsearch qsearchs dbdef dbh );
7 use FS::cust_main_Mixin;
8 use FS::cust_pkg;
9 use FS::part_pkg;
10 use FS::cust_bill;
11 use FS::cust_bill_pkg_detail;
12 use FS::cust_bill_pkg_display;
13 use FS::cust_bill_pay_pkg;
14 use FS::cust_credit_bill_pkg;
15 use FS::cust_tax_exempt_pkg;
16 use FS::cust_bill_pkg_tax_location;
17 use FS::cust_bill_pkg_tax_rate_location;
18 use FS::cust_tax_adjustment;
19
20 @ISA = qw( FS::cust_main_Mixin FS::Record );
21
22 $DEBUG = 0;
23 $me = '[FS::cust_bill_pkg]';
24
25 =head1 NAME
26
27 FS::cust_bill_pkg - Object methods for cust_bill_pkg records
28
29 =head1 SYNOPSIS
30
31   use FS::cust_bill_pkg;
32
33   $record = new FS::cust_bill_pkg \%hash;
34   $record = new FS::cust_bill_pkg { 'column' => 'value' };
35
36   $error = $record->insert;
37
38   $error = $record->check;
39
40 =head1 DESCRIPTION
41
42 An FS::cust_bill_pkg object represents an invoice line item.
43 FS::cust_bill_pkg inherits from FS::Record.  The following fields are currently
44 supported:
45
46 =over 4
47
48 =item billpkgnum
49
50 primary key
51
52 =item invnum
53
54 invoice (see L<FS::cust_bill>)
55
56 =item pkgnum
57
58 package (see L<FS::cust_pkg>) or 0 for the special virtual sales tax package, or -1 for the virtual line item (itemdesc is used for the line)
59
60 =item pkgpart_override
61
62 optional package definition (see L<FS::part_pkg>) override
63
64 =item setup
65
66 setup fee
67
68 =item recur
69
70 recurring fee
71
72 =item sdate
73
74 starting date of recurring fee
75
76 =item edate
77
78 ending date of recurring fee
79
80 =item itemdesc
81
82 Line item description (overrides normal package description)
83
84 =item quantity
85
86 If not set, defaults to 1
87
88 =item unitsetup
89
90 If not set, defaults to setup
91
92 =item unitrecur
93
94 If not set, defaults to recur
95
96 =item hidden
97
98 If set to Y, indicates data should not appear as separate line item on invoice
99
100 =back
101
102 sdate and edate are specified as UNIX timestamps; see L<perlfunc/"time">.  Also
103 see L<Time::Local> and L<Date::Parse> for conversion functions.
104
105 =head1 METHODS
106
107 =over 4
108
109 =item new HASHREF
110
111 Creates a new line item.  To add the line item to the database, see
112 L<"insert">.  Line items are normally created by calling the bill method of a
113 customer object (see L<FS::cust_main>).
114
115 =cut
116
117 sub table { 'cust_bill_pkg'; }
118
119 =item insert
120
121 Adds this line item to the database.  If there is an error, returns the error,
122 otherwise returns false.
123
124 =cut
125
126 sub insert {
127   my $self = shift;
128
129   local $SIG{HUP} = 'IGNORE';
130   local $SIG{INT} = 'IGNORE';
131   local $SIG{QUIT} = 'IGNORE';
132   local $SIG{TERM} = 'IGNORE';
133   local $SIG{TSTP} = 'IGNORE';
134   local $SIG{PIPE} = 'IGNORE';
135
136   my $oldAutoCommit = $FS::UID::AutoCommit;
137   local $FS::UID::AutoCommit = 0;
138   my $dbh = dbh;
139
140   my $error = $self->SUPER::insert;
141   if ( $error ) {
142     $dbh->rollback if $oldAutoCommit;
143     return $error;
144   }
145
146   if ( $self->get('details') ) {
147     foreach my $detail ( @{$self->get('details')} ) {
148       my $cust_bill_pkg_detail = new FS::cust_bill_pkg_detail {
149         'billpkgnum' => $self->billpkgnum,
150         'format'     => (ref($detail) ? $detail->[0] : '' ),
151         'detail'     => (ref($detail) ? $detail->[1] : $detail ),
152         'amount'     => (ref($detail) ? $detail->[2] : '' ),
153         'classnum'   => (ref($detail) ? $detail->[3] : '' ),
154         'phonenum'   => (ref($detail) ? $detail->[4] : '' ),
155         'duration'   => (ref($detail) ? $detail->[5] : '' ),
156         'regionname' => (ref($detail) ? $detail->[6] : '' ),
157       };
158       $error = $cust_bill_pkg_detail->insert;
159       if ( $error ) {
160         $dbh->rollback if $oldAutoCommit;
161         return "error inserting cust_bill_pkg_detail: $error";
162       }
163     }
164   }
165
166   if ( $self->get('display') ) {
167     foreach my $cust_bill_pkg_display ( @{ $self->get('display') } ) {
168       $cust_bill_pkg_display->billpkgnum($self->billpkgnum);
169       $error = $cust_bill_pkg_display->insert;
170       if ( $error ) {
171         $dbh->rollback if $oldAutoCommit;
172         return "error inserting cust_bill_pkg_display: $error";
173       }
174     }
175   }
176
177   if ( $self->_cust_tax_exempt_pkg ) {
178     foreach my $cust_tax_exempt_pkg ( @{$self->_cust_tax_exempt_pkg} ) {
179       $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
180       $error = $cust_tax_exempt_pkg->insert;
181       if ( $error ) {
182         $dbh->rollback if $oldAutoCommit;
183         return "error inserting cust_tax_exempt_pkg: $error";
184       }
185     }
186   }
187
188   my $tax_location = $self->get('cust_bill_pkg_tax_location');
189   if ( $tax_location ) {
190     foreach my $cust_bill_pkg_tax_location ( @$tax_location ) {
191       $cust_bill_pkg_tax_location->billpkgnum($self->billpkgnum);
192       $error = $cust_bill_pkg_tax_location->insert;
193       if ( $error ) {
194         $dbh->rollback if $oldAutoCommit;
195         return "error inserting cust_bill_pkg_tax_location: $error";
196       }
197     }
198   }
199
200   my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
201   if ( $tax_rate_location ) {
202     foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
203       $cust_bill_pkg_tax_rate_location->billpkgnum($self->billpkgnum);
204       $error = $cust_bill_pkg_tax_rate_location->insert;
205       if ( $error ) {
206         $dbh->rollback if $oldAutoCommit;
207         return "error inserting cust_bill_pkg_tax_rate_location: $error";
208       }
209     }
210   }
211
212   my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
213   if ( $cust_tax_adjustment ) {
214     $cust_tax_adjustment->billpkgnum($self->billpkgnum);
215     $error = $cust_tax_adjustment->replace;
216     if ( $error ) {
217       $dbh->rollback if $oldAutoCommit;
218       return "error replacing cust_tax_adjustment: $error";
219     }
220   }
221
222   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
223   '';
224
225 }
226
227 =item delete
228
229 Not recommended.
230
231 =cut
232
233 sub delete {
234   my $self = shift;
235
236   local $SIG{HUP} = 'IGNORE';
237   local $SIG{INT} = 'IGNORE';
238   local $SIG{QUIT} = 'IGNORE';
239   local $SIG{TERM} = 'IGNORE';
240   local $SIG{TSTP} = 'IGNORE';
241   local $SIG{PIPE} = 'IGNORE';
242
243   my $oldAutoCommit = $FS::UID::AutoCommit;
244   local $FS::UID::AutoCommit = 0;
245   my $dbh = dbh;
246
247   foreach my $table (qw(
248     cust_bill_pkg_detail
249     cust_bill_pkg_display
250     cust_bill_pkg_tax_location
251     cust_bill_pkg_tax_rate_location
252     cust_tax_exempt_pkg
253     cust_bill_pay_pkg
254     cust_credit_bill_pkg
255   )) {
256
257     foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
258       my $error = $linked->delete;
259       if ( $error ) {
260         $dbh->rollback if $oldAutoCommit;
261         return $error;
262       }
263     }
264
265   }
266
267   foreach my $cust_tax_adjustment (
268     qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
269   ) {
270     $cust_tax_adjustment->billpkgnum(''); #NULL
271     my $error = $cust_tax_adjustment->replace;
272     if ( $error ) {
273       $dbh->rollback if $oldAutoCommit;
274       return $error;
275     }
276   }
277
278   my $error = $self->SUPER::delete(@_);
279   if ( $error ) {
280     $dbh->rollback if $oldAutoCommit;
281     return $error;
282   }
283
284   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
285
286   '';
287
288 }
289
290 #alas, bin/follow-tax-rename
291 #
292 #=item replace OLD_RECORD
293 #
294 #Currently unimplemented.  This would be even more of an accounting nightmare
295 #than deleteing the items.  Just don't do it.
296 #
297 #=cut
298 #
299 #sub replace {
300 #  return "Can't modify cust_bill_pkg records!";
301 #}
302
303 =item check
304
305 Checks all fields to make sure this is a valid line item.  If there is an
306 error, returns the error, otherwise returns false.  Called by the insert
307 method.
308
309 =cut
310
311 sub check {
312   my $self = shift;
313
314   my $error =
315          $self->ut_numbern('billpkgnum')
316       || $self->ut_snumber('pkgnum')
317       || $self->ut_number('invnum')
318       || $self->ut_money('setup')
319       || $self->ut_money('recur')
320       || $self->ut_numbern('sdate')
321       || $self->ut_numbern('edate')
322       || $self->ut_textn('itemdesc')
323       || $self->ut_textn('itemcomment')
324       || $self->ut_enum('hidden', [ '', 'Y' ])
325   ;
326   return $error if $error;
327
328   #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
329   if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
330     return "Unknown pkgnum ". $self->pkgnum
331       unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
332   }
333
334   return "Unknown invnum"
335     unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
336
337   $self->SUPER::check;
338 }
339
340 =item cust_pkg
341
342 Returns the package (see L<FS::cust_pkg>) for this invoice line item.
343
344 =cut
345
346 sub cust_pkg {
347   my $self = shift;
348   carp "$me $self -> cust_pkg" if $DEBUG;
349   qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
350 }
351
352 =item part_pkg
353
354 Returns the package definition for this invoice line item.
355
356 =cut
357
358 sub part_pkg {
359   my $self = shift;
360   if ( $self->pkgpart_override ) {
361     qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } );
362   } else {
363     my $part_pkg;
364     my $cust_pkg = $self->cust_pkg;
365     $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
366     $part_pkg;
367   }
368 }
369
370 =item cust_bill
371
372 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
373
374 =cut
375
376 sub cust_bill {
377   my $self = shift;
378   qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
379 }
380
381 =item previous_cust_bill_pkg
382
383 Returns the previous cust_bill_pkg for this package, if any.
384
385 =cut
386
387 sub previous_cust_bill_pkg {
388   my $self = shift;
389   return unless $self->sdate;
390   qsearchs({
391     'table'    => 'cust_bill_pkg',
392     'hashref'  => { 'pkgnum' => $self->pkgnum,
393                     'sdate'  => { op=>'<', value=>$self->sdate },
394                   },
395     'order_by' => 'ORDER BY sdate DESC LIMIT 1',
396   });
397 }
398
399 =item details [ OPTION => VALUE ... ]
400
401 Returns an array of detail information for the invoice line item.
402
403 Currently available options are: I<format> I<escape_function>
404
405 If I<format> is set to html or latex then the array members are improved
406 for tabular appearance in those environments if possible.
407
408 If I<escape_function> is set then the array members are processed by this
409 function before being returned.
410
411 =cut
412
413 sub details {
414   my ( $self, %opt ) = @_;
415   my $format = $opt{format} || '';
416   my $escape_function = $opt{escape_function} || sub { shift };
417   return () unless defined dbdef->table('cust_bill_pkg_detail');
418
419   eval "use Text::CSV_XS;";
420   die $@ if $@;
421   my $csv = new Text::CSV_XS;
422
423   my $format_sub = sub { my $detail = shift;
424                          $csv->parse($detail) or return "can't parse $detail";
425                          join(' - ', map { &$escape_function($_) }
426                                      $csv->fields
427                              );
428                        };
429
430   $format_sub = sub { my $detail = shift;
431                       $csv->parse($detail) or return "can't parse $detail";
432                       join('</TD><TD>', map { &$escape_function($_) }
433                                         $csv->fields
434                           );
435                     }
436     if $format eq 'html';
437
438   $format_sub = sub { my $detail = shift;
439                       $csv->parse($detail) or return "can't parse $detail";
440                       #join(' & ', map { '\small{'. &$escape_function($_). '}' }
441                       #            $csv->fields );
442                       my $result = '';
443                       my $column = 1;
444                       foreach ($csv->fields) {
445                         $result .= ' & ' if $column > 1;
446                         if ($column > 6) {                     # KLUDGE ALERT!
447                           $result .= '\multicolumn{1}{l}{\scriptsize{'.
448                                      &$escape_function($_). '}}';
449                         }else{
450                           $result .= '\scriptsize{'.  &$escape_function($_). '}';
451                         }
452                         $column++;
453                       }
454                       $result;
455                     }
456     if $format eq 'latex';
457
458   $format_sub = $opt{format_function} if $opt{format_function};
459
460   map { ( $_->format eq 'C'
461           ? &{$format_sub}( $_->detail, $_ )
462           : &{$escape_function}( $_->detail )
463         )
464       }
465     qsearch ({ 'table'    => 'cust_bill_pkg_detail',
466                'hashref'  => { 'billpkgnum' => $self->billpkgnum },
467                'order_by' => 'ORDER BY detailnum',
468             });
469     #qsearch ( 'cust_bill_pkg_detail', { 'lineitemnum' => $self->lineitemnum });
470 }
471
472 =item desc
473
474 Returns a description for this line item.  For typical line items, this is the
475 I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
476 For one-shot line items and named taxes, it is the I<itemdesc> field of this
477 line item, and for generic taxes, simply returns "Tax".
478
479 =cut
480
481 sub desc {
482   my $self = shift;
483
484   if ( $self->pkgnum > 0 ) {
485     $self->itemdesc || $self->part_pkg->pkg;
486   } else {
487     my $desc = $self->itemdesc || 'Tax';
488     $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
489     $desc;
490   }
491 }
492
493 =item owed_setup
494
495 Returns the amount owed (still outstanding) on this line item's setup fee,
496 which is the amount of the line item minus all payment applications (see
497 L<FS::cust_bill_pay_pkg> and credit applications (see
498 L<FS::cust_credit_bill_pkg>).
499
500 =cut
501
502 sub owed_setup {
503   my $self = shift;
504   $self->owed('setup', @_);
505 }
506
507 =item owed_recur
508
509 Returns the amount owed (still outstanding) on this line item's recurring fee,
510 which is the amount of the line item minus all payment applications (see
511 L<FS::cust_bill_pay_pkg> and credit applications (see
512 L<FS::cust_credit_bill_pkg>).
513
514 =cut
515
516 sub owed_recur {
517   my $self = shift;
518   $self->owed('recur', @_);
519 }
520
521 # modeled after cust_bill::owed...
522 sub owed {
523   my( $self, $field ) = @_;
524   my $balance = $self->$field();
525   $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
526   $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
527   $balance = sprintf( '%.2f', $balance );
528   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
529   $balance;
530 }
531
532 sub cust_bill_pay_pkg {
533   my( $self, $field ) = @_;
534   qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
535                                   'setuprecur' => $field,
536                                 }
537          );
538 }
539
540 sub cust_credit_bill_pkg {
541   my( $self, $field ) = @_;
542   qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
543                                      'setuprecur' => $field,
544                                    }
545          );
546 }
547
548 =item units
549
550 Returns the number of billing units (for tax purposes) represented by this,
551 line item.
552
553 =cut
554
555 sub units {
556   my $self = shift;
557   $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
558 }
559
560 =item quantity
561
562 =cut
563
564 sub quantity {
565   my( $self, $value ) = @_;
566   if ( defined($value) ) {
567     $self->setfield('quantity', $value);
568   }
569   $self->getfield('quantity') || 1;
570 }
571
572 =item unitsetup
573
574 =cut
575
576 sub unitsetup {
577   my( $self, $value ) = @_;
578   if ( defined($value) ) {
579     $self->setfield('unitsetup', $value);
580   }
581   $self->getfield('unitsetup') eq ''
582     ? $self->getfield('setup')
583     : $self->getfield('unitsetup');
584 }
585
586 =item unitrecur
587
588 =cut
589
590 sub unitrecur {
591   my( $self, $value ) = @_;
592   if ( defined($value) ) {
593     $self->setfield('unitrecur', $value);
594   }
595   $self->getfield('unitrecur') eq ''
596     ? $self->getfield('recur')
597     : $self->getfield('unitrecur');
598 }
599
600 =item disintegrate
601
602 Returns a list of cust_bill_pkg objects each with no more than a single class
603 (including setup or recur) of charge.
604
605 =cut
606
607 sub disintegrate {
608   my $self = shift;
609   # XXX this goes away with cust_bill_pkg refactor
610
611   my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
612   my %cust_bill_pkg = ();
613
614   $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
615   $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
616
617
618   #split setup and recur
619   if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
620     my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
621     $cust_bill_pkg->set('details', []);
622     $cust_bill_pkg->recur(0);
623     $cust_bill_pkg->unitrecur(0);
624     $cust_bill_pkg->type('');
625     $cust_bill_pkg_recur->setup(0);
626     $cust_bill_pkg_recur->unitsetup(0);
627     $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
628
629   }
630
631   #split usage from recur
632   my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage );
633   warn "usage is $usage\n" if $DEBUG > 1;
634   if ($usage) {
635     my $cust_bill_pkg_usage =
636         new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
637     $cust_bill_pkg_usage->recur( $usage );
638     $cust_bill_pkg_usage->type( 'U' );
639     my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
640     $cust_bill_pkg{recur}->recur( $recur );
641     $cust_bill_pkg{recur}->type( '' );
642     $cust_bill_pkg{recur}->set('details', []);
643     $cust_bill_pkg{''} = $cust_bill_pkg_usage;
644   }
645
646   #subdivide usage by usage_class
647   if (exists($cust_bill_pkg{''})) {
648     foreach my $class (grep { $_ } $self->usage_classes) {
649       my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
650       my $cust_bill_pkg_usage =
651           new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
652       $cust_bill_pkg_usage->recur( $usage );
653       $cust_bill_pkg_usage->set('details', []);
654       my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
655       $cust_bill_pkg{''}->recur( $classless );
656       $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
657     }
658     warn "Unexpected classless usage value: ". $cust_bill_pkg{''}->recur
659       if ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur < 0);
660     delete $cust_bill_pkg{''}
661       unless ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur > 0);
662   }
663
664 #  # sort setup,recur,'', and the rest numeric && return
665 #  my @result = map { $cust_bill_pkg{$_} }
666 #               sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
667 #                      ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
668 #                    }
669 #               keys %cust_bill_pkg;
670 #
671 #  return (@result);
672
673    %cust_bill_pkg;
674 }
675
676 =item usage CLASSNUM
677
678 Returns the amount of the charge associated with usage class CLASSNUM if
679 CLASSNUM is defined.  Otherwise returns the total charge associated with
680 usage.
681   
682 =cut
683
684 sub usage {
685   my( $self, $classnum ) = @_;
686   my $sum = 0;
687   my @values = ();
688
689   if ( $self->get('details') ) {
690
691     @values = 
692       map { $_->[2] }
693       grep { ref($_) && ( defined($classnum) ? $_->[3] eq $classnum : 1 ) }
694       @{ $self->get('details') };
695
696   }else{
697
698     my $hashref = { 'billpkgnum' => $self->billpkgnum };
699     $hashref->{ 'classnum' } = $classnum if defined($classnum);
700     @values = map { $_->amount } qsearch('cust_bill_pkg_detail', $hashref);
701
702   }
703
704   foreach ( @values ) {
705     $sum += $_ if $_;
706   }
707   $sum;
708 }
709
710 =item usage_classes
711
712 Returns a list of usage classnums associated with this invoice line's
713 details.
714   
715 =cut
716
717 sub usage_classes {
718   my( $self ) = @_;
719
720   if ( $self->get('details') ) {
721
722     my %seen = ();
723     foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
724       $seen{ $detail->[3] } = 1;
725     }
726     keys %seen;
727
728   }else{
729
730     map { $_->classnum }
731         qsearch({ table   => 'cust_bill_pkg_detail',
732                   hashref => { billpkgnum => $self->billpkgnum },
733                   select  => 'DISTINCT classnum',
734                });
735
736   }
737
738 }
739
740 =item cust_bill_pkg_display [ type => TYPE ]
741
742 Returns an array of display information for the invoice line item optionally
743 limited to 'TYPE'.
744
745 =cut
746
747 sub cust_bill_pkg_display {
748   my ( $self, %opt ) = @_;
749
750   my $default =
751     new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
752
753   return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
754
755   my $type = $opt{type} if exists $opt{type};
756   my @result;
757
758   if ( scalar( $self->get('display') ) ) {
759     @result = grep { defined($type) ? ($type eq $_->type) : 1 }
760               @{ $self->get('display') };
761   }else{
762     my $hashref = { 'billpkgnum' => $self->billpkgnum };
763     $hashref->{type} = $type if defined($type);
764     
765     @result = qsearch ({ 'table'    => 'cust_bill_pkg_display',
766                          'hashref'  => { 'billpkgnum' => $self->billpkgnum },
767                          'order_by' => 'ORDER BY billpkgdisplaynum',
768                       });
769   }
770
771   push @result, $default unless ( scalar(@result) || $type );
772
773   @result;
774
775 }
776
777 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
778 # and FS::cust_main::bill
779
780 sub _cust_tax_exempt_pkg {
781   my ( $self ) = @_;
782
783   $self->{Hash}->{_cust_tax_exempt_pkg} or
784   $self->{Hash}->{_cust_tax_exempt_pkg} = [];
785
786 }
787
788
789 =item cust_bill_pkg_detail [ CLASSNUM ]
790
791 Returns the list of associated cust_bill_pkg_detail objects
792 The optional CLASSNUM argument will limit the details to the specified usage
793 class.
794
795 =cut
796
797 sub cust_bill_pkg_detail {
798   my $self = shift;
799   my $classnum = shift || '';
800
801   my %hash = ( 'billpkgnum' => $self->billpkgnum );
802   $hash{classnum} = $classnum if $classnum;
803
804   qsearch ( 'cust_bill_pkg_detail', { %hash  } ),
805
806 }
807
808 =back
809
810 =head1 BUGS
811
812 setup and recur shouldn't be separate fields.  There should be one "amount"
813 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
814
815 A line item with both should really be two separate records (preserving
816 sdate and edate for setup fees for recurring packages - that information may
817 be valuable later).  Invoice generation (cust_main::bill), invoice printing
818 (cust_bill), tax reports (report_tax.cgi) and line item reports 
819 (cust_bill_pkg.cgi) would need to be updated.
820
821 owed_setup and owed_recur could then be repaced by just owed, and
822 cust_bill::open_cust_bill_pkg and
823 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
824
825 =head1 SEE ALSO
826
827 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
828 from the base documentation.
829
830 =cut
831
832 1;
833