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