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