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