X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fpart_pkg.pm;h=036daf705bed63967a170bf5d6e5cf8b0b3de751;hb=c0660bdfce760cc982a145fc3aebaaec911fc32a;hp=856a693dd053743a90c241b1401f74984f012336;hpb=c6782ab85ea83e0c78d85b8975985aac9d467f9d;p=freeside.git diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm index 856a693dd..036daf705 100644 --- a/FS/FS/part_pkg.pm +++ b/FS/FS/part_pkg.pm @@ -1,13 +1,16 @@ package FS::part_pkg; +use base qw( FS::m2m_Common FS::o2m_Common FS::option_Common ); use strict; -use vars qw( @ISA %plans $DEBUG $setup_hack $skip_pkg_svc_hack ); +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; @@ -16,6 +19,7 @@ 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; @@ -24,7 +28,6 @@ use FS::part_pkg_discount; use FS::part_pkg_usage; use FS::part_pkg_vendor; -@ISA = qw( FS::m2m_Common FS::option_Common ); $DEBUG = 0; $setup_hack = 0; $skip_pkg_svc_hack = 0; @@ -114,6 +117,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 @@ -213,23 +218,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 ) { @@ -599,7 +587,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' ] ) @@ -629,8 +617,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; @@ -715,6 +705,35 @@ sub propagate { join("\n", @error); } +=item pkg_locale LOCALE + +Returns a customer-viewable string representing this package for the given +locale, from the part_pkg_msgcat table. If the given locale is empty or no +localized string is found, returns the base pkg field. + +=cut + +sub pkg_locale { + my( $self, $locale ) = @_; + return $self->pkg unless $locale; + my $part_pkg_msgcat = $self->part_pkg_msgcat($locale) or return $self->pkg; + $part_pkg_msgcat->pkg; +} + +=item part_pkg_msgcat LOCALE + +Like pkg_locale, but returns the FS::part_pkg_msgcat object itself. + +=cut + +sub part_pkg_msgcat { + my( $self, $locale ) = @_; + qsearchs( 'part_pkg_msgcat', { + pkgpart => $self->pkgpart, + locale => $locale, + }); +} + =item pkg_comment [ OPTION => VALUE... ] Returns an (internal) string representing this package. Currently, @@ -733,7 +752,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 @@ -742,7 +772,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 ) ? ' - ' : '' ). + $price_info; +} + +sub pkg_price_info { + my $self = shift; + $self->pkg. ' - '. ($self->price_info || 'No charge'); } =item pkg_class @@ -990,10 +1029,24 @@ 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; } +# 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 freqs_href { # moved to FS::Misc to make this accessible to other packages # at initialization @@ -1051,6 +1104,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; @@ -1143,6 +1199,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, @@ -1316,74 +1374,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 { +sub tax_rates { 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 { - 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 @@ -1488,6 +1519,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 @@ -1555,16 +1619,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; @@ -1581,15 +1649,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', @@ -1700,7 +1778,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 => '' });