silence a warning
[freeside.git] / FS / FS / cust_bill_pkg.pm
index 72f9ce4..267804b 100644 (file)
@@ -1,12 +1,28 @@
 package FS::cust_bill_pkg;
 
 use strict;
-use vars qw( @ISA );
-use FS::Record qw( qsearchs );
+use vars qw( @ISA $DEBUG $me );
+use Carp;
+use Text::CSV_XS;
+use FS::Record qw( qsearch qsearchs dbdef dbh );
+use FS::cust_main_Mixin;
 use FS::cust_pkg;
+use FS::part_pkg;
 use FS::cust_bill;
+use FS::cust_bill_pkg_detail;
+use FS::cust_bill_pkg_display;
+use FS::cust_bill_pkg_discount;
+use FS::cust_bill_pay_pkg;
+use FS::cust_credit_bill_pkg;
+use FS::cust_tax_exempt_pkg;
+use FS::cust_bill_pkg_tax_location;
+use FS::cust_bill_pkg_tax_rate_location;
+use FS::cust_tax_adjustment;
 
-@ISA = qw(FS::Record );
+@ISA = qw( FS::cust_main_Mixin FS::Record );
+
+$DEBUG = 0;
+$me = '[FS::cust_bill_pkg]';
 
 =head1 NAME
 
@@ -21,10 +37,6 @@ FS::cust_bill_pkg - Object methods for cust_bill_pkg records
 
   $error = $record->insert;
 
-  $error = $new_record->replace($old_record);
-
-  $error = $record->delete;
-
   $error = $record->check;
 
 =head1 DESCRIPTION
@@ -35,17 +47,57 @@ supported:
 
 =over 4
 
-=item invnum - invoice (see L<FS::cust_bill>)
+=item billpkgnum
+
+primary key
+
+=item invnum
+
+invoice (see L<FS::cust_bill>)
+
+=item pkgnum
+
+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)
+
+=item pkgpart_override
+
+optional package definition (see L<FS::part_pkg>) override
+
+=item setup
+
+setup fee
+
+=item recur
+
+recurring fee
+
+=item sdate
+
+starting date of recurring fee
+
+=item edate
+
+ending date of recurring fee
+
+=item itemdesc
+
+Line item description (overrides normal package description)
 
-=item pkgnum - package (see L<FS::cust_pkg>) or 0 for the special virtual sales tax package
+=item quantity
 
-=item setup - setup fee
+If not set, defaults to 1
 
-=item recur - recurring fee
+=item unitsetup
 
-=item sdate - starting date of recurring fee
+If not set, defaults to setup
 
-=item edate - ending date of recurring fee
+=item unitrecur
+
+If not set, defaults to recur
+
+=item hidden
+
+If set to Y, indicates data should not appear as separate line item on invoice
 
 =back
 
@@ -71,28 +123,209 @@ sub table { 'cust_bill_pkg'; }
 Adds this line item to the database.  If there is an error, returns the error,
 otherwise returns false.
 
