+=item part_pkg_currency [ CURRENCY ]
+
+Returns all currency options as FS::part_pkg_currency objects (see
+L<FS::part_pkg_currency>), or, if a currency is specified, only return the
+objects for that currency.
+
+=cut
+
+sub part_pkg_currency {
+ my $self = shift;
+ my %hash = ( 'pkgpart' => $self->pkgpart );
+ $hash{'currency'} = shift if @_;
+ qsearch('part_pkg_currency', \%hash );
+}
+
+=item part_pkg_currency_options CURRENCY
+
+Returns a list of option names and values from FS::part_pkg_currency for the
+specified currency.
+
+=cut
+
+sub part_pkg_currency_options {
+ my $self = shift;
+ map { $_->optionname => $_->optionvalue } $self->part_pkg_currency(shift);
+}
+
+=item part_pkg_currency_option CURRENCY OPTIONNAME
+
+Returns the option value for the given name and currency.
+
+=cut
+
+sub part_pkg_currency_option {
+ my( $self, $currency, $optionname ) = @_;
+ my $part_pkg_currency =
+ qsearchs('part_pkg_currency', { 'pkgpart' => $self->pkgpart,
+ 'currency' => $currency,
+ 'optionname' => $optionname,
+ }
+ )#;
+ #fatal if not found? that works for our use cases from
+ #part_pkg/currency_fixed, but isn't how we would typically/expect the method
+ #to behave. have to catch it there if we change it here...
+ or die "Unknown price for ". $self->pkg_comment. " in $currency\n";
+
+ $part_pkg_currency->optionvalue;
+}
+
+=item fcc_option OPTIONNAME
+
+Returns the FCC 477 report option value for the given name, or the empty
+string.
+
+=cut
+
+sub fcc_option {
+ my ($self, $name) = @_;
+ my $part_pkg_fcc_option =
+ qsearchs('part_pkg_fcc_option', {
+ pkgpart => $self->pkgpart,
+ fccoptionname => $name,
+ });
+ $part_pkg_fcc_option ? $part_pkg_fcc_option->optionvalue : '';
+}
+
+=item fcc_options
+
+Returns all FCC 477 report options for this package, as a hash-like list.
+
+=cut
+
+sub fcc_options {
+ my $self = shift;
+ map { $_->fccoptionname => $_->optionvalue }
+ qsearch('part_pkg_fcc_option', { pkgpart => $self->pkgpart });
+}
+
+=item bill_part_pkg_link
+
+Returns the associated part_pkg_link records (see L<FS::part_pkg_link>).
+
+=cut
+
+sub bill_part_pkg_link {
+ shift->_part_pkg_link('bill', @_);
+}
+
+=item svc_part_pkg_link
+
+Returns the associated part_pkg_link records (see L<FS::part_pkg_link>).
+
+=cut
+
+sub svc_part_pkg_link {
+ shift->_part_pkg_link('svc', @_);
+}
+
+=item supp_part_pkg_link
+
+Returns the associated part_pkg_link records of type 'supp' (supplemental
+packages).
+
+=cut
+
+sub supp_part_pkg_link {
+ shift->_part_pkg_link('supp', @_);
+}
+
+sub _part_pkg_link {
+ my( $self, $type ) = @_;
+ qsearch({ table => 'part_pkg_link',
+ hashref => { 'src_pkgpart' => $self->pkgpart,
+ 'link_type' => $type,
+ #protection against infinite recursive links
+ 'dst_pkgpart' => { op=>'!=', value=> $self->pkgpart },
+ },
+ order_by => "ORDER BY hidden",
+ });
+}
+
+sub self_and_bill_linked {
+ shift->_self_and_linked('bill', @_);
+}
+
+sub self_and_svc_linked {
+ shift->_self_and_linked('svc', @_);
+}
+
+sub _self_and_linked {
+ my( $self, $type, $hidden ) = @_;
+ $hidden ||= '';
+
+ my @result = ();
+ foreach ( ( $self, map { $_->dst_pkg->_self_and_linked($type, $_->hidden) }
+ $self->_part_pkg_link($type) ) )
+ {
+ $_->hidden($hidden) if $hidden;
+ push @result, $_;
+ }
+
+ (@result);
+}
+
+=item part_pkg_taxoverride [ CLASS ]
+
+Returns all associated FS::part_pkg_taxoverride objects (see
+L<FS::part_pkg_taxoverride>). Limits the returned set to those
+of class CLASS if defined. Class may be one of 'setup', 'recur',
+the empty string (default), or a usage class number (see L<FS::usage_class>).
+When a class is specified, the empty string class (default) is returned
+if no more specific values exist.
+
+=cut
+
+sub part_pkg_taxoverride {
+ my $self = shift;
+ my $class = shift;
+
+ my $hashref = { 'pkgpart' => $self->pkgpart };
+ $hashref->{'usage_class'} = $class if defined($class);
+ my @overrides = qsearch('part_pkg_taxoverride', $hashref );
+
+ unless ( scalar(@overrides) || !defined($class) || !$class ){
+ $hashref->{'usage_class'} = '';
+ @overrides = qsearch('part_pkg_taxoverride', $hashref );
+ }
+
+ @overrides;
+}
+
+=item has_taxproduct
+
+Returns true if this package has any taxproduct associated with it.
+
+=cut
+
+sub has_taxproduct {
+ my $self = shift;
+
+ $self->taxproductnum ||
+ scalar( grep { $_ =~/^usage_taxproductnum_/ && $self->option($_) }
+ keys %{ {$self->options} }
+ )
+
+}
+
+
+=item taxproduct [ CLASS ]
+
+Returns the associated tax product for this package definition (see
+L<FS::part_pkg_taxproduct>). CLASS may be one of 'setup', 'recur' or
+the usage classnum (see L<FS::usage_class>). Returns the default
+tax product for this record if the more specific CLASS value does
+not exist.
+
+=cut
+
+sub taxproduct {
+ my $self = shift;
+ my $class = shift;
+
+ my $part_pkg_taxproduct;
+
+ my $taxproductnum = $self->taxproductnum;
+ if ($class) {
+ my $class_taxproductnum = $self->option("usage_taxproductnum_$class", 1);
+ $taxproductnum = $class_taxproductnum
+ if $class_taxproductnum
+ }
+
+ $part_pkg_taxproduct =
+ qsearchs( 'part_pkg_taxproduct', { 'taxproductnum' => $taxproductnum } );
+
+ unless ($part_pkg_taxproduct || $taxproductnum eq $self->taxproductnum ) {
+ $taxproductnum = $self->taxproductnum;
+ $part_pkg_taxproduct =
+ qsearchs( 'part_pkg_taxproduct', { 'taxproductnum' => $taxproductnum } );
+ }
+
+ $part_pkg_taxproduct;
+}
+
+=item taxproduct_description [ CLASS ]
+
+Returns the description of the associated tax product for this package
+definition (see L<FS::part_pkg_taxproduct>).
+
+=cut
+
+sub taxproduct_description {
+ my $self = shift;
+ my $part_pkg_taxproduct = $self->taxproduct(@_);
+ $part_pkg_taxproduct ? $part_pkg_taxproduct->description : '';
+}
+
+
+=item tax_rates DATA_PROVIDER, GEOCODE, [ CLASS ]
+
+Returns the tax table entries (L<FS::tax_rate> objects) that apply to this
+package in the location specified by GEOCODE, for usage class CLASS (one of
+'setup', 'recur', null, or a C<usage_class> number).
+
+=cut
+
+sub tax_rates {
+ my $self = shift;
+ my ($vendor, $geocode, $class) = @_;
+ # if this part_pkg is overridden into a specific taxclass, get that class
+ my @taxclassnums = map { $_->taxclassnum }
+ $self->part_pkg_taxoverride($class);
+ # otherwise, get its tax product category
+ if (!@taxclassnums) {
+ my $part_pkg_taxproduct = $self->taxproduct($class);
+ # If this isn't defined, then the class has no taxproduct designation,
+ # so return no tax rates.
+ return () if !$part_pkg_taxproduct;
+
+ # convert the taxproduct to the tax classes that might apply to it in
+ # $geocode
+ @taxclassnums = map { $_->taxclassnum }
+ grep { $_->taxable eq 'Y' } # why do we need this?
+ $part_pkg_taxproduct->part_pkg_taxrate($geocode);
+ }
+ return unless @taxclassnums;
+
+ # then look up the actual tax_rate entries
+ warn "Found taxclassnum values of ". join(',', @taxclassnums) ."\n"
+ if $DEBUG;
+ my $extra_sql = "AND taxclassnum IN (". join(',', @taxclassnums) . ")";
+ my @taxes = qsearch({ 'table' => 'tax_rate',
+ 'hashref' => { 'geocode' => $geocode,
+ 'data_vendor' => $vendor,
+ 'disabled' => '' },
+ 'extra_sql' => $extra_sql,
+ });
+ warn "Found taxes ". join(',', map {$_->taxnum} @taxes) ."\n"
+ if $DEBUG;
+
+ return @taxes;
+}
+
+=item part_pkg_discount
+
+Returns the package to discount m2m records (see L<FS::part_pkg_discount>)
+for this package.
+
+=item part_pkg_usage
+
+Returns the voice usage pools (see L<FS::part_pkg_usage>) defined for
+this package.
+