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