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