This commit was generated by cvs2svn to compensate for changes in r8690,
[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 #modeled after owed
533 sub payable {
534   my( $self, $field ) = @_;
535   my $balance = $self->$field();
536   $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
537   $balance = sprintf( '%.2f', $balance );
538   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
539   $balance;
540 }
541
542 sub cust_bill_pay_pkg {
543   my( $self, $field ) = @_;
544   qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
545                                   'setuprecur' => $field,
546                                 }
547          );
548 }
549
550 sub cust_credit_bill_pkg {
551   my( $self, $field ) = @_;
552   qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
553                                      'setuprecur' => $field,
554                                    }
555          );
556 }
557
558 =item units
559
560 Returns the number of billing units (for tax purposes) represented by this,
561 line item.
562
563 =cut
564
565 sub units {
566   my $self = shift;
567   $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
568 }
569
570 =item quantity
571
572 =cut
573
574 sub quantity {
575   my( $self, $value ) = @_;
576   if ( defined($value) ) {
577     $self->setfield('quantity', $value);
578   }
579   $self->getfield('quantity') || 1;
580 }
581
582 =item unitsetup
583
584 =cut
585
586 sub unitsetup {
587   my( $self, $value ) = @_;
588   if ( defined($value) ) {
589     $self->setfield('unitsetup', $value);
590   }
591   $self->getfield('unitsetup') eq ''
592     ? $self->getfield('setup')
593     : $self->getfield('unitsetup');
594 }
595
596 =item unitrecur
597
598 =cut
599
600 sub unitrecur {
601   my( $self, $value ) = @_;
602   if ( defined($value) ) {
603     $self->setfield('unitrecur', $value);
604   }
605   $self->getfield('unitrecur') eq ''
606     ? $self->getfield('recur')
607     : $self->getfield('unitrecur');
608 }
609
610 =item disintegrate
611
612 Returns a list of cust_bill_pkg objects each with no more than a single class
613 (including setup or recur) of charge.
614
615 =cut
616
617 sub disintegrate {
618   my $self = shift;
619   # XXX this goes away with cust_bill_pkg refactor
620
621   my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
622   my %cust_bill_pkg = ();
623
624   $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
625   $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
626
627
628   #split setup and recur
629   if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
630     my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
631     $cust_bill_pkg->set('details', []);
632     $cust_bill_pkg->recur(0);
633     $cust_bill_pkg->unitrecur(0);
634     $cust_bill_pkg->type('');
635     $cust_bill_pkg_recur->setup(0);
636     $cust_bill_pkg_recur->unitsetup(0);
637     $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
638
639   }
640
641   #split usage from recur
642   my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage )
643     if exists($cust_bill_pkg{recur});
644   warn "usage is $usage\n" if $DEBUG > 1;
645   if ($usage) {
646     my $cust_bill_pkg_usage =
647         new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
648     $cust_bill_pkg_usage->recur( $usage );
649     $cust_bill_pkg_usage->type( 'U' );
650     my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
651     $cust_bill_pkg{recur}->recur( $recur );
652     $cust_bill_pkg{recur}->type( '' );
653     $cust_bill_pkg{recur}->set('details', []);
654     $cust_bill_pkg{''} = $cust_bill_pkg_usage;
655   }
656
657   #subdivide usage by usage_class
658   if (exists($cust_bill_pkg{''})) {
659     foreach my $class (grep { $_ } $self->usage_classes) {
660       my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
661       my $cust_bill_pkg_usage =
662           new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
663       $cust_bill_pkg_usage->recur( $usage );
664       $cust_bill_pkg_usage->set('details', []);
665       my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
666       $cust_bill_pkg{''}->recur( $classless );
667       $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
668     }
669     delete $cust_bill_pkg{''} unless $cust_bill_pkg{''}->recur;
670   }
671
672 #  # sort setup,recur,'', and the rest numeric && return
673 #  my @result = map { $cust_bill_pkg{$_} }
674 #               sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
675 #                      ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
676 #                    }
677 #               keys %cust_bill_pkg;
678 #
679 #  return (@result);
680
681    %cust_bill_pkg;
682 }
683
684 =item usage CLASSNUM
685
686 Returns the amount of the charge associated with usage class CLASSNUM if
687 CLASSNUM is defined.  Otherwise returns the total charge associated with
688 usage.
689   
690 =cut
691
692 sub usage {
693   my( $self, $classnum ) = @_;
694   my $sum = 0;
695   my @values = ();
696
697   if ( $self->get('details') ) {
698
699     @values = 
700       map { $_->[2] }
701       grep { ref($_) && ( defined($classnum) ? $_->[3] eq $classnum : 1 ) }
702       @{ $self->get('details') };
703
704   }else{
705
706     my $hashref = { 'billpkgnum' => $self->billpkgnum };
707     $hashref->{ 'classnum' } = $classnum if defined($classnum);
708     @values = map { $_->amount } qsearch('cust_bill_pkg_detail', $hashref);
709
710   }
711
712   foreach ( @values ) {
713     $sum += $_ if $_;
714   }
715   $sum;
716 }
717
718 =item usage_classes
719
720 Returns a list of usage classnums associated with this invoice line's
721 details.
722   
723 =cut
724
725 sub usage_classes {
726   my( $self ) = @_;
727
728   if ( $self->get('details') ) {
729
730     my %seen = ();
731     foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
732       $seen{ $detail->[3] } = 1;
733     }
734     keys %seen;
735
736   }else{
737
738     map { $_->classnum }
739         qsearch({ table   => 'cust_bill_pkg_detail',
740                   hashref => { billpkgnum => $self->billpkgnum },
741                   select  => 'DISTINCT classnum',
742                });
743
744   }
745
746 }
747
748 =item cust_bill_pkg_display [ type => TYPE ]
749
750 Returns an array of display information for the invoice line item optionally
751 limited to 'TYPE'.
752
753 =cut
754
755 sub cust_bill_pkg_display {
756   my ( $self, %opt ) = @_;
757
758   my $default =
759     new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
760
761   return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
762
763   my $type = $opt{type} if exists $opt{type};
764   my @result;
765
766   if ( scalar( $self->get('display') ) ) {
767     @result = grep { defined($type) ? ($type eq $_->type) : 1 }
768               @{ $self->get('display') };
769   }else{
770     my $hashref = { 'billpkgnum' => $self->billpkgnum };
771     $hashref->{type} = $type if defined($type);
772     
773     @result = qsearch ({ 'table'    => 'cust_bill_pkg_display',
774                          'hashref'  => { 'billpkgnum' => $self->billpkgnum },
775                          'order_by' => 'ORDER BY billpkgdisplaynum',
776                       });
777   }
778
779   push @result, $default unless ( scalar(@result) || $type );
780
781   @result;
782
783 }
784
785 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
786 # and FS::cust_main::bill
787
788 sub _cust_tax_exempt_pkg {
789   my ( $self ) = @_;
790
791   $self->{Hash}->{_cust_tax_exempt_pkg} or
792   $self->{Hash}->{_cust_tax_exempt_pkg} = [];
793
794 }
795
796 =item cust_bill_pkg_tax_Xlocation
797
798 Returns the list of associated cust_bill_pkg_tax_location and/or
799 cust_bill_pkg_tax_rate_location objects
800
801 =cut
802
803 sub cust_bill_pkg_tax_Xlocation {
804   my $self = shift;
805
806   my %hash = ( 'billpkgnum' => $self->billpkgnum );
807
808   (
809     qsearch ( 'cust_bill_pkg_tax_location', { %hash  } ),
810     qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
811   );
812
813 }
814
815 =item cust_bill_pkg_detail [ CLASSNUM ]
816
817 Returns the list of associated cust_bill_pkg_detail objects
818 The optional CLASSNUM argument will limit the details to the specified usage
819 class.
820
821 =cut
822
823 sub cust_bill_pkg_detail {
824   my $self = shift;
825   my $classnum = shift || '';
826
827   my %hash = ( 'billpkgnum' => $self->billpkgnum );
828   $hash{classnum} = $classnum if $classnum;
829
830   qsearch ( 'cust_bill_pkg_detail', { %hash  } ),
831
832 }
833
834 =back
835
836 =head1 BUGS
837
838 setup and recur shouldn't be separate fields.  There should be one "amount"
839 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
840
841 A line item with both should really be two separate records (preserving
842 sdate and edate for setup fees for recurring packages - that information may
843 be valuable later).  Invoice generation (cust_main::bill), invoice printing
844 (cust_bill), tax reports (report_tax.cgi) and line item reports 
845 (cust_bill_pkg.cgi) would need to be updated.
846
847 owed_setup and owed_recur could then be repaced by just owed, and
848 cust_bill::open_cust_bill_pkg and
849 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
850
851 =head1 SEE ALSO
852
853 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
854 from the base documentation.
855
856 =cut
857
858 1;
859