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