X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fpart_pkg.pm;h=06f304a2201457181c7a487494c1385177622e82;hb=d7cf0d6bb3b81b1c91ef1bcc3252d56f96b65b0f;hp=e788269f74e4eea7b0b62cd8913586d24ef0043c;hpb=ded0ab5cac02f099b387de360fb6dd6bd8cbb6b4;p=freeside.git diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm index e788269f7..06f304a22 100644 --- a/FS/FS/part_pkg.pm +++ b/FS/FS/part_pkg.pm @@ -1,20 +1,25 @@ 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 ); use Carp qw(carp cluck confess); use Scalar::Util qw( blessed ); -use Time::Local qw( timelocal_nocheck ); +use DateTime; +use Time::Local qw( timelocal timelocal_nocheck ); # eventually replace with DateTime 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; @@ -23,8 +28,8 @@ 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; $setup_hack = 0; @@ -115,6 +120,8 @@ If this record is not obsolete, will be null. ancestor of this record. If this record is not a successor to another part_pkg, will be equal to pkgpart. +=item delay_start - Number of days to delay package start, by default + =back =head1 METHODS @@ -177,6 +184,9 @@ records will be inserted. If I is set to a hashref of options, appropriate FS::part_pkg_option records will be inserted. +If I is set to a hashref of options (with the keys as +option_CURRENCY), appropriate FS::part_pkg::currency records will be inserted. + =cut sub insert { @@ -214,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 ) { @@ -251,6 +244,33 @@ sub insert { } } + warn " inserting part_pkg_currency records" if $DEBUG; + 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' => $optionname, + 'currency' => $currency, + 'optionvalue' => $part_pkg_currency{$key}, + }; + my $error = $part_pkg_currency->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + unless ( $skip_pkg_svc_hack ) { warn " inserting pkg_svc records" if $DEBUG; @@ -316,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; @@ -344,7 +369,9 @@ and I If I is set to a hashref with svcparts as keys and quantities as values, the appropriate FS::pkg_svc records will be replaced. I can be set to a hashref of svcparts and flag values ('Y' or '') to set the -'hidden' field in these records. +'hidden' field in these records. I can be set to a hashref of +svcparts and flag values ('Y' or '') to set the 'bulk_skip' field in those +records. If I is set to the svcpart of the primary service, the appropriate FS::pkg_svc record will be updated. @@ -352,6 +379,9 @@ FS::pkg_svc record will be updated. If I is set to a hashref, the appropriate FS::part_pkg_option records will be replaced. +If I is set to a hashref of options (with the keys as +option_CURRENCY), appropriate FS::part_pkg::currency records will be replaced. + =cut sub replace { @@ -447,13 +477,43 @@ sub replace { } } + #trivial nit: not the most efficient to delete and reinsert + warn " deleting old part_pkg_currency records" if $DEBUG; + foreach my $part_pkg_currency ( $old->part_pkg_currency ) { + my $error = $part_pkg_currency->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "error deleting part_pkg_currency record: $error"; + } + } + + warn " inserting new part_pkg_currency records" if $DEBUG; + my %part_pkg_currency = %{ $options->{'part_pkg_currency'} || {} }; + foreach my $key ( keys %part_pkg_currency ) { + $key =~ /^(.+)_([A-Z]{3})$/ or next; + my $part_pkg_currency = new FS::part_pkg_currency { + 'pkgpart' => $new->pkgpart, + 'optionname' => $1, + 'currency' => $2, + 'optionvalue' => $part_pkg_currency{$key}, + }; + my $error = $part_pkg_currency->insert; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return "error inserting part_pkg_currency record: $error"; + } + } + + warn " replacing pkg_svc records" if $DEBUG; my $pkg_svc = $options->{'pkg_svc'}; my $hidden_svc = $options->{'hidden_svc'} || {}; + my $bulk_skip = $options->{'bulk_skip'} || {}; if ( $pkg_svc ) { # if it wasn't passed, don't change existing pkg_svcs foreach my $part_svc ( qsearch('part_svc', {} ) ) { - my $quantity = $pkg_svc->{$part_svc->svcpart} || 0; - my $hidden = $hidden_svc->{$part_svc->svcpart} || ''; + my $quantity = $pkg_svc->{$part_svc->svcpart} || 0; + my $hidden = $hidden_svc->{$part_svc->svcpart} || ''; + my $bulk_skip = $bulk_skip->{$part_svc->svcpart} || ''; my $primary_svc = ( defined($options->{'primary_svc'}) && $options->{'primary_svc'} && $options->{'primary_svc'} == $part_svc->svcpart @@ -469,16 +529,19 @@ sub replace { my $old_quantity = 0; my $old_primary_svc = ''; my $old_hidden = ''; + my $old_bulk_skip = ''; if ( $old_pkg_svc ) { $old_quantity = $old_pkg_svc->quantity; $old_primary_svc = $old_pkg_svc->primary_svc if $old_pkg_svc->dbdef_table->column('primary_svc'); # is this needed? $old_hidden = $old_pkg_svc->hidden; + $old_bulk_skip = $old_pkg_svc->old_bulk_skip; } - next unless $old_quantity != $quantity || - $old_primary_svc ne $primary_svc || - $old_hidden ne $hidden; + next unless $old_quantity != $quantity + || $old_primary_svc ne $primary_svc + || $old_hidden ne $hidden + || $old_bulk_skip ne $bulk_skip; my $new_pkg_svc = new FS::pkg_svc( { 'pkgsvcnum' => ( $old_pkg_svc ? $old_pkg_svc->pkgsvcnum : '' ), @@ -487,6 +550,7 @@ sub replace { 'quantity' => $quantity, 'primary_svc' => $primary_svc, 'hidden' => $hidden, + 'bulk_skip' => $bulk_skip, } ); my $error = $old_pkg_svc ? $new_pkg_svc->replace($old_pkg_svc) @@ -558,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; ''; @@ -600,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' ] ) @@ -630,8 +699,10 @@ sub check { ) || $self->ut_numbern('fcc_ds0s') || $self->ut_numbern('fcc_voip_class') + || $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; @@ -716,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 @@ -763,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 @@ -772,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 @@ -780,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 @@ -856,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 @@ -881,11 +992,6 @@ definition. =cut -sub type_pkgs { - my $self = shift; - qsearch('type_pkgs', { 'pkgpart' => $self->pkgpart } ); -} - sub pkg_svc { my $self = shift; @@ -1020,10 +1126,30 @@ sub is_free { } } +# whether the plan allows discounts to be applied to this package 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 { + my $self = shift; + + my $delay = $self->delay_start or return ''; + + # avoid timelocal silliness + my $dt = DateTime->today(time_zone => 'local'); + $dt->add(days => $delay); + $dt->epoch; +} + +sub can_currency_exchange { 0; } + sub freqs_href { # moved to FS::Misc to make this accessible to other packages # at initialization @@ -1081,6 +1207,9 @@ sub add_freq { if ( $freq =~ /^\d+$/ ) { $mon += $freq; until ( $mon < 12 ) { $mon -= 12; $year++; } + + $mday = 28 if $mday > 28 && FS::Conf->new->exists('anniversary-rollback'); + } elsif ( $freq =~ /^(\d+)w$/ ) { my $weeks = $1; $mday += $weeks * 7; @@ -1122,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 @@ -1145,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. @@ -1173,6 +1288,8 @@ will be suppressed. sub option { my( $self, $opt, $ornull ) = @_; + cluck "$self -> option: searching for $opt" + if $DEBUG; my $part_pkg_option = qsearchs('part_pkg_option', { pkgpart => $self->pkgpart, @@ -1188,6 +1305,84 @@ sub option { ''; } +=item part_pkg_currency [ CURRENCY ] + +Returns all currency options as FS::part_pkg_currency objects (see +L), 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). @@ -1346,74 +1541,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 @@ -1421,25 +1589,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 @@ -1518,6 +1672,39 @@ sub cust_bill_pkg_recur { $cust_bill_pkg->recur; } +=item unit_setup CUST_PKG + +Returns the setup fee for one unit of the package. + +=cut + +sub unit_setup { + my ($self, $cust_pkg) = @_; + $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 @@ -1585,16 +1772,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; @@ -1611,15 +1802,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', @@ -1730,7 +1931,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 => '' }); @@ -1819,8 +2020,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";