+ $self->SUPER::check;
+}
+
+=item regularize_details
+
+Converts the contents of the 'details' pseudo-field to
+L<FS::cust_bill_pkg_detail> objects, if they aren't already.
+
+=cut
+
+sub regularize_details {
+ my $self = shift;
+ if ( $self->get('details') ) {
+ foreach my $detail ( @{$self->get('details')} ) {
+ if ( ref($detail) ne 'FS::cust_bill_pkg_detail' ) {
+ # then turn it into one
+ my %hash = ();
+ if ( ! ref($detail) ) {
+ $hash{'detail'} = $detail;
+ }
+ elsif ( ref($detail) eq 'HASH' ) {
+ %hash = %$detail;
+ }
+ elsif ( ref($detail) eq 'ARRAY' ) {
+ carp "passing invoice details as arrays is deprecated";
+ #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];
+ }
+ else {
+ die "unknown detail type ". ref($detail);
+ }
+ $detail = new FS::cust_bill_pkg_detail \%hash;
+ }
+ $detail->billpkgnum($self->billpkgnum) if $self->billpkgnum;
+ }
+ }
+ return;
+}
+
+=item set_exemptions TAXOBJECT, OPTIONS
+
+Sets up tax exemptions. TAXOBJECT is the L<FS::cust_main_county> or
+L<FS::tax_rate> record for the tax.
+
+This will deal with the following cases:
+
+=over 4
+
+=item Fully exempt customers (cust_main.tax flag) or customer classes
+(cust_class.tax).
+
+=item Customers exempt from specific named taxes (cust_main_exemption
+records).
+
+=item Taxes that don't apply to setup or recurring fees
+(cust_main_county.setuptax and recurtax, tax_rate.setuptax and recurtax).
+
+=item Packages that are marked as tax-exempt (part_pkg.setuptax,
+part_pkg.recurtax).
+
+=item Fees that aren't marked as taxable (part_fee.taxable).
+
+=back
+
+It does NOT deal with monthly tax exemptions, which need more context
+than this humble little method cares to deal with.
+
+OPTIONS should include "custnum" => the customer number if this tax line
+hasn't been inserted (which it probably hasn't).
+
+Returns a list of exemption objects, which will also be attached to the
+line item as the 'cust_tax_exempt_pkg' pseudo-field. Inserting the line
+item will insert these records as well.
+
+=cut
+
+sub set_exemptions {
+ my $self = shift;
+ my $tax = shift;
+ my %opt = @_;
+
+ my $part_pkg = $self->part_pkg;
+ my $part_fee = $self->part_fee;
+
+ my $cust_main;
+ my $custnum = $opt{custnum};
+ $custnum ||= $self->cust_bill->custnum if $self->cust_bill;
+
+ $cust_main = FS::cust_main->by_key( $custnum )
+ or die "set_exemptions can't identify customer (pass custnum option)\n";
+
+ my @new_exemptions;
+ my $taxable_charged = $self->setup + $self->recur;
+ return unless $taxable_charged > 0;
+
+ ### Fully exempt customer ###
+ my $exempt_cust;
+ my $conf = FS::Conf->new;
+ if ( $conf->exists('cust_class-tax_exempt') ) {
+ my $cust_class = $cust_main->cust_class;
+ $exempt_cust = $cust_class->tax if $cust_class;
+ } else {
+ $exempt_cust = $cust_main->tax;
+ }
+
+ ### Exemption from named tax ###
+ my $exempt_cust_taxname;
+ if ( !$exempt_cust and $tax->taxname ) {
+ $exempt_cust_taxname = $cust_main->tax_exemption($tax->taxname);
+ }
+
+ if ( $exempt_cust ) {
+
+ push @new_exemptions, FS::cust_tax_exempt_pkg->new({
+ amount => $taxable_charged,
+ exempt_cust => 'Y',
+ });
+ $taxable_charged = 0;
+
+ } elsif ( $exempt_cust_taxname ) {
+
+ push @new_exemptions, FS::cust_tax_exempt_pkg->new({
+ amount => $taxable_charged,
+ exempt_cust_taxname => 'Y',
+ });
+ $taxable_charged = 0;
+
+ }
+
+ my $exempt_setup = ( ($part_fee and not $part_fee->taxable)
+ or ($part_pkg and $part_pkg->setuptax)
+ or $tax->setuptax );
+
+ if ( $exempt_setup
+ and $self->setup > 0
+ and $taxable_charged > 0 ) {
+
+ push @new_exemptions, FS::cust_tax_exempt_pkg->new({
+ amount => $self->setup,
+ exempt_setup => 'Y'
+ });
+ $taxable_charged -= $self->setup;
+
+ }
+
+ my $exempt_recur = ( ($part_fee and not $part_fee->taxable)
+ or ($part_pkg and $part_pkg->recurtax)
+ or $tax->recurtax );
+
+ if ( $exempt_recur
+ and $self->recur > 0
+ and $taxable_charged > 0 ) {
+
+ push @new_exemptions, FS::cust_tax_exempt_pkg->new({
+ amount => $self->recur,
+ exempt_recur => 'Y'
+ });
+ $taxable_charged -= $self->recur;
+
+ }
+
+ foreach (@new_exemptions) {
+ $_->set('taxnum', $tax->taxnum);
+ $_->set('taxtype', ref($tax));
+ }
+
+ push @{ $self->cust_tax_exempt_pkg }, @new_exemptions;
+ return @new_exemptions;
+
+}
+
+=item cust_bill
+
+Returns the invoice (see L<FS::cust_bill>) for this invoice line item.
+
+=item cust_main
+
+Returns the customer (L<FS::cust_main> object) for this line item.
+
+=cut
+
+sub cust_main {
+ # required for cust_main_Mixin equivalence
+ # and use cust_bill instead of cust_pkg because this might not have a
+ # cust_pkg
+ my $self = shift;
+ my $cust_bill = $self->cust_bill or return '';
+ $cust_bill->cust_main;
+}
+
+=item previous_cust_bill_pkg
+
+Returns the previous cust_bill_pkg for this package, if any.
+
+=cut
+
+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 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 _item_discount
+
+If this item has any discounts, returns a hashref in the format used
+by L<FS::Template_Mixin/_items_cust_bill_pkg> to describe the discount(s)
+on an invoice. This will contain the keys 'description', 'amount',
+'ext_description' (an arrayref of text lines describing the discounts),
+and '_is_discount' (a flag).
+
+The value for 'amount' will be negative, and will be scaled for the package
+quantity.
+
+=cut
+
+sub _item_discount {
+ my $self = shift;
+ my %options = @_;
+
+ my @pkg_discounts = $self->pkg_discount;
+ return if @pkg_discounts == 0;
+ # special case: if there are old "discount details" on this line item, don't
+ # show discount line items
+ if ( FS::cust_bill_pkg_detail->count("detail LIKE 'Includes discount%' AND billpkgnum = ?", $self->billpkgnum || 0) > 0 ) {
+ return;
+ }
+
+ my @ext;
+ my $d = {
+ _is_discount => 1,
+ description => $self->mt('Discount'),
+ setup_amount => 0,
+ recur_amount => 0,
+ ext_description => \@ext,
+ pkgpart => $self->pkgpart,
+ feepart => $self->feepart,
+ # maybe should show quantity/unit discount?
+ };
+ foreach my $pkg_discount (@pkg_discounts) {
+ push @ext, $pkg_discount->description;
+ my $setuprecur = $pkg_discount->cust_pkg_discount->setuprecur;
+ $d->{$setuprecur.'_amount'} -= $pkg_discount->amount;
+ }
+ $d->{setup_amount} *= $self->quantity || 1; # ??
+ $d->{recur_amount} *= $self->quantity || 1; # ??
+
+ return $d;
+}
+
+=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 this line item's package.
+
+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;
+
+ # whether to break this down into setup/recur/usage
+ 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;
+
+ # if we don't have to separate setup/recur/usage, or put this in a
+ # package-specific section, or display a usage summary, then don't
+ # even create one of these. The item will just display in the unnamed
+ # section as a single line plus details.
+ return $self->set('display', [])
+ unless $separate || $categoryname || $usage_mandate;
+
+ my @display = ();
+
+ my %hash = ( 'section' => $categoryname );
+
+ # whether to put usage details in a separate section, and if so, which one
+ my $usage_section = $part_pkg->option('usage_section', 'Hush!')
+ || $cust_pkg->part_pkg->option('usage_section', 'Hush!');
+
+ # whether to show a usage summary line (total usage charges, no details)
+ my $summary = $part_pkg->option('summarize_usage', 'Hush!')
+ || $cust_pkg->part_pkg->option('summarize_usage', 'Hush!');
+
+ if ( $separate ) {
+ # create lines for setup and (non-usage) recur, in the main section
+ push @display, new FS::cust_bill_pkg_display { type => 'S', %hash };
+ push @display, new FS::cust_bill_pkg_display { type => 'R', %hash };
+ } else {
+ # display everything in a single line
+ push @display, new FS::cust_bill_pkg_display
+ { type => '',
+ %hash,
+ # and if usage_mandate is enabled, hide details
+ # (this only works on multisection invoices...)
+ ( ( $usage_mandate ) ? ( 'summary' => 'Y' ) : () ),
+ };
+ }
+
+ if ($separate && $usage_section && $summary) {
+ # create a line for the usage summary in the main section
+ 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) {
+ # show call details for this line item in the usage section.
+ # if usage_mandate is on, this will display below the section subtotal.
+ # this also happens if usage is in a separate section and there's a
+ # summary in the main section, though I'm not sure why.
+ $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 hash: keys are "setup", "recur" or usage classnum, values are
+FS::cust_bill_pkg objects, each with no more than a single class (setup or
+recur) of charge.
+
+=cut
+
+sub disintegrate {
+ my $self = shift;
+ # XXX this goes away with cust_bill_pkg refactor
+ # or at least I wish it would, but it turns out to be harder than
+ # that.
+
+ #my $cust_bill_pkg = new FS::cust_bill_pkg { $self->hash }; # wha huh?
+ my %cust_bill_pkg = ();
+
+ my $usage_total;
+ foreach my $classnum ($self->usage_classes) {
+ my $amount = $self->usage($classnum);
+ next if $amount == 0; # though if so we shouldn't be here
+ my $usage_item = FS::cust_bill_pkg->new({
+ $self->hash,
+ 'setup' => 0,
+ 'recur' => $amount,
+ 'taxclass' => $classnum,
+ 'inherit' => $self
+ });
+ $cust_bill_pkg{$classnum} = $usage_item;
+ $usage_total += $amount;
+ }
+
+ foreach (qw(setup recur)) {
+ next if ($self->get($_) == 0);
+ my $item = FS::cust_bill_pkg->new({
+ $self->hash,
+ 'setup' => 0,
+ 'recur' => 0,
+ 'taxclass' => $_,
+ 'inherit' => $self,
+ });
+ $item->set($_, $self->get($_));
+ $cust_bill_pkg{$_} = $item;
+ }
+
+ if ($usage_total) {
+ $cust_bill_pkg{recur}->set('recur',
+ sprintf('%.2f', $cust_bill_pkg{recur}->get('recur') - $usage_total)
+ );
+ }
+
+ %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 ) = @_;
+ $self->regularize_details;
+
+ if ( $self->get('details') ) {
+
+ return sum( 0,
+ map { $_->amount || 0 }
+ grep { !defined($classnum) or $classnum eq $_->classnum }
+ @{ $self->get('details') }
+ );
+
+ } else {
+
+ my $sql = 'SELECT SUM(COALESCE(amount,0)) FROM cust_bill_pkg_detail '.
+ ' WHERE billpkgnum = '. $self->billpkgnum;
+ if (defined $classnum) {
+ if ($classnum =~ /^(\d+)$/) {
+ $sql .= " AND classnum = $1";
+ } elsif ($classnum eq '') {
+ $sql .= " AND classnum IS NULL";
+ }
+ }
+
+ my $sth = dbh->prepare($sql) or die dbh->errstr;
+ $sth->execute or die $sth->errstr;
+
+ return $sth->fetchrow_arrayref->[0] || 0;
+
+ }
+
+}
+
+=item usage_classes
+
+Returns a list of usage classnums associated with this invoice line's
+details.
+
+=cut
+
+sub usage_classes {
+ my( $self ) = @_;
+ $self->regularize_details;
+
+ if ( $self->get('details') ) {
+
+ my %seen = ( map { $_->classnum => 1 } @{ $self->get('details') } );
+ keys %seen;
+
+ } else {
+
+ map { $_->classnum }
+ qsearch({ table => 'cust_bill_pkg_detail',
+ hashref => { billpkgnum => $self->billpkgnum },
+ select => 'DISTINCT classnum',
+ });
+
+ }
+
+}
+
+sub cust_tax_exempt_pkg {
+ my ( $self ) = @_;
+
+ my $array = $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 recur_show_zero
+
+Whether to show a zero recurring amount. This is true if the package or its
+definition has the recur_show_zero flag, and the recurring fee is actually
+zero for this period.
+
+=cut
+
+sub recur_show_zero {
+ my( $self, $what ) = @_;
+
+ return 0 unless $self->get('recur') == 0 && $self->pkgnum;
+
+ $self->cust_pkg->_X_show_zero('recur');
+}
+
+=item setup_show_zero
+
+Whether to show a zero setup charge. This requires the package or its
+definition to have the setup_show_zero flag, but it also returns false if
+the package's setup date is before this line item's start date.
+
+=cut
+
+sub setup_show_zero {
+ my $self = shift;
+ return 0 unless $self->get('setup') == 0 && $self->pkgnum;
+ my $cust_pkg = $self->cust_pkg;
+ return 0 if ( $self->sdate || 0 ) > ( $cust_pkg->setup || 0 );
+ return $cust_pkg->_X_show_zero('setup');
+}
+
+=item credited [ BEFORE, AFTER, OPTIONS ]
+
+Returns the sum of credits applied to this item. Arguments are the same as
+owed_sql/paid_sql/credited_sql.
+
+=cut
+
+sub credited {
+ my $self = shift;
+ $self->scalar_sql('SELECT '. $self->credited_sql(@_).' FROM cust_bill_pkg WHERE billpkgnum = ?', $self->billpkgnum);
+}
+
+=item tax_locationnum
+
+Returns the L<FS::cust_location> number that this line item is in for tax
+purposes. For package sales, it's the package tax location; for fees,
+it's the customer's default service location.
+
+=cut
+
+sub tax_locationnum {
+ my $self = shift;
+ if ( $self->pkgnum ) { # normal sales
+ return $self->cust_pkg->tax_locationnum;
+ } elsif ( $self->feepart ) { # fees
+ my $custnum = $self->fee_origin->custnum;
+ if ( $custnum ) {
+ return FS::cust_main->by_key($custnum)->ship_locationnum;
+ }
+ } else { # taxes
+ return '';
+ }
+}
+
+sub tax_location {
+ my $self = shift;
+ if ( $self->pkgnum ) { # normal sales
+ return $self->cust_pkg->tax_location;
+ } elsif ( $self->feepart ) { # fees
+ my $custnum = $self->fee_origin->custnum;
+ if ( $custnum ) {
+ return FS::cust_main->by_key($custnum)->ship_location;
+ }
+ } else { # taxes
+ return;
+ }