delete invoices, RT#4048
[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     $self->cust_pkg->part_pkg;
365   }
366 }
367
368 =item cust_bill
369
370 Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
371
372 =cut
373
374 sub cust_bill {
375   my $self = shift;
376   qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
377 }
378
379 =item previous_cust_bill_pkg
380
381 Returns the previous cust_bill_pkg for this package, if any.
382
383 =cut
384
385 sub previous_cust_bill_pkg {
386   my $self = shift;
387   return unless $self->sdate;
388   qsearchs({
389     'table'    => 'cust_bill_pkg',
390     'hashref'  => { 'pkgnum' => $self->pkgnum,
391                     'sdate'  => { op=>'<', value=>$self->sdate },
392                   },
393     'order_by' => 'ORDER BY sdate DESC LIMIT 1',
394   });
395 }
396
397 =item details [ OPTION => VALUE ... ]
398
399 Returns an array of detail information for the invoice line item.
400
401 Currently available options are: I<format> I<escape_function>
402
403 If I<format> is set to html or latex then the array members are improved
404 for tabular appearance in those environments if possible.
405
406 If I<escape_function> is set then the array members are processed by this
407 function before being returned.
408
409 =cut
410
411 sub details {
412   my ( $self, %opt ) = @_;
413   my $format = $opt{format} || '';
414   my $escape_function = $opt{escape_function} || sub { shift };
415   return () unless defined dbdef->table('cust_bill_pkg_detail');
416
417   eval "use Text::CSV_XS;";
418   die $@ if $@;
419   my $csv = new Text::CSV_XS;
420
421   my $format_sub = sub { my $detail = shift;
422                          $csv->parse($detail) or return "can't parse $detail";
423                          join(' - ', map { &$escape_function($_) }
424                                      $csv->fields
425                              );
426                        };
427
428   $format_sub = sub { my $detail = shift;
429                       $csv->parse($detail) or return "can't parse $detail";
430                       join('</TD><TD>', map { &$escape_function($_) }
431                                         $csv->fields
432                           );
433                     }
434     if $format eq 'html';
435
436   $format_sub = sub { my $detail = shift;
437                       $csv->parse($detail) or return "can't parse $detail";
438                       #join(' & ', map { '\small{'. &$escape_function($_). '}' }
439                       #            $csv->fields );
440                       my $result = '';
441                       my $column = 1;
442                       foreach ($csv->fields) {
443                         $result .= ' & ' if $column > 1;
444                         if ($column > 6) {                     # KLUDGE ALERT!
445                           $result .= '\multicolumn{1}{l}{\scriptsize{'.
446                                      &$escape_function($_). '}}';
447                         }else{
448                           $result .= '\scriptsize{'.  &$escape_function($_). '}';
449                         }
450                         $column++;
451                       }
452                       $result;
453                     }
454     if $format eq 'latex';
455
456   $format_sub = $opt{format_function} if $opt{format_function};
457
458   map { ( $_->format eq 'C'
459           ? &{$format_sub}( $_->detail, $_ )
460           : &{$escape_function}( $_->detail )
461         )
462       }
463     qsearch ({ 'table'    => 'cust_bill_pkg_detail',
464                'hashref'  => { 'billpkgnum' => $self->billpkgnum },
465                'order_by' => 'ORDER BY detailnum',
466             });
467     #qsearch ( 'cust_bill_pkg_detail', { 'lineitemnum' => $self->lineitemnum });
468 }
469
470 =item desc
471
472 Returns a description for this line item.  For typical line items, this is the
473 I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
474 For one-shot line items and named taxes, it is the I<itemdesc> field of this
475 line item, and for generic taxes, simply returns "Tax".
476
477 =cut
478
479 sub desc {
480   my $self = shift;
481
482   if ( $self->pkgnum > 0 ) {
483     $self->itemdesc || $self->part_pkg->pkg;
484   } else {
485     my $desc = $self->itemdesc || 'Tax';
486     $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
487     $desc;
488   }
489 }
490
491 =item owed_setup
492
493 Returns the amount owed (still outstanding) on this line item's setup fee,
494 which is the amount of the line item minus all payment applications (see
495 L<FS::cust_bill_pay_pkg> and credit applications (see
496 L<FS::cust_credit_bill_pkg>).
497
498 =cut
499
500 sub owed_setup {
501   my $self = shift;
502   $self->owed('setup', @_);
503 }
504
505 =item owed_recur
506
507 Returns the amount owed (still outstanding) on this line item's recurring fee,
508 which is the amount of the line item minus all payment applications (see
509 L<FS::cust_bill_pay_pkg> and credit applications (see
510 L<FS::cust_credit_bill_pkg>).
511
512 =cut
513
514 sub owed_recur {
515   my $self = shift;
516   $self->owed('recur', @_);
517 }
518
519 # modeled after cust_bill::owed...
520 sub owed {
521   my( $self, $field ) = @_;
522   my $balance = $self->$field();
523   $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
524   $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
525   $balance = sprintf( '%.2f', $balance );
526   $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
527   $balance;
528 }
529
530 sub cust_bill_pay_pkg {
531   my( $self, $field ) = @_;
532   qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
533                                   'setuprecur' => $field,
534                                 }
535          );
536 }
537
538 sub cust_credit_bill_pkg {
539   my( $self, $field ) = @_;
540   qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
541                                      'setuprecur' => $field,
542                                    }
543          );
544 }
545
546 =item units
547
548 Returns the number of billing units (for tax purposes) represented by this,
549 line item.
550
551 =cut
552
553 sub units {
554   my $self = shift;
555   $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
556 }
557
558 =item quantity
559
560 =cut
561
562 sub quantity {
563   my( $self, $value ) = @_;
564   if ( defined($value) ) {
565     $self->setfield('quantity', $value);
566   }
567   $self->getfield('quantity') || 1;
568 }
569
570 =item unitsetup
571
572 =cut
573
574 sub unitsetup {
575   my( $self, $value ) = @_;
576   if ( defined($value) ) {
577     $self->setfield('unitsetup', $value);
578   }
579   $self->getfield('unitsetup') eq ''
580     ? $self->getfield('setup')
581     : $self->getfield('unitsetup');
582 }
583
584 =item unitrecur
585
586 =cut
587
588 sub unitrecur {
589   my( $self, $value ) = @_;
590   if ( defined($value) ) {
591     $self->setfield('unitrecur', $value);
592   }
593   $self->getfield('unitrecur') eq ''
594     ? $self->getfield('recur')
595     : $self->getfield('unitrecur');
596 }
597
598 =item disintegrate
599
600 Returns a list of cust_bill_pkg objects each with no more than a single class
601 (including setup or recur) of charge.
602
603 =cut
604
605 sub disintegrate {
606   my $self = shift;
607   # XXX this goes away with cust_bill_pkg refactor
608
609   my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
610   my %cust_bill_pkg = ();
611
612   $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
613   $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
614
615
616   #split setup and recur
617   if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
618     my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
619     $cust_bill_pkg->set('details', []);
620     $cust_bill_pkg->recur(0);
621     $cust_bill_pkg->unitrecur(0);
622     $cust_bill_pkg->type('');
623     $cust_bill_pkg_recur->setup(0);
624     $cust_bill_pkg_recur->unitsetup(0);
625     $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
626
627   }
628
629   #split usage from recur
630   my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage );
631   warn "usage is $usage\n" if $DEBUG;
632   if ($usage) {
633     my $cust_bill_pkg_usage =
634         new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
635     $cust_bill_pkg_usage->recur( $usage );
636     $cust_bill_pkg_usage->type( 'U' );
637     my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
638     $cust_bill_pkg{recur}->recur( $recur );
639     $cust_bill_pkg{recur}->type( '' );
640     $cust_bill_pkg{recur}->set('details', []);
641     $cust_bill_pkg{''} = $cust_bill_pkg_usage;
642   }
643
644   #subdivide usage by usage_class
645   if (exists($cust_bill_pkg{''})) {
646     foreach my $class (grep { $_ } $self->usage_classes) {
647       my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
648       my $cust_bill_pkg_usage =
649           new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
650       $cust_bill_pkg_usage->recur( $usage );
651       $cust_bill_pkg_usage->set('details', []);
652       my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
653       $cust_bill_pkg{''}->recur( $classless );
654       $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
655     }
656     delete $cust_bill_pkg{''} unless $cust_bill_pkg{''}->recur;
657   }
658
659 #  # sort setup,recur,'', and the rest numeric && return
660 #  my @result = map { $cust_bill_pkg{$_} }
661 #               sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
662 #                      ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
663 #                    }
664 #               keys %cust_bill_pkg;
665 #
666 #  return (@result);
667
668    %cust_bill_pkg;
669 }
670
671 =item usage CLASSNUM
672
673 Returns the amount of the charge associated with usage class CLASSNUM if
674 CLASSNUM is defined.  Otherwise returns the total charge associated with
675 usage.
676   
677 =cut
678
679 sub usage {
680   my( $self, $classnum ) = @_;
681   my $sum = 0;
682   my @values = ();
683
684   if ( $self->get('details') ) {
685
686     @values = 
687       map { $_->[2] }
688       grep { ref($_) && ( defined($classnum) ? $_->[3] eq $classnum : 1 ) }
689       @{ $self->get('details') };
690
691   }else{
692
693     my $hashref = { 'billpkgnum' => $self->billpkgnum };
694     $hashref->{ 'classnum' } = $classnum if defined($classnum);
695     @values = map { $_->amount } qsearch('cust_bill_pkg_detail', $hashref);
696
697   }
698
699   foreach ( @values ) {
700     $sum += $_ if $_;
701   }
702   $sum;
703 }
704
705 =item usage_classes
706
707 Returns a list of usage classnums associated with this invoice line's
708 details.
709   
710 =cut
711
712 sub usage_classes {
713   my( $self ) = @_;
714
715   if ( $self->get('details') ) {
716
717     my %seen = ();
718     foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
719       $seen{ $detail->[3] } = 1;
720     }
721     keys %seen;
722
723   }else{
724
725     map { $_->classnum }
726         qsearch({ table   => 'cust_bill_pkg_detail',
727                   hashref => { billpkgnum => $self->billpkgnum },
728                   select  => 'DISTINCT classnum',
729                });
730
731   }
732
733 }
734
735 =item cust_bill_pkg_display [ type => TYPE ]
736
737 Returns an array of display information for the invoice line item optionally
738 limited to 'TYPE'.
739
740 =cut
741
742 sub cust_bill_pkg_display {
743   my ( $self, %opt ) = @_;
744
745   my $default =
746     new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
747
748   return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
749
750   my $type = $opt{type} if exists $opt{type};
751   my @result;
752
753   if ( scalar( $self->get('display') ) ) {
754     @result = grep { defined($type) ? ($type eq $_->type) : 1 }
755               @{ $self->get('display') };
756   }else{
757     my $hashref = { 'billpkgnum' => $self->billpkgnum };
758     $hashref->{type} = $type if defined($type);
759     
760     @result = qsearch ({ 'table'    => 'cust_bill_pkg_display',
761                          'hashref'  => { 'billpkgnum' => $self->billpkgnum },
762                          'order_by' => 'ORDER BY billpkgdisplaynum',
763                       });
764   }
765
766   push @result, $default unless ( scalar(@result) || $type );
767
768   @result;
769
770 }
771
772 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
773 # and FS::cust_main::bill
774
775 sub _cust_tax_exempt_pkg {
776   my ( $self ) = @_;
777
778   $self->{Hash}->{_cust_tax_exempt_pkg} or
779   $self->{Hash}->{_cust_tax_exempt_pkg} = [];
780
781 }
782
783
784 =back
785
786 =head1 BUGS
787
788 setup and recur shouldn't be separate fields.  There should be one "amount"
789 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
790
791 A line item with both should really be two separate records (preserving
792 sdate and edate for setup fees for recurring packages - that information may
793 be valuable later).  Invoice generation (cust_main::bill), invoice printing
794 (cust_bill), tax reports (report_tax.cgi) and line item reports 
795 (cust_bill_pkg.cgi) would need to be updated.
796
797 owed_setup and owed_recur could then be repaced by just owed, and
798 cust_bill::open_cust_bill_pkg and
799 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
800
801 =head1 SEE ALSO
802
803 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
804 from the base documentation.
805
806 =cut
807
808 1;
809