+=cut
+
+sub insert {
+  my $self = shift;
+
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
+
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
+
+  my $error = $self->SUPER::insert;
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  if ( $self->get('details') ) {
+    foreach my $detail ( @{$self->get('details')} ) {
+      my %hash = ();
+      if ( ref($detail) ) {
+        if ( ref($detail) eq 'ARRAY' ) {
+          #carp "this way sucks, use a hash"; #but more useful/friendly
+          $hash{'format'}      = $detail->[0];
+          $hash{'detail'}      = $detail->[1];
+          $hash{'amount'}      = $detail->[2];
+          $hash{'classnum'}    = $detail->[3];
+          $hash{'phonenum'}    = $detail->[4];
+          $hash{'accountcode'} = $detail->[5];
+          $hash{'startdate'}   = $detail->[6];
+          $hash{'duration'}    = $detail->[7];
+          $hash{'regionname'}  = $detail->[8];
+        } elsif ( ref($detail) eq 'HASH' ) {
+          %hash = %$detail;
+        } else {
+          die "unknow detail type ". ref($detail);
+        }
+      } else {
+        $hash{'detail'} = $detail;
+      }
+      $hash{'billpkgnum'} = $self->billpkgnum;
+      my $cust_bill_pkg_detail = new FS::cust_bill_pkg_detail \%hash;
+      $error = $cust_bill_pkg_detail->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "error inserting cust_bill_pkg_detail: $error";
+      }
+    }
+  }
+
+  if ( $self->get('display') ) {
+    foreach my $cust_bill_pkg_display ( @{ $self->get('display') } ) {
+      $cust_bill_pkg_display->billpkgnum($self->billpkgnum);
+      $error = $cust_bill_pkg_display->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "error inserting cust_bill_pkg_display: $error";
+      }
+    }
+  }
+
+  if ( $self->get('discounts') ) {
+    foreach my $cust_bill_pkg_discount ( @{$self->get('discounts')} ) {
+      $cust_bill_pkg_discount->billpkgnum($self->billpkgnum);
+      $error = $cust_bill_pkg_discount->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "error inserting cust_bill_pkg_discount: $error";
+      }
+    }
+  }
+
+  if ( $self->_cust_tax_exempt_pkg ) {
+    foreach my $cust_tax_exempt_pkg ( @{$self->_cust_tax_exempt_pkg} ) {
+      $cust_tax_exempt_pkg->billpkgnum($self->billpkgnum);
+      $error = $cust_tax_exempt_pkg->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "error inserting cust_tax_exempt_pkg: $error";
+      }
+    }
+  }
+
+  my $tax_location = $self->get('cust_bill_pkg_tax_location');
+  if ( $tax_location ) {
+    foreach my $cust_bill_pkg_tax_location ( @$tax_location ) {
+      $cust_bill_pkg_tax_location->billpkgnum($self->billpkgnum);
+      $error = $cust_bill_pkg_tax_location->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "error inserting cust_bill_pkg_tax_location: $error";
+      }
+    }
+  }
+
+  my $tax_rate_location = $self->get('cust_bill_pkg_tax_rate_location');
+  if ( $tax_rate_location ) {
+    foreach my $cust_bill_pkg_tax_rate_location ( @$tax_rate_location ) {
+      $cust_bill_pkg_tax_rate_location->billpkgnum($self->billpkgnum);
+      $error = $cust_bill_pkg_tax_rate_location->insert;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return "error inserting cust_bill_pkg_tax_rate_location: $error";
+      }
+    }
+  }
+
+  my $cust_tax_adjustment = $self->get('cust_tax_adjustment');
+  if ( $cust_tax_adjustment ) {
+    $cust_tax_adjustment->billpkgnum($self->billpkgnum);
+    $error = $cust_tax_adjustment->replace;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return "error replacing cust_tax_adjustment: $error";
+    }
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+  '';
+
+}
+
 =item delete
 
-Currently unimplemented.  I don't remove line items because there would then be
-no record the items ever existed (which is bad, no?)
+Not recommended.
 
 =cut
 
 sub delete {
-  return "Can't delete cust_bill_pkg records!";
-}
+  my $self = shift;
 
-=item replace OLD_RECORD
+  local $SIG{HUP} = 'IGNORE';
+  local $SIG{INT} = 'IGNORE';
+  local $SIG{QUIT} = 'IGNORE';
+  local $SIG{TERM} = 'IGNORE';
+  local $SIG{TSTP} = 'IGNORE';
+  local $SIG{PIPE} = 'IGNORE';
 
-Currently unimplemented.  This would be even more of an accounting nightmare
-than deleteing the items.  Just don't do it.
+  my $oldAutoCommit = $FS::UID::AutoCommit;
+  local $FS::UID::AutoCommit = 0;
+  my $dbh = dbh;
 
-=cut
+  foreach my $table (qw(
+    cust_bill_pkg_detail
+    cust_bill_pkg_display
+    cust_bill_pkg_tax_location
+    cust_bill_pkg_tax_rate_location
+    cust_tax_exempt_pkg
+    cust_bill_pay_pkg
+    cust_credit_bill_pkg
+  )) {
+
+    foreach my $linked ( qsearch($table, { billpkgnum=>$self->billpkgnum }) ) {
+      my $error = $linked->delete;
+      if ( $error ) {
+        $dbh->rollback if $oldAutoCommit;
+        return $error;
+      }
+    }
+
+  }
+
+  foreach my $cust_tax_adjustment (
+    qsearch('cust_tax_adjustment', { billpkgnum=>$self->billpkgnum })
+  ) {
+    $cust_tax_adjustment->billpkgnum(''); #NULL
+    my $error = $cust_tax_adjustment->replace;
+    if ( $error ) {
+      $dbh->rollback if $oldAutoCommit;
+      return $error;
+    }
+  }
+
+  my $error = $self->SUPER::delete(@_);
+  if ( $error ) {
+    $dbh->rollback if $oldAutoCommit;
+    return $error;
+  }
+
+  $dbh->commit or die $dbh->errstr if $oldAutoCommit;
+
+  '';
 
