optimize invoice rendering with lots of CDRs, RT#15155
[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
830   if ( $self->get('details') ) {
831
832     foreach my $value (
833       map { ref($_) eq 'HASH'
834               ? $_->{'amount'}
835               : $_->[2] 
836           }
837       grep { ref($_) && ( defined($classnum)
838                             ? $classnum eq ( ref($_) eq 'HASH'
839                                                ? $_->{'classnum'}
840                                                : $_->[3]
841                                            )
842                             : 1
843                         )
844            }
845       @{ $self->get('details') }
846     ) {
847       $sum += $value if $value;
848     }
849
850     return $sum;
851
852   } else {
853
854     my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
855               ' WHERE billpkgnum = '. $self->billpkgnum;
856     $sql .= " AND classnum = $classnum" if defined($classnum);
857
858     my $sth = dbh->prepare($sql) or die dbh->errstr;
859     $sth->execute or die $sth->errstr;
860
861     return $sth->fetchrow_arrayref->[0];
862
863   }
864
865 }
866
867 =item usage_classes
868
869 Returns a list of usage classnums associated with this invoice line's
870 details.
871   
872 =cut
873
874 sub usage_classes {
875   my( $self ) = @_;
876
877   if ( $self->get('details') ) {
878
879     my %seen = ();
880     foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
881       $seen{ ref($detail) eq 'HASH'
882                ? $detail->{'classnum'}
883                : $detail->[3]
884            } = 1;
885     }
886     keys %seen;
887
888   } else {
889
890     map { $_->classnum }
891         qsearch({ table   => 'cust_bill_pkg_detail',
892                   hashref => { billpkgnum => $self->billpkgnum },
893                   select  => 'DISTINCT classnum',
894                });
895
896   }
897
898 }
899
900 =item cust_bill_pkg_display [ type => TYPE ]
901
902 Returns an array of display information for the invoice line item optionally
903 limited to 'TYPE'.
904
905 =cut
906
907 sub cust_bill_pkg_display {
908   my ( $self, %opt ) = @_;
909
910   my $default =
911     new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
912
913   return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
914
915   my $type = $opt{type} if exists $opt{type};
916   my @result;
917
918   if ( $self->get('display') ) {
919     @result = grep { defined($type) ? ($type eq $_->type) : 1 }
920               @{ $self->get('display') };
921   } else {
922     my $hashref = { 'billpkgnum' => $self->billpkgnum };
923     $hashref->{type} = $type if defined($type);
924     
925     @result = qsearch ({ 'table'    => 'cust_bill_pkg_display',
926                          'hashref'  => { 'billpkgnum' => $self->billpkgnum },
927                          'order_by' => 'ORDER BY billpkgdisplaynum',
928                       });
929   }
930
931   push @result, $default unless ( scalar(@result) || $type );
932
933   @result;
934
935 }
936
937 # reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
938 # and FS::cust_main::bill
939
940 sub _cust_tax_exempt_pkg {
941   my ( $self ) = @_;
942
943   $self->{Hash}->{_cust_tax_exempt_pkg} or
944   $self->{Hash}->{_cust_tax_exempt_pkg} = [];
945
946 }
947
948 =item cust_bill_pkg_tax_Xlocation
949
950 Returns the list of associated cust_bill_pkg_tax_location and/or
951 cust_bill_pkg_tax_rate_location objects
952
953 =cut
954
955 sub cust_bill_pkg_tax_Xlocation {
956   my $self = shift;
957
958   my %hash = ( 'billpkgnum' => $self->billpkgnum );
959
960   (
961     qsearch ( 'cust_bill_pkg_tax_location', { %hash  } ),
962     qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
963   );
964
965 }
966
967 =item cust_bill_pkg_detail [ CLASSNUM ]
968
969 Returns the list of associated cust_bill_pkg_detail objects
970 The optional CLASSNUM argument will limit the details to the specified usage
971 class.
972
973 =cut
974
975 sub cust_bill_pkg_detail {
976   my $self = shift;
977   my $classnum = shift || '';
978
979   my %hash = ( 'billpkgnum' => $self->billpkgnum );
980   $hash{classnum} = $classnum if $classnum;
981
982   qsearch( 'cust_bill_pkg_detail', \%hash ),
983
984 }
985
986 =item cust_bill_pkg_discount 
987
988 Returns the list of associated cust_bill_pkg_discount objects.
989
990 =cut
991
992 sub cust_bill_pkg_discount {
993   my $self = shift;
994   qsearch( 'cust_bill_pkg_discount', { 'billpkgnum' => $self->billpkgnum } );
995 }
996
997 =item recur_show_zero
998
999 =cut
1000
1001 sub recur_show_zero {
1002   #my $self = shift;
1003   #   $self->recur == 0
1004   #&& $self->pkgnum
1005   #&& $self->cust_pkg->part_pkg->recur_show_zero;
1006
1007   shift->_X_show_zero('recur');
1008
1009 }
1010
1011 sub setup_show_zero {
1012   shift->_X_show_zero('setup');
1013 }
1014
1015 sub _X_show_zero {
1016   my( $self, $what ) = @_;
1017
1018   return 0 unless $self->$what() == 0 && $self->pkgnum;
1019
1020   $self->cust_pkg->_X_show_zero($what);
1021 }
1022
1023 =back
1024
1025 =head1 BUGS
1026
1027 setup and recur shouldn't be separate fields.  There should be one "amount"
1028 field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
1029
1030 A line item with both should really be two separate records (preserving
1031 sdate and edate for setup fees for recurring packages - that information may
1032 be valuable later).  Invoice generation (cust_main::bill), invoice printing
1033 (cust_bill), tax reports (report_tax.cgi) and line item reports 
1034 (cust_bill_pkg.cgi) would need to be updated.
1035
1036 owed_setup and owed_recur could then be repaced by just owed, and
1037 cust_bill::open_cust_bill_pkg and
1038 cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
1039
1040 =head1 SEE ALSO
1041
1042 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html
1043 from the base documentation.
1044
1045 =cut
1046
1047 1;
1048