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