X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fpart_pkg.pm;h=f408552e36dac21151ffff71eaf6e42db5f81b1e;hb=f9930edeaceb217a6503fa49078fcff2b588caf8;hp=9ce2e9687a751e0ee38e78a241d2b87efaca6485;hpb=65f3ee811a83be47ef02e0919ec122ad4083ccbd;p=freeside.git diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm index 9ce2e9687..f408552e3 100644 --- a/FS/FS/part_pkg.pm +++ b/FS/FS/part_pkg.pm @@ -1,5 +1,7 @@ package FS::part_pkg; -use base qw( FS::m2m_Common FS::o2m_Common FS::option_Common ); +use base qw( FS::part_pkg::API + FS::m2m_Common FS::o2m_Common FS::option_Common + ); use strict; use vars qw( %plans $DEBUG $setup_hack $skip_pkg_svc_hack ); @@ -10,12 +12,14 @@ use Time::Local qw( timelocal timelocal_nocheck ); # eventually replace with Dat use Tie::IxHash; use FS::Conf; use FS::Record qw( qsearch qsearchs dbh dbdef ); +use FS::Cursor; # for upgrade use FS::pkg_svc; use FS::part_svc; use FS::cust_pkg; use FS::agent_type; use FS::type_pkgs; use FS::part_pkg_option; +use FS::part_pkg_fcc_option; use FS::pkg_class; use FS::agent; use FS::part_pkg_msgcat; @@ -24,11 +28,10 @@ use FS::part_pkg_taxoverride; use FS::part_pkg_taxproduct; use FS::part_pkg_link; use FS::part_pkg_discount; -use FS::part_pkg_usage; use FS::part_pkg_vendor; use FS::part_pkg_currency; -$DEBUG = 0; +$DEBUG = 1; $setup_hack = 0; $skip_pkg_svc_hack = 0; @@ -221,23 +224,6 @@ sub insert { } } - my $conf = new FS::Conf; - if ( $conf->exists('agent_defaultpkg') ) { - warn " agent_defaultpkg set; allowing all agents to purchase package" - if $DEBUG; - foreach my $agent_type ( qsearch('agent_type', {} ) ) { - my $type_pkgs = new FS::type_pkgs({ - 'typenum' => $agent_type->typenum, - 'pkgpart' => $self->pkgpart, - }); - my $error = $type_pkgs->insert; - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; - } - } - } - warn " inserting part_pkg_taxoverride records" if $DEBUG; my %overrides = %{ $options{'tax_overrides'} || {} }; foreach my $usage_class ( keys %overrides ) { @@ -262,10 +248,20 @@ sub insert { my %part_pkg_currency = %{ $options{'part_pkg_currency'} || {} }; foreach my $key ( keys %part_pkg_currency ) { $key =~ /^(.+)_([A-Z]{3})$/ or next; + my( $optionname, $currency ) = ( $1, $2 ); + if ( $part_pkg_currency{$key} =~ /^\s*$/ ) { + if ( $self->option($optionname) == 0 ) { + $part_pkg_currency{$key} = '0'; + } else { + $dbh->rollback if $oldAutoCommit; + ( my $thing = $optionname ) =~ s/_/ /g; + return ucfirst($thing). " $currency is required"; + } + } my $part_pkg_currency = new FS::part_pkg_currency { 'pkgpart' => $self->pkgpart, - 'optionname' => $1, - 'currency' => $2, + 'optionname' => $optionname, + 'currency' => $currency, 'optionvalue' => $part_pkg_currency{$key}, }; my $error = $part_pkg_currency->insert; @@ -340,6 +336,11 @@ sub insert { } } + if ( $options{fcc_options} ) { + warn " updating fcc options " if $DEBUG; + $self->set_fcc_options( $options{fcc_options} ); + } + warn " committing transaction" if $DEBUG and $oldAutoCommit; $dbh->commit or die $dbh->errstr if $oldAutoCommit; @@ -621,6 +622,11 @@ sub replace { } } + if ( $options->{fcc_options} ) { + warn " updating fcc options " if $DEBUG; + $new->set_fcc_options( $options->{fcc_options} ); + } + warn " committing transaction" if $DEBUG and $oldAutoCommit; $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; @@ -663,7 +669,7 @@ sub check { my $error = $self->ut_numbern('pkgpart') || $self->ut_text('pkg') - || $self->ut_text('comment') + || $self->ut_textn('comment') || $self->ut_textn('promo_code') || $self->ut_alphan('plan') || $self->ut_enum('setuptax', [ '', 'Y' ] ) @@ -696,6 +702,7 @@ sub check { || $self->ut_numbern('delay_start') || $self->ut_foreign_keyn('successor', 'part_pkg', 'pkgpart') || $self->ut_foreign_keyn('family_pkgpart', 'part_pkg', 'pkgpart') + || $self->ut_alphan('agent_pkgpartid') || $self->SUPER::check ; return $error if $error; @@ -780,6 +787,44 @@ sub propagate { join("\n", @error); } +=item set_fcc_options HASHREF + +Sets the FCC options on this package definition to the values specified +in HASHREF. + +=cut + +sub set_fcc_options { + my $self = shift; + my $pkgpart = $self->pkgpart; + my $options; + if (ref $_[0]) { + $options = shift; + } else { + $options = { @_ }; + } + + my %existing_num = map { $_->fccoptionname => $_->num } + qsearch('part_pkg_fcc_option', { pkgpart => $pkgpart }); + + local $FS::Record::nowarn_identical = 1; + # set up params for process_o2m + my $i = 0; + my $params = {}; + foreach my $name (keys %$options ) { + $params->{ "num$i" } = $existing_num{$name} || ''; + $params->{ "num$i".'_fccoptionname' } = $name; + $params->{ "num$i".'_optionvalue' } = $options->{$name}; + $i++; + } + + $self->process_o2m( + table => 'part_pkg_fcc_option', + fields => [qw( fccoptionname optionvalue )], + params => $params, + ); +} + =item pkg_locale LOCALE Returns a customer-viewable string representing this package for the given @@ -827,7 +872,18 @@ sub pkg_comment { #$self->pkg. ' - '. $self->comment; #$self->pkg. ' ('. $self->comment. ')'; my $pre = $opt{nopkgpart} ? '' : $self->pkgpart. ': '; - $pre. $self->pkg. ' - '. $self->custom_comment; + my $custom_comment = $self->custom_comment(%opt); + $pre. $self->pkg. ( $custom_comment ? " - $custom_comment" : '' ); +} + +#without price info (so without hitting the DB again) +sub pkg_comment_only { + my $self = shift; + my %opt = @_; + + my $pre = $opt{nopkgpart} ? '' : $self->pkgpart. ': '; + my $comment = $self->comment; + $pre. $self->pkg. ( $comment ? " - $comment" : '' ); } sub price_info { # safety, in case a part_pkg hasn't defined price_info @@ -836,7 +892,16 @@ sub price_info { # safety, in case a part_pkg hasn't defined price_info sub custom_comment { my $self = shift; - ( $self->custom ? '(CUSTOM) ' : '' ). $self->comment . ' ' . $self->price_info; + my $price_info = $self->price_info(@_); + ( $self->custom ? '(CUSTOM) ' : '' ). + $self->comment. + ( ($self->custom || $self->comment) ? ' - ' : '' ). + ($price_info || 'No charge'); +} + +sub pkg_price_info { + my $self = shift; + $self->pkg. ' - '. ($self->price_info || 'No charge'); } =item pkg_class @@ -844,17 +909,6 @@ sub custom_comment { Returns the package class, as an FS::pkg_class object, or the empty string if there is no package class. -=cut - -sub pkg_class { - my $self = shift; - if ( $self->classnum ) { - qsearchs('pkg_class', { 'classnum' => $self->classnum } ); - } else { - return ''; - } -} - =item addon_pkg_class Returns the add-on package class, as an FS::pkg_class object, or the empty @@ -920,13 +974,6 @@ sub addon_classname { Returns the associated agent for this event, if any, as an FS::agent object. -=cut - -sub agent { - my $self = shift; - qsearchs('agent', { 'agentnum' => $self->agentnum } ); -} - =item pkg_svc [ HASHREF | OPTION => VALUE ] Returns all FS::pkg_svc objects (see L) for this package @@ -945,11 +992,6 @@ definition. =cut -sub type_pkgs { - my $self = shift; - qsearch('type_pkgs', { 'pkgpart' => $self->pkgpart } ); -} - sub pkg_svc { my $self = shift; @@ -1089,6 +1131,10 @@ sub can_discount { 0; } # whether the plan allows changing the start date sub can_start_date { 1; } + +# whether the plan supports part_pkg_usageprice add-ons (a specific kind of +# pre-selectable usage pricing, there's others this doesn't refer to) +sub can_usageprice { 0; } # the delay start date if present sub delay_start_date { @@ -1205,13 +1251,6 @@ sub plandata { Returns all vendor/external package ids as FS::part_pkg_vendor objects (see L). -=cut - -sub part_pkg_vendor { - my $self = shift; - qsearch('part_pkg_vendor', { 'pkgpart' => $self->pkgpart } ); -} - =item vendor_pkg_ids Returns a list of vendor/external package ids by exportnum @@ -1228,13 +1267,6 @@ sub vendor_pkg_ids { Returns all options as FS::part_pkg_option objects (see L). -=cut - -sub part_pkg_option { - my $self = shift; - qsearch('part_pkg_option', { 'pkgpart' => $self->pkgpart } ); -} - =item options Returns a list of option names and values suitable for assigning to a hash. @@ -1256,18 +1288,28 @@ will be suppressed. sub option { my( $self, $opt, $ornull ) = @_; + + #cache: was pulled up in the original part_pkg query + if ( $opt =~ /^(setup|recur)_fee$/ && defined($self->hashref->{"_$opt"}) ) { + return $self->hashref->{"_$opt"}; + } + + cluck "$self -> option: searching for $opt" + if $DEBUG; my $part_pkg_option = qsearchs('part_pkg_option', { pkgpart => $self->pkgpart, optionname => $opt, } ); return $part_pkg_option->optionvalue if $part_pkg_option; + my %plandata = map { /^(\w+)=(.*)$/; ( $1 => $2 ); } split("\n", $self->get('plandata') ); return $plandata{$opt} if exists $plandata{$opt}; cluck "WARNING: (pkgpart ". $self->pkgpart. ") Package def option $opt ". "not found in options or plandata!\n" unless $ornull; + ''; } @@ -1320,6 +1362,35 @@ sub part_pkg_currency_option { $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). @@ -1478,74 +1549,47 @@ sub taxproduct_description { $part_pkg_taxproduct ? $part_pkg_taxproduct->description : ''; } -=item part_pkg_taxrate DATA_PROVIDER, GEOCODE, [ CLASS ] -Returns the package to taxrate m2m records for this package in the location -specified by GEOCODE (see L) and usage class CLASS. -CLASS may be one of 'setup', 'recur', or one of the usage classes numbers -(see L). +=item tax_rates DATA_PROVIDER, GEOCODE, [ CLASS ] + +Returns the tax table entries (L objects) that apply to this +package in the location specified by GEOCODE, for usage class CLASS (one of +'setup', 'recur', null, or a C number). =cut -sub _expand_cch_taxproductnum { - my $self = shift; - my $class = shift; - my $part_pkg_taxproduct = $self->taxproduct($class); - - my ($a,$b,$c,$d) = ( $part_pkg_taxproduct - ? ( split ':', $part_pkg_taxproduct->taxproduct ) - : () - ); - $a = '' unless $a; $b = '' unless $b; $c = '' unless $c; $d = '' unless $d; - my $extra_sql = "AND ( taxproduct = '$a:$b:$c:$d' - OR taxproduct = '$a:$b:$c:' - OR taxproduct = '$a:$b:".":$d' - OR taxproduct = '$a:$b:".":' )"; - map { $_->taxproductnum } qsearch( { 'table' => 'part_pkg_taxproduct', - 'hashref' => { 'data_vendor'=>'cch' }, - 'extra_sql' => $extra_sql, - } ); - -} - -sub part_pkg_taxrate { +sub tax_rates { my $self = shift; - my ($data_vendor, $geocode, $class) = @_; - - my $dbh = dbh; - my $extra_sql = 'WHERE part_pkg_taxproduct.data_vendor = '. - dbh->quote($data_vendor); - - # CCH oddness in m2m - $extra_sql .= ' AND ('. - join(' OR ', map{ 'geocode = '. $dbh->quote(substr($geocode, 0, $_)) } - qw(10 5 2) - ). - ')'; - # much more CCH oddness in m2m -- this is kludgy - my @tpnums = $self->_expand_cch_taxproductnum($class); - if (scalar(@tpnums)) { - $extra_sql .= ' AND ('. - join(' OR ', map{ "taxproductnum = $_" } @tpnums ). - ')'; - } else { - $extra_sql .= ' AND ( 0 = 1 )'; + my ($vendor, $geocode, $class) = @_; + my @taxclassnums = map { $_->taxclassnum } + $self->part_pkg_taxoverride($class); + 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; - my $addl_from = 'LEFT JOIN part_pkg_taxproduct USING ( taxproductnum )'; - my $order_by = 'ORDER BY taxclassnum, length(geocode) desc, length(taxproduct) desc'; - my $select = 'DISTINCT ON(taxclassnum) *, taxproduct'; + # 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 }, + 'extra_sql' => $extra_sql, + }); + warn "Found taxes ". join(',', map {$_->taxnum} @taxes) ."\n" + if $DEBUG; - # should qsearch preface columns with the table to facilitate joins? - qsearch( { 'table' => 'part_pkg_taxrate', - 'select' => $select, - 'hashref' => { # 'data_vendor' => $data_vendor, - # 'taxproductnum' => $self->taxproductnum, - }, - 'addl_from' => $addl_from, - 'extra_sql' => $extra_sql, - 'order_by' => $order_by, - } ); + return @taxes; } =item part_pkg_discount @@ -1553,25 +1597,11 @@ sub part_pkg_taxrate { Returns the package to discount m2m records (see L) for this package. -=cut - -sub part_pkg_discount { - my $self = shift; - qsearch('part_pkg_discount', { 'pkgpart' => $self->pkgpart }); -} - =item part_pkg_usage Returns the voice usage pools (see L) defined for this package. -=cut - -sub part_pkg_usage { - my $self = shift; - qsearch('part_pkg_usage', { 'pkgpart' => $self->pkgpart }); -} - =item _rebless Reblesses the object into the FS::part_pkg::PLAN class (if available), where @@ -1661,6 +1691,28 @@ sub unit_setup { $self->option('setup_fee') || 0; } +=item setup_margin + +unit_setup minus setup_cost + +=cut + +sub setup_margin { + my $self = shift; + $self->unit_setup(@_) - $self->setup_cost; +} + +=item recur_margin_permonth + +base_recur_permonth minus recur_cost_permonth + +=cut + +sub recur_margin_permonth { + my $self = shift; + $self->base_recur_permonth(@_) - $self->recur_cost_permonth(@_); +} + =item format OPTION DATA Returns data formatted according to the function 'format' described @@ -1728,16 +1780,20 @@ sub _upgrade_data { # class method $part_pkg->replace; } + # the rest can be done asynchronously +} +sub queueable_upgrade { # now upgrade to the explicit custom flag - @part_pkg = qsearch({ + my $search = FS::Cursor->new({ 'table' => 'part_pkg', 'hashref' => { disabled => 'Y', custom => '' }, 'extra_sql' => "AND comment LIKE '(CUSTOM) %'", }); + my $dbh = dbh; - foreach my $part_pkg (@part_pkg) { + while (my $part_pkg = $search->fetch) { my $new = new FS::part_pkg { $part_pkg->hash }; $new->custom('Y'); my $comment = $part_pkg->comment; @@ -1754,15 +1810,25 @@ sub _upgrade_data { # class method 'primary_svc' => $primary, 'options' => $options, ); - die $error if $error; + if ($error) { + warn "pkgpart#".$part_pkg->pkgpart.": $error\n"; + $dbh->rollback; + } else { + $dbh->commit; + } } # set family_pkgpart on any packages that don't have it - @part_pkg = qsearch('part_pkg', { 'family_pkgpart' => '' }); - foreach my $part_pkg (@part_pkg) { + $search = FS::Cursor->new('part_pkg', { 'family_pkgpart' => '' }); + while (my $part_pkg = $search->fetch) { $part_pkg->set('family_pkgpart' => $part_pkg->pkgpart); my $error = $part_pkg->SUPER::replace; - die $error if $error; + if ($error) { + warn "pkgpart#".$part_pkg->pkgpart.": $error\n"; + $dbh->rollback; + } else { + $dbh->commit; + } } my @part_pkg_option = qsearch('part_pkg_option', @@ -1873,7 +1939,7 @@ sub _upgrade_data { # class method } } # $bad_upgrade exists else { # do the original upgrade, but correctly this time - @part_pkg = qsearch('part_pkg', { + my @part_pkg = qsearch('part_pkg', { fcc_ds0s => { op => '>', value => 0 }, fcc_voip_class => '' }); @@ -1962,8 +2028,8 @@ sub _pkgs_sql { #false laziness w/part_export & cdr my %info; foreach my $INC ( @INC ) { - warn "globbing $INC/FS/part_pkg/*.pm\n" if $DEBUG; - foreach my $file ( glob("$INC/FS/part_pkg/*.pm") ) { + warn "globbing $INC/FS/part_pkg/[a-z]*.pm\n" if $DEBUG; + foreach my $file ( glob("$INC/FS/part_pkg/[a-z]*.pm") ) { warn "attempting to load plan info from $file\n" if $DEBUG; $file =~ /\/(\w+)\.pm$/ or do { warn "unrecognized file in $INC/FS/part_pkg/: $file\n";