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