credits return taxes, but the magic calculation button does not yet work properly...
[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 = 1;
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       };
156       $error = $cust_bill_pkg_detail->insert;
157       if ( $error ) {
158         $dbh->rollback if $oldAutoCommit;
159         return "error inserting cust_bill_pkg_detail: $error";
160       }
161     }
162   }
163
164   if ( $self->get('display') ) {
165     foreach my $cust_bill_pkg_display ( @{ $self->get('display') } ) {
166       $cust_bill_pkg_display->billpkgnum($self->billpkgnum);
167       $error = $cust_bill_pkg_display->insert;
168       if ( $error ) {
169         $dbh->rollback if $oldAutoCommit;
170         return "error inserting cust_bill_pkg_display: $error";
171       }
172     }
173   }
174
175   if ( $self->_cust_tax_exempt_pkg ) {
176     foreach my $cust_tax_exempt_pkg ( @{$self->_cust_tax_exempt_pkg} ) {
177       $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
178       $error = $cust_tax_exempt_pkg->insert;
179       if ( $error ) {
180         $dbh->rollback if $oldAutoCommit;
181         return "error inserting cust_tax_exempt_pkg: $error";
182       }
183     }
184   }
185
186   my $tax_location = $self->get('cust_bill_pkg_tax_location');
187   if ( $tax_location ) {
188     foreach my $cust_bill_pkg_tax_location ( @$tax_location ) {
189       $cust_bill_pkg_tax_location->billpkgnum($self->billpkgnum);
190       $error = $cust_bill_pkg_tax_location->insert;
191       if ( $error ) {
192         $dbh->rollback if $oldAutoCommit;
193         return "error inserting cust_bill_pkg_tax_location: $error";
194       }
195     }
196   }
197
198   my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
199   if ( $tax_rate_location ) {
200     foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
201       $cust_bill_pkg_tax_rate_location->billpkgnum($self->billpkgnum);
202       $error = $cust_bill_pkg_tax_rate_location->insert;
203       if ( $error ) {
204         $dbh->rollback if $oldAutoCommit;
205         return "error inserting cust_bill_pkg_tax_rate_location: $error";
206       }
207     }
208   }
209
210   my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
211   if ( $cust_tax_adjustment ) {
212     $cust_tax_adjustment->billpkgnum($self->billpkgnum);
213     $error = $cust_tax_adjustment->replace;
214     if ( $error ) {
215       $dbh->rollback if $oldAutoCommit;
216       return "error replacing cust_tax_adjustment: $error";
217     }
218   }
219
220   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
221   '';
222
223 }
224
225 =item delete
226
227 Not recommended.
228
229 =cut
230
231 sub delete {
232   my $self = shift;
233
234   local $SIG{HUP} = 'IGNORE';
235   local $SIG{INT} = 'IGNORE';
236   local $SIG{QUIT} = 'IGNORE';
237   local $SIG{TERM} = 'IGNORE';
238   local $SIG{TSTP} = 'IGNORE';
239   local $SIG{PIPE} = 'IGNORE';
240
241   my $oldAutoCommit = $FS::UID::AutoCommit;
242   local $FS::UID::AutoCommit = 0;
243   my $dbh = dbh;
244
245   foreach my $table (qw(
246     cust_bill_pkg_detail
247     cust_bill_pkg_display
248     cust_bill_pkg_tax_location
249     cust_bill_pkg_tax_rate_location
250     cust_tax_exempt_pkg
251     cust_bill_pay_pkg
252     cust_credit_bill_pkg
253   )) {
254
255     foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
256       my $error = $linked->delete;
257       if ( $error ) {
258         $dbh->rollback if $oldAutoCommit;
259         return $error;
260       }
261     }
262
263   }
264
265   foreach my $cust_tax_adjustment (
266     qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
267   ) {
268     $cust_tax_adjustment->billpkgnum(''); #NULL
269     my $error = $cust_tax_adjustment->replace;
270     if ( $error ) {
271       $dbh->rollback if $oldAutoCommit;
272       return $error;
273     }
274   }
275
276   my $error = $self->SUPER::delete(@_);
277   if ( $error ) {
278     $dbh->rollback if $oldAutoCommit;
279     return $error;
280   }
281
282   $dbh->commit or die $dbh->errstr if $oldAutoCommit;
283
284   '';
285
286 }
287
288 #alas, bin/follow-tax-rename
289 #
290 #=item replace OLD_RECORD
291 #
292 #Currently unimplemented.  This would be even more of an accounting nightmare
293 #than deleteing the items.  Just don't do it.
294 #
295 #=cut
296 #
297 #sub replace {
298 #  return "Can't modify cust_bill_pkg records!";
299 #}
300
301 =item check
302
303 Checks all fields to make sure this is a valid line item.  If there is an
304 error, returns the error, otherwise returns false.  Called by the insert
305 method.
306
307 =cut
308
309 sub check {
310   my $self = shift;
311
312   my $error =
313          $self->ut_numbern('billpkgnum')
314       || $self->ut_snumber('pkgnum')
315       || $self->ut_number('invnum')
316       || $self->ut_money('setup')
317       || $self->ut_money('recur')
318       || $self->ut_numbern('sdate')
319       || $self->ut_numbern('edate')
320       || $self->ut_textn('itemdesc')
321       || $self->ut_textn('itemcomment')
322       || $self->ut_enum('hidden', [ '', 'Y' ])
323   ;
324   return $error if $error;
325
326   #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
327   if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
328     return "Unknown pkgnum ". $self->pkgnum
329       unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
330   }
331
332   return "Unknown invnum"
333     unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
334
335   $self->SUPER::check;
336 }
337
338 =item cust_pkg
339
340 Returns the package (see L<FS::cust_pkg>) for this invoice line item.
341
342 =cut
343
344 sub cust_pkg {
345   my $self = shift;
346   carp "$me $self -> cust_pkg" if $DEBUG;
347   qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
348 }
349
350 =item part_pkg
351
352 Returns the package definition for this invoice line item.
353
354 =cut
355
356 sub part_pkg {
357   my $self = shift;
358   if ( $self->pkgpart_override ) {
359     qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } );
360   } else {
361     my $part_pkg;
362     my $cust_pkg = $self->cust_pkg;
363     $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
364     $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 > 1;
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 =item cust_bill_pkg_tax_Xlocation
784
785 Returns the list of associated cust_bill_pkg_tax_location and/or
786 cust_bill_pkg_tax_rate_location objects
787
788 =cut
789
790 sub cust_bill_pkg_tax_Xlocation {
791   my $self = shift;
792
793   my %hash = ( 'billpkgnum' => $self->billpkgnum );
794
795   (
796     qsearch ( 'cust_bill_pkg_tax_location', { %hash  } ),
797     qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
798   );
799
800 }
801
802 =back
803
804 =head1 BUGS
805
806 setup and recur shouldn't be separate fields.  There should be one "amount"
807 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
808
809 A line item with both should really be two separate records (preserving
810 sdate and edate for setup fees for recurring packages - that information may
811 be valuable later).  Invoice generation (cust_main::bill), invoice printing
812 (cust_bill), tax reports (report_tax.cgi) and line item reports 
813 (cust_bill_pkg.cgi) would need to be updated.
814
815 owed_setup and owed_recur could then be repaced by just owed, and
816 cust_bill::open_cust_bill_pkg and
817 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
818
819 =head1 SEE ALSO
820
821 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
822 from the base documentation.
823
824 =cut
825
826 1;
827