This commit was generated by cvs2svn to compensate for changes in r8593,
[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     if exists($cust_bill_pkg{recur});
634   warn "usage is $usage\n" if $DEBUG > 1;
635   if ($usage) {
636     my $cust_bill_pkg_usage =
637         new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
638     $cust_bill_pkg_usage->recur( $usage );
639     $cust_bill_pkg_usage->type( 'U' );
640     my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
641     $cust_bill_pkg{recur}->recur( $recur );
642     $cust_bill_pkg{recur}->type( '' );
643     $cust_bill_pkg{recur}->set('details', []);
644     $cust_bill_pkg{''} = $cust_bill_pkg_usage;
645   }
646
647   #subdivide usage by usage_class
648   if (exists($cust_bill_pkg{''})) {
649     foreach my $class (grep { $_ } $self->usage_classes) {
650       my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
651       my $cust_bill_pkg_usage =
652           new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
653       $cust_bill_pkg_usage->recur( $usage );
654       $cust_bill_pkg_usage->set('details', []);
655       my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
656       $cust_bill_pkg{''}->recur( $classless );
657       $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
658     }
659     delete $cust_bill_pkg{''} unless $cust_bill_pkg{''}->recur;
660   }
661
662 #  # sort setup,recur,'', and the rest numeric && return
663 #  my @result = map { $cust_bill_pkg{$_} }
664 #               sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
665 #                      ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
666 #                    }
667 #               keys %cust_bill_pkg;
668 #
669 #  return (@result);
670
671    %cust_bill_pkg;
672 }
673
674 =item usage CLASSNUM
675
676 Returns the amount of the charge associated with usage class CLASSNUM if
677 CLASSNUM is defined.  Otherwise returns the total charge associated with
678 usage.
679   
680 =cut
681
682 sub usage {
683   my( $self, $classnum ) = @_;
684   my $sum = 0;
685   my @values = ();
686
687   if ( $self->get('details') ) {
688
689     @values = 
690       map { $_->[2] }
691       grep { ref($_) && ( defined($classnum) ? $_->[3] eq $classnum : 1 ) }
692       @{ $self->get('details') };
693
694   }else{
695
696     my $hashref = { 'billpkgnum' => $self->billpkgnum };
697     $hashref->{ 'classnum' } = $classnum if defined($classnum);
698     @values = map { $_->amount } qsearch('cust_bill_pkg_detail', $hashref);
699
700   }
701
702   foreach ( @values ) {
703     $sum += $_ if $_;
704   }
705   $sum;
706 }
707
708 =item usage_classes
709
710 Returns a list of usage classnums associated with this invoice line's
711 details.
712   
713 =cut
714
715 sub usage_classes {
716   my( $self ) = @_;
717
718   if ( $self->get('details') ) {
719
720     my %seen = ();
721     foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
722       $seen{ $detail->[3] } = 1;
723     }
724     keys %seen;
725
726   }else{
727
728     map { $_->classnum }
729         qsearch({ table   => 'cust_bill_pkg_detail',
730                   hashref => { billpkgnum => $self->billpkgnum },
731                   select  => 'DISTINCT classnum',
732                });
733
734   }
735
736 }
737
738 =item cust_bill_pkg_display [ type => TYPE ]
739
740 Returns an array of display information for the invoice line item optionally
741 limited to 'TYPE'.
742
743 =cut
744
745 sub cust_bill_pkg_display {
746   my ( $self, %opt ) = @_;
747
748   my $default =
749     new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
750
751   return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
752
753   my $type = $opt{type} if exists $opt{type};
754   my @result;
755
756   if ( scalar( $self->get('display') ) ) {
757     @result = grep { defined($type) ? ($type eq $_->type) : 1 }
758               @{ $self->get('display') };
759   }else{
760     my $hashref = { 'billpkgnum' => $self->billpkgnum };
761     $hashref->{type} = $type if defined($type);
762     
763     @result = qsearch ({ 'table'    => 'cust_bill_pkg_display',
764                          'hashref'  => { 'billpkgnum' => $self->billpkgnum },
765                          'order_by' => 'ORDER BY billpkgdisplaynum',
766                       });
767   }
768
769   push @result, $default unless ( scalar(@result) || $type );
770
771   @result;
772
773 }
774
775 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
776 # and FS::cust_main::bill
777
778 sub _cust_tax_exempt_pkg {
779   my ( $self ) = @_;
780
781   $self->{Hash}->{_cust_tax_exempt_pkg} or
782   $self->{Hash}->{_cust_tax_exempt_pkg} = [];
783
784 }
785
786 =item cust_bill_pkg_tax_Xlocation
787
788 Returns the list of associated cust_bill_pkg_tax_location and/or
789 cust_bill_pkg_tax_rate_location objects
790
791 =cut
792
793 sub cust_bill_pkg_tax_Xlocation {
794   my $self = shift;
795
796   my %hash = ( 'billpkgnum' => $self->billpkgnum );
797
798   (
799     qsearch ( 'cust_bill_pkg_tax_location', { %hash  } ),
800     qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
801   );
802
803 }
804
805 =item cust_bill_pkg_detail [ CLASSNUM ]
806
807 Returns the list of associated cust_bill_pkg_detail objects
808 The optional CLASSNUM argument will limit the details to the specified usage
809 class.
810
811 =cut
812
813 sub cust_bill_pkg_detail {
814   my $self = shift;
815   my $classnum = shift || '';
816
817   my %hash = ( 'billpkgnum' => $self->billpkgnum );
818   $hash{classnum} = $classnum if $classnum;
819
820   qsearch ( 'cust_bill_pkg_detail', { %hash  } ),
821
822 }
823
824 =back
825
826 =head1 BUGS
827
828 setup and recur shouldn't be separate fields.  There should be one "amount"
829 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
830
831 A line item with both should really be two separate records (preserving
832 sdate and edate for setup fees for recurring packages - that information may
833 be valuable later).  Invoice generation (cust_main::bill), invoice printing
834 (cust_bill), tax reports (report_tax.cgi) and line item reports 
835 (cust_bill_pkg.cgi) would need to be updated.
836
837 owed_setup and owed_recur could then be repaced by just owed, and
838 cust_bill::open_cust_bill_pkg and
839 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
840
841 =head1 SEE ALSO
842
843 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
844 from the base documentation.
845
846 =cut
847
848 1;
849