c825c15672bdeb37919d0f6fb34f5b4711ebcfd8
[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->get('discounts') ) {
178     foreach my $cust_bill_pkg_discount ( @{$self->get('discounts')} ) {
179       $cust_bill_pkg_discount->billpkgnum($self->billpkgnum);
180       $error = $cust_bill_pkg_discount->insert;
181       if ( $error ) {
182         $dbh->rollback if $oldAutoCommit;
183         return "error inserting cust_bill_pkg_discount: $error";
184       }
185     }
186   }
187
188   if ( $self->_cust_tax_exempt_pkg ) {
189     foreach my $cust_tax_exempt_pkg ( @{$self->_cust_tax_exempt_pkg} ) {
190       $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
191       $error = $cust_tax_exempt_pkg->insert;
192       if ( $error ) {
193         $dbh->rollback if $oldAutoCommit;
194         return "error inserting cust_tax_exempt_pkg: $error";
195       }
196     }
197   }
198
199   my $tax_location = $self->get('cust_bill_pkg_tax_location');
200   if ( $tax_location ) {
201     foreach my $cust_bill_pkg_tax_location ( @$tax_location ) {
202       $cust_bill_pkg_tax_location->billpkgnum($self->billpkgnum);
203       $error = $cust_bill_pkg_tax_location->insert;
204       if ( $error ) {
205         $dbh->rollback if $oldAutoCommit;
206         return "error inserting cust_bill_pkg_tax_location: $error";
207       }
208     }
209   }
210
211   my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
212   if ( $tax_rate_location ) {
213     foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
214       $cust_bill_pkg_tax_rate_location->billpkgnum($self->billpkgnum);
215       $error = $cust_bill_pkg_tax_rate_location->insert;
216       if ( $error ) {
217         $dbh->rollback if $oldAutoCommit;
218         return "error inserting cust_bill_pkg_tax_rate_location: $error";
219       }
220     }
221   }
222
223   my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
224   if ( $cust_tax_adjustment ) {
225     $cust_tax_adjustment->billpkgnum($self->billpkgnum);
226     $error = $cust_tax_adjustment->replace;
227     if ( $error ) {
228       $dbh->rollback if $oldAutoCommit;
229       return "error replacing cust_tax_adjustment: $error";
230     }
231   }
232
233   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
234   '';
235
236 }
237
238 =item delete
239
240 Not recommended.
241
242 =cut
243
244 sub delete {
245   my $self = shift;
246
247   local $SIG{HUP} = 'IGNORE';
248   local $SIG{INT} = 'IGNORE';
249   local $SIG{QUIT} = 'IGNORE';
250   local $SIG{TERM} = 'IGNORE';
251   local $SIG{TSTP} = 'IGNORE';
252   local $SIG{PIPE} = 'IGNORE';
253
254   my $oldAutoCommit = $FS::UID::AutoCommit;
255   local $FS::UID::AutoCommit = 0;
256   my $dbh = dbh;
257
258   foreach my $table (qw(
259     cust_bill_pkg_detail
260     cust_bill_pkg_display
261     cust_bill_pkg_tax_location
262     cust_bill_pkg_tax_rate_location
263     cust_tax_exempt_pkg
264     cust_bill_pay_pkg
265     cust_credit_bill_pkg
266   )) {
267
268     foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
269       my $error = $linked->delete;
270       if ( $error ) {
271         $dbh->rollback if $oldAutoCommit;
272         return $error;
273       }
274     }
275
276   }
277
278   foreach my $cust_tax_adjustment (
279     qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
280   ) {
281     $cust_tax_adjustment->billpkgnum(''); #NULL
282     my $error = $cust_tax_adjustment->replace;
283     if ( $error ) {
284       $dbh->rollback if $oldAutoCommit;
285       return $error;
286     }
287   }
288
289   my $error = $self->SUPER::delete(@_);
290   if ( $error ) {
291     $dbh->rollback if $oldAutoCommit;
292     return $error;
293   }
294
295   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
296
297   '';
298
299 }
300
301 #alas, bin/follow-tax-rename
302 #
303 #=item replace OLD_RECORD
304 #
305 #Currently unimplemented.  This would be even more of an accounting nightmare
306 #than deleteing the items.  Just don't do it.
307 #
308 #=cut
309 #
310 #sub replace {
311 #  return "Can't modify cust_bill_pkg records!";
312 #}
313
314 =item check
315
316 Checks all fields to make sure this is a valid line item.  If there is an
317 error, returns the error, otherwise returns false.  Called by the insert
318 method.
319
320 =cut
321
322 sub check {
323   my $self = shift;
324
325   my $error =
326          $self->ut_numbern('billpkgnum')
327       || $self->ut_snumber('pkgnum')
328       || $self->ut_number('invnum')
329       || $self->ut_money('setup')
330       || $self->ut_money('recur')
331       || $self->ut_numbern('sdate')
332       || $self->ut_numbern('edate')
333       || $self->ut_textn('itemdesc')
334       || $self->ut_textn('itemcomment')
335       || $self->ut_enum('hidden', [ '', 'Y' ])
336   ;
337   return $error if $error;
338
339   #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
340   if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
341     return "Unknown pkgnum ". $self->pkgnum
342       unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
343   }
344
345   return "Unknown invnum"
346     unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
347
348   $self->SUPER::check;
349 }
350
351 =item cust_pkg
352
353 Returns the package (see L<FS::cust_pkg>) for this invoice line item.
354
355 =cut
356
357 sub cust_pkg {
358   my $self = shift;
359   carp "$me $self -> cust_pkg" if $DEBUG;
360   qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
361 }
362
363 =item part_pkg
364
365 Returns the package definition for this invoice line item.
366
367 =cut
368
369 sub part_pkg {
370   my $self = shift;
371   if ( $self->pkgpart_override ) {
372     qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } );
373   } else {
374     my $part_pkg;
375     my $cust_pkg = $self->cust_pkg;
376     $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
377     $part_pkg;
378   }
379 }
380
381 =item cust_bill
382
383 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
384
385 =cut
386
387 sub cust_bill {
388   my $self = shift;
389   qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
390 }
391
392 =item previous_cust_bill_pkg
393
394 Returns the previous cust_bill_pkg for this package, if any.
395
396 =cut
397
398 sub previous_cust_bill_pkg {
399   my $self = shift;
400   return unless $self->sdate;
401   qsearchs({
402     'table'    => 'cust_bill_pkg',
403     'hashref'  => { 'pkgnum' => $self->pkgnum,
404                     'sdate'  => { op=>'<', value=>$self->sdate },
405                   },
406     'order_by' => 'ORDER BY sdate DESC LIMIT 1',
407   });
408 }
409
410 =item details [ OPTION => VALUE ... ]
411
412 Returns an array of detail information for the invoice line item.
413
414 Currently available options are: I<format> I<escape_function>
415
416 If I<format> is set to html or latex then the array members are improved
417 for tabular appearance in those environments if possible.
418
419 If I<escape_function> is set then the array members are processed by this
420 function before being returned.
421
422 =cut
423
424 sub details {
425   my ( $self, %opt ) = @_;
426   my $format = $opt{format} || '';
427   my $escape_function = $opt{escape_function} || sub { shift };
428   return () unless defined dbdef->table('cust_bill_pkg_detail');
429
430   eval "use Text::CSV_XS;";
431   die $@ if $@;
432   my $csv = new Text::CSV_XS;
433
434   my $format_sub = sub { my $detail = shift;
435                          $csv->parse($detail) or return "can't parse $detail";
436                          join(' - ', map { &$escape_function($_) }
437                                      $csv->fields
438                              );
439                        };
440
441   $format_sub = sub { my $detail = shift;
442                       $csv->parse($detail) or return "can't parse $detail";
443                       join('</TD><TD>', map { &$escape_function($_) }
444                                         $csv->fields
445                           );
446                     }
447     if $format eq 'html';
448
449   $format_sub = sub { my $detail = shift;
450                       $csv->parse($detail) or return "can't parse $detail";
451                       #join(' & ', map { '\small{'. &$escape_function($_). '}' }
452                       #            $csv->fields );
453                       my $result = '';
454                       my $column = 1;
455                       foreach ($csv->fields) {
456                         $result .= ' & ' if $column > 1;
457                         if ($column > 6) {                     # KLUDGE ALERT!
458                           $result .= '\multicolumn{1}{l}{\scriptsize{'.
459                                      &$escape_function($_). '}}';
460                         }else{
461                           $result .= '\scriptsize{'.  &$escape_function($_). '}';
462                         }
463                         $column++;
464                       }
465                       $result;
466                     }
467     if $format eq 'latex';
468
469   $format_sub = $opt{format_function} if $opt{format_function};
470
471   map { ( $_->format eq 'C'
472           ? &{$format_sub}( $_->detail, $_ )
473           : &{$escape_function}( $_->detail )
474         )
475       }
476     qsearch ({ 'table'    => 'cust_bill_pkg_detail',
477                'hashref'  => { 'billpkgnum' => $self->billpkgnum },
478                'order_by' => 'ORDER BY detailnum',
479             });
480     #qsearch ( 'cust_bill_pkg_detail', { 'lineitemnum' => $self->lineitemnum });
481 }
482
483 =item desc
484
485 Returns a description for this line item.  For typical line items, this is the
486 I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
487 For one-shot line items and named taxes, it is the I<itemdesc> field of this
488 line item, and for generic taxes, simply returns "Tax".
489
490 =cut
491
492 sub desc {
493   my $self = shift;
494
495   if ( $self->pkgnum > 0 ) {
496     $self->itemdesc || $self->part_pkg->pkg;
497   } else {
498     my $desc = $self->itemdesc || 'Tax';
499     $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
500     $desc;
501   }
502 }
503
504 =item owed_setup
505
506 Returns the amount owed (still outstanding) on this line item's setup fee,
507 which is the amount of the line item minus all payment applications (see
508 L<FS::cust_bill_pay_pkg> and credit applications (see
509 L<FS::cust_credit_bill_pkg>).
510
511 =cut
512
513 sub owed_setup {
514   my $self = shift;
515   $self->owed('setup', @_);
516 }
517
518 =item owed_recur
519
520 Returns the amount owed (still outstanding) on this line item's recurring fee,
521 which is the amount of the line item minus all payment applications (see
522 L<FS::cust_bill_pay_pkg> and credit applications (see
523 L<FS::cust_credit_bill_pkg>).
524
525 =cut
526
527 sub owed_recur {
528   my $self = shift;
529   $self->owed('recur', @_);
530 }
531
532 # modeled after cust_bill::owed...
533 sub owed {
534   my( $self, $field ) = @_;
535   my $balance = $self->$field();
536   $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
537   $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
538   $balance = sprintf( '%.2f', $balance );
539   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
540   $balance;
541 }
542
543 #modeled after owed
544 sub payable {
545   my( $self, $field ) = @_;
546   my $balance = $self->$field();
547   $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
548   $balance = sprintf( '%.2f', $balance );
549   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
550   $balance;
551 }
552
553 sub cust_bill_pay_pkg {
554   my( $self, $field ) = @_;
555   qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
556                                   'setuprecur' => $field,
557                                 }
558          );
559 }
560
561 sub cust_credit_bill_pkg {
562   my( $self, $field ) = @_;
563   qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
564                                      'setuprecur' => $field,
565                                    }
566          );
567 }
568
569 =item units
570
571 Returns the number of billing units (for tax purposes) represented by this,
572 line item.
573
574 =cut
575
576 sub units {
577   my $self = shift;
578   $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
579 }
580
581 =item quantity
582
583 =cut
584
585 sub quantity {
586   my( $self, $value ) = @_;
587   if ( defined($value) ) {
588     $self->setfield('quantity', $value);
589   }
590   $self->getfield('quantity') || 1;
591 }
592
593 =item unitsetup
594
595 =cut
596
597 sub unitsetup {
598   my( $self, $value ) = @_;
599   if ( defined($value) ) {
600     $self->setfield('unitsetup', $value);
601   }
602   $self->getfield('unitsetup') eq ''
603     ? $self->getfield('setup')
604     : $self->getfield('unitsetup');
605 }
606
607 =item unitrecur
608
609 =cut
610
611 sub unitrecur {
612   my( $self, $value ) = @_;
613   if ( defined($value) ) {
614     $self->setfield('unitrecur', $value);
615   }
616   $self->getfield('unitrecur') eq ''
617     ? $self->getfield('recur')
618     : $self->getfield('unitrecur');
619 }
620
621 =item disintegrate
622
623 Returns a list of cust_bill_pkg objects each with no more than a single class
624 (including setup or recur) of charge.
625
626 =cut
627
628 sub disintegrate {
629   my $self = shift;
630   # XXX this goes away with cust_bill_pkg refactor
631
632   my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
633   my %cust_bill_pkg = ();
634
635   $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
636   $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
637
638
639   #split setup and recur
640   if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
641     my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
642     $cust_bill_pkg->set('details', []);
643     $cust_bill_pkg->recur(0);
644     $cust_bill_pkg->unitrecur(0);
645     $cust_bill_pkg->type('');
646     $cust_bill_pkg_recur->setup(0);
647     $cust_bill_pkg_recur->unitsetup(0);
648     $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
649
650   }
651
652   #split usage from recur
653   my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage )
654     if exists($cust_bill_pkg{recur});
655   warn "usage is $usage\n" if $DEBUG > 1;
656   if ($usage) {
657     my $cust_bill_pkg_usage =
658         new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
659     $cust_bill_pkg_usage->recur( $usage );
660     $cust_bill_pkg_usage->type( 'U' );
661     my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
662     $cust_bill_pkg{recur}->recur( $recur );
663     $cust_bill_pkg{recur}->type( '' );
664     $cust_bill_pkg{recur}->set('details', []);
665     $cust_bill_pkg{''} = $cust_bill_pkg_usage;
666   }
667
668   #subdivide usage by usage_class
669   if (exists($cust_bill_pkg{''})) {
670     foreach my $class (grep { $_ } $self->usage_classes) {
671       my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
672       my $cust_bill_pkg_usage =
673           new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
674       $cust_bill_pkg_usage->recur( $usage );
675       $cust_bill_pkg_usage->set('details', []);
676       my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
677       $cust_bill_pkg{''}->recur( $classless );
678       $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
679     }
680     warn "Unexpected classless usage value: ". $cust_bill_pkg{''}->recur
681       if ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur < 0);
682     delete $cust_bill_pkg{''}
683       unless ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur > 0);
684   }
685
686 #  # sort setup,recur,'', and the rest numeric && return
687 #  my @result = map { $cust_bill_pkg{$_} }
688 #               sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
689 #                      ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
690 #                    }
691 #               keys %cust_bill_pkg;
692 #
693 #  return (@result);
694
695    %cust_bill_pkg;
696 }
697
698 =item usage CLASSNUM
699
700 Returns the amount of the charge associated with usage class CLASSNUM if
701 CLASSNUM is defined.  Otherwise returns the total charge associated with
702 usage.
703   
704 =cut
705
706 sub usage {
707   my( $self, $classnum ) = @_;
708   my $sum = 0;
709   my @values = ();
710
711   if ( $self->get('details') ) {
712
713     @values = 
714       map { $_->[2] }
715       grep { ref($_) && ( defined($classnum) ? $_->[3] eq $classnum : 1 ) }
716       @{ $self->get('details') };
717
718   }else{
719
720     my $hashref = { 'billpkgnum' => $self->billpkgnum };
721     $hashref->{ 'classnum' } = $classnum if defined($classnum);
722     @values = map { $_->amount } qsearch('cust_bill_pkg_detail', $hashref);
723
724   }
725
726   foreach ( @values ) {
727     $sum += $_ if $_;
728   }
729   $sum;
730 }
731
732 =item usage_classes
733
734 Returns a list of usage classnums associated with this invoice line's
735 details.
736   
737 =cut
738
739 sub usage_classes {
740   my( $self ) = @_;
741
742   if ( $self->get('details') ) {
743
744     my %seen = ();
745     foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
746       $seen{ $detail->[3] } = 1;
747     }
748     keys %seen;
749
750   }else{
751
752     map { $_->classnum }
753         qsearch({ table   => 'cust_bill_pkg_detail',
754                   hashref => { billpkgnum => $self->billpkgnum },
755                   select  => 'DISTINCT classnum',
756                });
757
758   }
759
760 }
761
762 =item cust_bill_pkg_display [ type => TYPE ]
763
764 Returns an array of display information for the invoice line item optionally
765 limited to 'TYPE'.
766
767 =cut
768
769 sub cust_bill_pkg_display {
770   my ( $self, %opt ) = @_;
771
772   my $default =
773     new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
774
775   return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
776
777   my $type = $opt{type} if exists $opt{type};
778   my @result;
779
780   if ( $self->get('display') ) {
781     @result = grep { defined($type) ? ($type eq $_->type) : 1 }
782               @{ $self->get('display') };
783   } else {
784     my $hashref = { 'billpkgnum' => $self->billpkgnum };
785     $hashref->{type} = $type if defined($type);
786     
787     @result = qsearch ({ 'table'    => 'cust_bill_pkg_display',
788                          'hashref'  => { 'billpkgnum' => $self->billpkgnum },
789                          'order_by' => 'ORDER BY billpkgdisplaynum',
790                       });
791   }
792
793   push @result, $default unless ( scalar(@result) || $type );
794
795   @result;
796
797 }
798
799 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
800 # and FS::cust_main::bill
801
802 sub _cust_tax_exempt_pkg {
803   my ( $self ) = @_;
804
805   $self->{Hash}->{_cust_tax_exempt_pkg} or
806   $self->{Hash}->{_cust_tax_exempt_pkg} = [];
807
808 }
809
810 =item cust_bill_pkg_tax_Xlocation
811
812 Returns the list of associated cust_bill_pkg_tax_location and/or
813 cust_bill_pkg_tax_rate_location objects
814
815 =cut
816
817 sub cust_bill_pkg_tax_Xlocation {
818   my $self = shift;
819
820   my %hash = ( 'billpkgnum' => $self->billpkgnum );
821
822   (
823     qsearch ( 'cust_bill_pkg_tax_location', { %hash  } ),
824     qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
825   );
826
827 }
828
829 =item cust_bill_pkg_detail [ CLASSNUM ]
830
831 Returns the list of associated cust_bill_pkg_detail objects
832 The optional CLASSNUM argument will limit the details to the specified usage
833 class.
834
835 =cut
836
837 sub cust_bill_pkg_detail {
838   my $self = shift;
839   my $classnum = shift || '';
840
841   my %hash = ( 'billpkgnum' => $self->billpkgnum );
842   $hash{classnum} = $classnum if $classnum;
843
844   qsearch ( 'cust_bill_pkg_detail', { %hash  } ),
845
846 }
847
848 =back
849
850 =head1 BUGS
851
852 setup and recur shouldn't be separate fields.  There should be one "amount"
853 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
854
855 A line item with both should really be two separate records (preserving
856 sdate and edate for setup fees for recurring packages - that information may
857 be valuable later).  Invoice generation (cust_main::bill), invoice printing
858 (cust_bill), tax reports (report_tax.cgi) and line item reports 
859 (cust_bill_pkg.cgi) would need to be updated.
860
861 owed_setup and owed_recur could then be repaced by just owed, and
862 cust_bill::open_cust_bill_pkg and
863 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
864
865 =head1 SEE ALSO
866
867 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
868 from the base documentation.
869
870 =cut
871
872 1;
873