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