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