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