-sub replace {
-  return "Can't modify cust_bill_pkg records!";
 }
 
+#alas, bin/follow-tax-rename
+#
+#=item replace OLD_RECORD
+#
+#Currently unimplemented.  This would be even more of an accounting nightmare
+#than deleteing the items.  Just don't do it.
+#
+#=cut
+#
+#sub replace {
+#  return "Can't modify cust_bill_pkg records!";
+#}
+
 =item check
 
 Checks all fields to make sure this is a valid line item.  If there is an
@@ -105,16 +338,21 @@ sub check {
   my $self = shift;
 
   my $error =
-    $self->ut_number('pkgnum')
+         $self->ut_numbern('billpkgnum')
+      || $self->ut_snumber('pkgnum')
       || $self->ut_number('invnum')
       || $self->ut_money('setup')
       || $self->ut_money('recur')
       || $self->ut_numbern('sdate')
       || $self->ut_numbern('edate')
+      || $self->ut_textn('itemdesc')
+      || $self->ut_textn('itemcomment')
+      || $self->ut_enum('hidden', [ '', 'Y' ])
   ;
   return $error if $error;
 
-  if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
+  #if ( $self->pkgnum != 0 ) { #allow unchecked pkgnum 0 for tax! (add to part_pkg?)
+  if ( $self->pkgnum > 0 ) { #allow -1 for non-pkg line items and 0 for tax (add to part_pkg?)
     return "Unknown pkgnum ". $self->pkgnum
       unless qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
   }
@@ -122,7 +360,7 @@ sub check {
   return "Unknown invnum"
     unless qsearchs( 'cust_bill' ,{ 'invnum' => $self->invnum } );
 
-  ''; #no error
+  $self->SUPER::check;
 }
 
 =item cust_pkg
@@ -133,17 +371,710 @@ Returns the package (see L<FS::cust_pkg>) for this invoice line item.
 
 sub cust_pkg {
   my $self = shift;
+  carp "$me $self -> cust_pkg" if $DEBUG;
   qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
 }
 
-=back
+=item part_pkg
+
+Returns the package definition for this invoice line item.
+
+=cut
+
+sub part_pkg {
+  my $self = shift;
+  if ( $self->pkgpart_override ) {
+    qsearchs('part_pkg', { 'pkgpart' => $self->pkgpart_override } );
+  } else {
+    my $part_pkg;
+    my $cust_pkg = $self->cust_pkg;
+    $part_pkg = $cust_pkg->part_pkg if $cust_pkg;
+    $part_pkg;
+  }
+}
+
+=item cust_bill
 
-=head1 VERSION
+Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
+
+=cut
+
+sub cust_bill {
+  my $self = shift;
+  qsearchs( 'cust_bill', { 'invnum' => $self->invnum } );
+}
+
+=item previous_cust_bill_pkg
+
+Returns the previous cust_bill_pkg for this package, if any.
+
+=cut
 
-$Id: cust_bill_pkg.pm,v 1.3 2002-04-06 22:32:43 ivan Exp $
+sub previous_cust_bill_pkg {
+  my $self = shift;
+  return unless $self->sdate;
+  qsearchs({
+    'table'    => 'cust_bill_pkg',
+    'hashref'  => { 'pkgnum' => $self->pkgnum,
+                    'sdate'  => { op=>'<', value=>$self->sdate },
+                  },
+    'order_by' => 'ORDER BY sdate DESC LIMIT 1',
+  });
+}
+
+=item details [ OPTION => VALUE ... ]
+
+Returns an array of detail information for the invoice line item.
+
+Currently available options are: I<format>, I<escape_function> and
+I<format_function>.
+
+If I<format> is set to html or latex then the array members are improved
+for tabular appearance in those environments if possible.
+
+If I<escape_function> is set then the array members are processed by this
+function before being returned.
+
+I<format_function> overrides the normal HTML or LaTeX function for returning
+formatted CDRs.  It can be set to a subroutine which returns an empty list
+to skip usage detail:
+
+  'format_function' => sub { () },
+
+=cut
+
+sub details {
+  my ( $self, %opt ) = @_;
+  my $escape_function = $opt{escape_function} || sub { shift };
+
+  my $csv = new Text::CSV_XS;
+
+  if ( $opt{format_function} ) {
+
+    #this still expects to be passed a cust_bill_pkg_detail object as the
+    #second argument, which is expensive
+    carp "deprecated format_function passed to cust_bill_pkg->details";
+    my $format_sub = $opt{format_function} if $opt{format_function};
+
+    map { ( $_->format eq 'C'
+              ? &{$format_sub}( $_->detail, $_ )
+              : &{$escape_function}( $_->detail )
+          )
+        }
+      qsearch ({ 'table'    => 'cust_bill_pkg_detail',
+                 'hashref'  => { 'billpkgnum' => $self->billpkgnum },
+                 'order_by' => 'ORDER BY detailnum',
+              });
+
+  } elsif ( $opt{'no_usage'} ) {
+
+    my $sql = "SELECT detail FROM cust_bill_pkg_detail ".
+              "  WHERE billpkgnum = ". $self->billpkgnum.
+              "    AND ( format IS NULL OR format != 'C' ) ".
+              "  ORDER BY detailnum";
+    my $sth = dbh->prepare($sql) or die dbh->errstr;
+    $sth->execute or die $sth->errstr;
+
+    map &{$escape_function}( $_->[0] ), @{ $sth->fetchall_arrayref };
+
+  } else {
+
+    my $format_sub;
+    my $format = $opt{format} || '';
+    if ( $format eq 'html' ) {
+
+      $format_sub = sub { my $detail = shift;
+                          $csv->parse($detail) or return "can't parse $detail";
+                          join('</TD><TD>', map { &$escape_function($_) }
+                                            $csv->fields
+                              );
+                        };
+
+    } elsif ( $format eq 'latex' ) {
+
+      $format_sub = sub {
+        my $detail = shift;
+        $csv->parse($detail) or return "can't parse $detail";
+        #join(' & ', map { '\small{'. &$escape_function($_). '}' }
+        #            $csv->fields );
+        my $result = '';
+        my $column = 1;
+        foreach ($csv->fields) {
+          $result .= ' & ' if $column > 1;
+          if ($column > 6) {                     # KLUDGE ALERT!
+            $result .= '\multicolumn{1}{l}{\scriptsize{'.
+                       &$escape_function($_). '}}';
+          }else{
+            $result .= '\scriptsize{'.  &$escape_function($_). '}';
+          }
+          $column++;
+        }
+        $result;
+      };
+
+    } else {
+
+      $format_sub = sub { my $detail = shift;
+                          $csv->parse($detail) or return "can't parse $detail";
+                          join(' - ', map { &$escape_function($_) }
+                                      $csv->fields
+                              );
+                        };
+
+    }
+
+    my $sql = "SELECT format, detail FROM cust_bill_pkg_detail ".
+              "  WHERE billpkgnum = ". $self->billpkgnum.
+              "  ORDER BY detailnum";
+    my $sth = dbh->prepare($sql) or die dbh->errstr;
+    $sth->execute or die $sth->errstr;
+
+    #avoid the fetchall_arrayref and loop for less memory usage?
+
+    map { (defined($_->[0]) && $_->[0] eq 'C')
+            ? &{$format_sub}(      $_->[1] )
+            : &{$escape_function}( $_->[1] );
+        }
+      @{ $sth->fetchall_arrayref };
+
+  }
+
+}
+
+=item details_header [ OPTION => VALUE ... ]
+
+Returns a list representing an invoice line item detail header, if any.
+This relies on the behavior of voip_cdr in that it expects the header
+to be the first CSV formatted detail (as is expected by invoice generation
+routines).  Returns the empty list otherwise.
+
+=cut
+
+sub details_header {
+  my $self = shift;
+  return '' unless defined dbdef->table('cust_bill_pkg_detail');
+
+  my $csv = new Text::CSV_XS;
+
+  my @detail = 
+    qsearch ({ 'table'    => 'cust_bill_pkg_detail',
+               'hashref'  => { 'billpkgnum' => $self->billpkgnum,
+                               'format'     => 'C',
+                             },
+               'order_by' => 'ORDER BY detailnum LIMIT 1',
+            });
+  return() unless scalar(@detail);
+  $csv->parse($detail[0]->detail) or return ();
+  $csv->fields;
+}
+
+=item desc
+
+Returns a description for this line item.  For typical line items, this is the
+I<pkg> field of the corresponding B<FS::part_pkg> object (see L<FS::part_pkg>).
+For one-shot line items and named taxes, it is the I<itemdesc> field of this
+line item, and for generic taxes, simply returns "Tax".
+
+=cut
+
+sub desc {
+  my $self = shift;
+
+  if ( $self->pkgnum > 0 ) {
+    $self->itemdesc || $self->part_pkg->pkg;
+  } else {
+    my $desc = $self->itemdesc || 'Tax';
+    $desc .= ' '. $self->itemcomment if $self->itemcomment =~ /\S/;
+    $desc;
+  }
+}
+
+=item owed_setup
+
+Returns the amount owed (still outstanding) on this line item's setup fee,
+which is the amount of the line item minus all payment applications (see
+L<FS::cust_bill_pay_pkg> and credit applications (see
+L<FS::cust_credit_bill_pkg>).
+
+=cut
+
+sub owed_setup {
+  my $self = shift;
+  $self->owed('setup', @_);
+}
+
+=item owed_recur
+
+Returns the amount owed (still outstanding) on this line item's recurring fee,
+which is the amount of the line item minus all payment applications (see
+L<FS::cust_bill_pay_pkg> and credit applications (see
+L<FS::cust_credit_bill_pkg>).
+
+=cut
+
+sub owed_recur {
+  my $self = shift;
+  $self->owed('recur', @_);
+}
+
+# modeled after cust_bill::owed...
+sub owed {
+  my( $self, $field ) = @_;
+  my $balance = $self->$field();
+  $balance -= $_->amount foreach ( $self->cust_bill_pay_pkg($field) );
+  $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
+  $balance = sprintf( '%.2f', $balance );
+  $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
+  $balance;
+}
+
+#modeled after owed
+sub payable {
+  my( $self, $field ) = @_;
+  my $balance = $self->$field();
+  $balance -= $_->amount foreach ( $self->cust_credit_bill_pkg($field) );
+  $balance = sprintf( '%.2f', $balance );
+  $balance =~ s/^\-0\.00$/0.00/; #yay ieee fp
+  $balance;
+}
+
+sub cust_bill_pay_pkg {
+  my( $self, $field ) = @_;
+  qsearch( 'cust_bill_pay_pkg', { 'billpkgnum' => $self->billpkgnum,
+                                  'setuprecur' => $field,
+                                }
+         );
+}
+
+sub cust_credit_bill_pkg {
+  my( $self, $field ) = @_;
+  qsearch( 'cust_credit_bill_pkg', { 'billpkgnum' => $self->billpkgnum,
+                                     'setuprecur' => $field,
+                                   }
+         );
+}
+
+=item units
+
+Returns the number of billing units (for tax purposes) represented by this,
+line item.
+
+=cut
+
+sub units {
+  my $self = shift;
+  $self->pkgnum ? $self->part_pkg->calc_units($self->cust_pkg) : 0; # 1?
+}
+
+=item quantity
+
+=cut
+
+sub quantity {
+  my( $self, $value ) = @_;
+  if ( defined($value) ) {
+    $self->setfield('quantity', $value);
+  }
+  $self->getfield('quantity') || 1;
+}
+
+=item unitsetup
+
+=cut
+
+sub unitsetup {
+  my( $self, $value ) = @_;
+  if ( defined($value) ) {
+    $self->setfield('unitsetup', $value);
+  }
+  $self->getfield('unitsetup') eq ''
+    ? $self->getfield('setup')
+    : $self->getfield('unitsetup');
+}
+
+=item unitrecur
+
+=cut
+
+sub unitrecur {
+  my( $self, $value ) = @_;
+  if ( defined($value) ) {
+    $self->setfield('unitrecur', $value);
+  }
+  $self->getfield('unitrecur') eq ''
+    ? $self->getfield('recur')
+    : $self->getfield('unitrecur');
+}
+
+=item set_display OPTION => VALUE ...
+
+A helper method for I<insert>, populates the pseudo-field B<display> with
+appropriate FS::cust_bill_pkg_display objects.
+
+Options are passed as a list of name/value pairs.  Options are:
+
+part_pkg: FS::part_pkg object from the 
+
+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.
+
+=cut
+
+sub set_display {
+  my( $self, %opt ) = @_;
+  my $part_pkg = $opt{'part_pkg'};
+  my $cust_pkg = new FS::cust_pkg { pkgpart => $opt{real_pkgpart} };
+
+  my $conf = new FS::Conf;
+
+  my $separate = $conf->exists('separate_usage');
+  my $usage_mandate =            $part_pkg->option('usage_mandate', 'Hush!')
+                    || $cust_pkg->part_pkg->option('usage_mandate', 'Hush!');
+
+  # or use the category from $opt{'part_pkg'} if its not bundled?
+  my $categoryname = $cust_pkg->part_pkg->categoryname;
+
+  return $self->set('display', [])
+    unless $separate || $categoryname || $usage_mandate;
+  
+  my @display = ();
+
+  my %hash = ( 'section' => $categoryname );
+
+  my $usage_section =            $part_pkg->option('usage_section', 'Hush!')
+                    || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
+
+  my $summary =            $part_pkg->option('summarize_usage', 'Hush!')
+              || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
+
+  if ( $separate ) {
+    push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
+    push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
+  } else {
+    push @display, new FS::cust_bill_pkg_display
+                     { type => '',
+                       %hash,
+                       ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
+                     };
+  }
+
+  if ($separate && $usage_section && $summary) {
+    push @display, new FS::cust_bill_pkg_display { type    => 'U',
+                                                   summary => 'Y',
+                                                   %hash,
+                                                 };
+  }
+  if ($usage_mandate || ($usage_section && $summary) ) {
+    $hash{post_total} = 'Y';
+  }
+
+  if ($separate || $usage_mandate) {
+    $hash{section} = $usage_section if $usage_section;
+    push @display, new FS::cust_bill_pkg_display { type => 'U', %hash };
+  }
+
+  $self->set('display', \@display);
+
+}
+
+=item disintegrate
+
+Returns a list of cust_bill_pkg objects each with no more than a single class
+(including setup or recur) of charge.
+
+=cut
+
+sub disintegrate {
+  my $self = shift;
+  # XXX this goes away with cust_bill_pkg refactor
+
+  my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash };
+  my %cust_bill_pkg = ();
+
+  $cust_bill_pkg{setup} = $cust_bill_pkg if $cust_bill_pkg->setup;
+  $cust_bill_pkg{recur} = $cust_bill_pkg if $cust_bill_pkg->recur;
+
+
+  #split setup and recur
+  if ($cust_bill_pkg->setup && $cust_bill_pkg->recur) {
+    my $cust_bill_pkg_recur = new FS::cust_bill_pkg { $cust_bill_pkg->hash };
+    $cust_bill_pkg->set('details', []);
+    $cust_bill_pkg->recur(0);
+    $cust_bill_pkg->unitrecur(0);
+    $cust_bill_pkg->type('');
+    $cust_bill_pkg_recur->setup(0);
+    $cust_bill_pkg_recur->unitsetup(0);
+    $cust_bill_pkg{recur} = $cust_bill_pkg_recur;
+
+  }
+
+  #split usage from recur
+  my $usage = sprintf( "%.2f", $cust_bill_pkg{recur}->usage )
+    if exists($cust_bill_pkg{recur});
+  warn "usage is $usage\n" if $DEBUG > 1;
+  if ($usage) {
+    my $cust_bill_pkg_usage =
+        new FS::cust_bill_pkg { $cust_bill_pkg{recur}->hash };
+    $cust_bill_pkg_usage->recur( $usage );
+    $cust_bill_pkg_usage->type( 'U' );
+    my $recur = sprintf( "%.2f", $cust_bill_pkg{recur}->recur - $usage );
+    $cust_bill_pkg{recur}->recur( $recur );
+    $cust_bill_pkg{recur}->type( '' );
+    $cust_bill_pkg{recur}->set('details', []);
+    $cust_bill_pkg{''} = $cust_bill_pkg_usage;
+  }
+
+  #subdivide usage by usage_class
+  if (exists($cust_bill_pkg{''})) {
+    foreach my $class (grep { $_ } $self->usage_classes) {
+      my $usage = sprintf( "%.2f", $cust_bill_pkg{''}->usage($class) );
+      my $cust_bill_pkg_usage =
+          new FS::cust_bill_pkg { $cust_bill_pkg{''}->hash };
+      $cust_bill_pkg_usage->recur( $usage );
+      $cust_bill_pkg_usage->set('details', []);
+      my $classless = sprintf( "%.2f", $cust_bill_pkg{''}->recur - $usage );
+      $cust_bill_pkg{''}->recur( $classless );
+      $cust_bill_pkg{$class} = $cust_bill_pkg_usage;
+    }
+    warn "Unexpected classless usage value: ". $cust_bill_pkg{''}->recur
+      if ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur < 0);
+    delete $cust_bill_pkg{''}
+      unless ($cust_bill_pkg{''}->recur && $cust_bill_pkg{''}->recur > 0);
+  }
+
+#  # sort setup,recur,'', and the rest numeric && return
+#  my @result = map { $cust_bill_pkg{$_} }
+#               sort { my $ad = ($a=~/^\d+$/); my $bd = ($b=~/^\d+$/);
+#                      ( $ad cmp $bd ) || ( $ad ? $a<=>$b : $b cmp $a )
+#                    }
+#               keys %cust_bill_pkg;
+#
+#  return (@result);
+
+   %cust_bill_pkg;
+}
+
+=item usage CLASSNUM
+
+Returns the amount of the charge associated with usage class CLASSNUM if
+CLASSNUM is defined.  Otherwise returns the total charge associated with
+usage.
+  
+=cut
+
+sub usage {
+  my( $self, $classnum ) = @_;
+
+  if ( $self->get('details') ) {
+
+    my $sum = 0;
+    foreach my $value (
+      map { ref($_) eq 'HASH'
+              ? $_->{'amount'}
+              : $_->[2] 
+          }
+      grep { ref($_) && ( defined($classnum)
+                            ? $classnum eq ( ref($_) eq 'HASH'
+                                               ? $_->{'classnum'}
+                                               : $_->[3]
+                                           )
+                            : 1
+                        )
+           }
+      @{ $self->get('details') }
+    ) {
+      $sum += $value if $value;
+    }
+
+    return $sum;
+
+  } else {
+
+    my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
+              ' WHERE billpkgnum = '. $self->billpkgnum;
+    $sql .= " AND classnum = $classnum" if defined($classnum);
+
+    my $sth = dbh->prepare($sql) or die dbh->errstr;
+    $sth->execute or die $sth->errstr;
+
+    return $sth->fetchrow_arrayref->[0];
+
+  }
+
+}
+
+=item usage_classes
+
+Returns a list of usage classnums associated with this invoice line's
+details.
+  
+=cut
+
+sub usage_classes {
+  my( $self ) = @_;
+
+  if ( $self->get('details') ) {
+
+    my %seen = ();
+    foreach my $detail ( grep { ref($_) } @{$self->get('details')} ) {
+      $seen{ ref($detail) eq 'HASH'
+               ? $detail->{'classnum'}
+               : $detail->[3]
+           } = 1;
+    }
+    keys %seen;
+
+  } else {
+
+    map { $_->classnum }
+        qsearch({ table   => 'cust_bill_pkg_detail',
+                  hashref => { billpkgnum => $self->billpkgnum },
+                  select  => 'DISTINCT classnum',
+               });
+
+  }
+
+}
+
+=item cust_bill_pkg_display [ type => TYPE ]
+
+Returns an array of display information for the invoice line item optionally
+limited to 'TYPE'.
+
+=cut
+
+sub cust_bill_pkg_display {
+  my ( $self, %opt ) = @_;
+
+  my $default =
+    new FS::cust_bill_pkg_display { billpkgnum =>$self->billpkgnum };
+
+  return ( $default ) unless defined dbdef->table('cust_bill_pkg_display');#hmmm
+
+  my $type = $opt{type} if exists $opt{type};
+  my @result;
+
+  if ( $self->get('display') ) {
+    @result = grep { defined($type) ? ($type eq $_->type) : 1 }
+              @{ $self->get('display') };
+  } else {
+    my $hashref = { 'billpkgnum' => $self->billpkgnum };
+    $hashref->{type} = $type if defined($type);
+    
+    @result = qsearch ({ 'table'    => 'cust_bill_pkg_display',
+                         'hashref'  => { 'billpkgnum' => $self->billpkgnum },
+                         'order_by' => 'ORDER BY billpkgdisplaynum',
+                      });
+  }
+
+  push @result, $default unless ( scalar(@result) || $type );
+
+  @result;
+
+}
+
+# reserving this name for my friends FS::{tax_rate|cust_main_county}::taxline
+# and FS::cust_main::bill
+
+sub _cust_tax_exempt_pkg {
+  my ( $self ) = @_;
+
+  $self->{Hash}->{_cust_tax_exempt_pkg} or
+  $self->{Hash}->{_cust_tax_exempt_pkg} = [];
+
+}
+
+=item cust_bill_pkg_tax_Xlocation
+
+Returns the list of associated cust_bill_pkg_tax_location and/or
+cust_bill_pkg_tax_rate_location objects
+
+=cut
+
+sub cust_bill_pkg_tax_Xlocation {
+  my $self = shift;
+
+  my %hash = ( 'billpkgnum' => $self->billpkgnum );
+
+  (
+    qsearch ( 'cust_bill_pkg_tax_location', { %hash  } ),
+    qsearch ( 'cust_bill_pkg_tax_rate_location', { %hash } )
+  );
+
+}
+
+=item cust_bill_pkg_detail [ CLASSNUM ]
+
+Returns the list of associated cust_bill_pkg_detail objects
+The optional CLASSNUM argument will limit the details to the specified usage
+class.
+
+=cut
+
+sub cust_bill_pkg_detail {
+  my $self = shift;
+  my $classnum = shift || '';
+
+  my %hash = ( 'billpkgnum' => $self->billpkgnum );
+  $hash{classnum} = $classnum if $classnum;
+
+  qsearch( 'cust_bill_pkg_detail', \%hash ),
+
+}
+
+=item cust_bill_pkg_discount 
+
+Returns the list of associated cust_bill_pkg_discount objects.
+
+=cut
+
+sub cust_bill_pkg_discount {
+  my $self = shift;
+  qsearch( 'cust_bill_pkg_discount', { 'billpkgnum' => $self->billpkgnum } );
+}
+
+=item recur_show_zero
+
+=cut
+
+sub recur_show_zero {
+  #my $self = shift;
+  #   $self->recur == 0
+  #&& $self->pkgnum
+  #&& $self->cust_pkg->part_pkg->recur_show_zero;
+
+  shift->_X_show_zero('recur');
+
+}
+
+sub setup_show_zero {
+  shift->_X_show_zero('setup');
+}
+
+sub _X_show_zero {
+  my( $self, $what ) = @_;
+
+  return 0 unless $self->$what() == 0 && $self->pkgnum;
+
+  $self->cust_pkg->_X_show_zero($what);
+}
+
+=back
 
 =head1 BUGS
 
+setup and recur shouldn't be separate fields.  There should be one "amount"
+field and a flag to tell you if it is a setup/one-time fee or a recurring fee.
+
+A line item with both should really be two separate records (preserving
+sdate and edate for setup fees for recurring packages - that information may
+be valuable later).  Invoice generation (cust_main::bill), invoice printing
+(cust_bill), tax reports (report_tax.cgi) and line item reports 
+(cust_bill_pkg.cgi) would need to be updated.
+
+owed_setup and owed_recur could then be repaced by just owed, and
+cust_bill::open_cust_bill_pkg and
+cust_bill_ApplicationCommon::apply_to_lineitems could be simplified.
+
 =head1 SEE ALSO
 
 L<FS::Record>, L<FS::cust_bill>, L<FS::cust_pkg>, L<FS::cust_main>, schema.html