X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fpart_pkg.pm;h=0165455444e219110097e976e8034d1edb415826;hb=78b44177a16bc4da793da1c461455a0e77d52999;hp=9ce2e9687a751e0ee38e78a241d2b87efaca6485;hpb=65f3ee811a83be47ef02e0919ec122ad4083ccbd;p=freeside.git diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm index 9ce2e9687..016545544 100644 --- a/FS/FS/part_pkg.pm +++ b/FS/FS/part_pkg.pm @@ -10,21 +10,19 @@ 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::pkg_class; -use FS::agent; use FS::part_pkg_msgcat; use FS::part_pkg_taxrate; 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; @@ -221,23 +219,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 +243,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; @@ -663,7 +654,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 +687,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; @@ -827,7 +819,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 +839,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 +856,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 +921,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 +939,6 @@ definition. =cut -sub type_pkgs { - my $self = shift; - qsearch('type_pkgs', { 'pkgpart' => $self->pkgpart } ); -} - sub pkg_svc { my $self = shift; @@ -1089,6 +1078,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 +1198,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 +1214,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,6 +1235,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, @@ -1478,74 +1459,40 @@ 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); + @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'; + 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 +1500,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 +1594,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 +1683,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 +1713,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 +1842,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 => '' });