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