X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fpart_pkg.pm;h=3308eadf45f8fe35a904d16598de9cd0815be6f3;hb=7d0b690f5684cdfdcd68d25568a101f6b088ad98;hp=96bd019a635271dcb65b94d003d0488da808119e;hpb=54e557ff60c8c11f3666c4f6acc33ebe8aa4b8ff;p=freeside.git diff --git a/FS/FS/part_pkg.pm b/FS/FS/part_pkg.pm index 96bd019a6..3308eadf4 100644 --- a/FS/FS/part_pkg.pm +++ b/FS/FS/part_pkg.pm @@ -4,6 +4,7 @@ use strict; use vars qw( @ISA %plans $DEBUG ); use Carp qw(carp cluck confess); use Scalar::Util qw( blessed ); +use Time::Local qw( timelocal_nocheck ); use Tie::IxHash; use FS::Conf; use FS::Record qw( qsearch qsearchs dbh dbdef ); @@ -144,6 +145,10 @@ record itself), the object will be updated to point to this package definition. In conjunction with I, if I is set to a scalar reference, the scalar will be updated with the custnum value from the cust_pkg record. +If I is set to a hashref with usage classes as keys and comma +separated tax class numbers as values, appropriate FS::part_pkg_taxoverride +records will be inserted. + If I is set to a hashref of options, appropriate FS::part_pkg_option records will be inserted. @@ -191,6 +196,22 @@ sub insert { } } + warn " inserting part_pkg_taxoverride records" if $DEBUG; + my %overrides = %{ $options{'tax_overrides'} || {} }; + foreach my $usage_class ( keys %overrides ) { + my @overrides = (grep "$_", split (',', $overrides{$usage_class}) ); + my $error = $self->process_m2m ( + 'link_table' => 'part_pkg_taxoverride', + 'target_table' => 'tax_class', + 'hashref' => { 'usage_class' => $usage_class }, + 'params' => \@overrides, + ); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + warn " inserting pkg_svc records" if $DEBUG; my $pkg_svc = $options{'pkg_svc'} || {}; foreach my $part_svc ( qsearch('part_svc', {} ) ) { @@ -254,14 +275,17 @@ sub delete { Replaces OLD_RECORD with this one in the database. If there is an error, returns the error, otherwise returns false. -Currently available options are: I and I +Currently available options are: I, I 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 replace. +values, the appropriate FS::pkg_svc records will be replaced. If I is set to the svcpart of the primary service, the appropriate FS::pkg_svc record will be updated. +If I is set to a hashref, the appropriate FS::part_pkg_option records +will be replaced. + =cut sub replace { @@ -276,6 +300,8 @@ sub replace { ? shift : { @_ }; + $options->{options} = {} unless defined($options->{options}); + warn "FS::part_pkg::replace called on $new to replace $old with options". join(', ', map "$_ => ". $options->{$_}, keys %$options) if $DEBUG; @@ -337,7 +363,13 @@ sub replace { my $pkg_svc = $options->{'pkg_svc'} || {}; foreach my $part_svc ( qsearch('part_svc', {} ) ) { my $quantity = $pkg_svc->{$part_svc->svcpart} || 0; - my $primary_svc = $options->{'primary_svc'} == $part_svc->svcpart ? 'Y' : ''; + my $primary_svc = + ( defined($options->{'primary_svc'}) + && $options->{'primary_svc'} == $part_svc->svcpart + ) + ? 'Y' + : ''; + my $old_pkg_svc = qsearchs('pkg_svc', { 'pkgpart' => $old->pkgpart, @@ -400,6 +432,12 @@ sub check { $self->freq($1); } + my @null_agentnum_right = ( 'Edit global package definitions' ); + push @null_agentnum_right, 'One-time charge' + if $self->freq =~ /^0/; + push @null_agentnum_right, 'Customize customer package' + if $self->disabled eq 'Y'; #good enough + my $error = $self->ut_numbern('pkgpart') || $self->ut_text('pkg') || $self->ut_text('comment') @@ -411,7 +449,12 @@ sub check { || $self->ut_enum('disabled', [ '', 'Y' ] ) || $self->ut_floatn('pay_weight') || $self->ut_floatn('credit_weight') - || $self->ut_agentnum_acl('agentnum', 'Edit global package definitions') + || $self->ut_numbern('taxproductnum') + || $self->ut_foreign_keyn('taxproductnum', + 'part_pkg_taxproduct', + 'taxproductnum' + ) + || $self->ut_agentnum_acl('agentnum', \@null_agentnum_right) || $self->SUPER::check ; return $error if $error; @@ -465,6 +508,21 @@ sub pkg_class { } } +=item categoryname + +Returns the package category name, or the empty string if there is no package +category. + +=cut + +sub categoryname { + my $self = shift; + my $pkg_class = $self->pkg_class; + $pkg_class + ? $pkg_class->categoryname + : ''; +} + =item classname Returns the package class name, or the empty string if there is no package @@ -548,15 +606,29 @@ sub svcpart { my $svcdb = scalar(@_) ? shift : ''; my @svcdb_pkg_svc = grep { ( $svcdb eq $_->part_svc->svcdb || !$svcdb ) } $self->pkg_svc; - my @pkg_svc = (); - @pkg_svc = grep { $_->primary_svc =~ /^Y/i } @svcdb_pkg_svc - if dbdef->table('pkg_svc')->column('primary_svc'); + my @pkg_svc = grep { $_->primary_svc =~ /^Y/i } @svcdb_pkg_svc; @pkg_svc = grep {$_->quantity == 1 } @svcdb_pkg_svc unless @pkg_svc; return '' if scalar(@pkg_svc) != 1; $pkg_svc[0]->svcpart; } +=item svcpart_unique_svcdb SVCDB + +Returns the svcpart of the a service definition (see L) matching +SVCDB associated with this package definition (see L). Returns +false if there not a primary service definition for SVCDB or there are multiple +service definitions for SVCDB. + +=cut + +sub svcpart_unique_svcdb { + my( $self, $svcdb ) = @_; + my @svcdb_pkg_svc = grep { ( $svcdb eq $_->part_svc->svcdb ) } $self->pkg_svc; + return '' if scalar(@svcdb_pkg_svc) != 1; + $svcdb_pkg_svc[0]->svcpart; +} + =item payby Returns a list of the acceptable payment types for this package. Eventually @@ -663,6 +735,41 @@ sub freq_pretty { } } +=item add_freq TIMESTAMP + +Adds the frequency of this package to the provided timestamp and returns +the resulting timestamp, or -1 if the frequency of this package could not be +parsed (shouldn't happen). + +=cut + +sub add_freq { + my( $self, $date ) = @_; + my $freq = $self->freq; + + #change this bit to use Date::Manip? CAREFUL with timezones (see + # mailing list archive) + my ($sec,$min,$hour,$mday,$mon,$year) = (localtime($date) )[0,1,2,3,4,5]; + + if ( $self->freq =~ /^\d+$/ ) { + $mon += $self->freq; + until ( $mon < 12 ) { $mon -= 12; $year++; } + } elsif ( $self->freq =~ /^(\d+)w$/ ) { + my $weeks = $1; + $mday += $weeks * 7; + } elsif ( $self->freq =~ /^(\d+)d$/ ) { + my $days = $1; + $mday += $days; + } elsif ( $self->freq =~ /^(\d+)h$/ ) { + my $hours = $1; + $hour += $hours; + } else { + return -1; + } + + timelocal_nocheck($sec,$min,$hour,$mday,$mon,$year); +} + =item plandata For backwards compatibility, returns the plandata field as well as all options @@ -731,7 +838,7 @@ sub option { =item bill_part_pkg_link -Returns the associated part_pkg_link records (see L). =cut @@ -741,6 +848,8 @@ sub bill_part_pkg_link { =item svc_part_pkg_link +Returns the associated part_pkg_link records (see L). + =cut sub svc_part_pkg_link { @@ -755,19 +864,99 @@ sub _part_pkg_link { ); } -=item part_pkg_taxoverride +sub self_and_bill_linked { + shift->_self_and_linked('bill', @_); +} + +sub _self_and_linked { + my( $self, $type ) = @_; + + ( $self, + map { $_->dst_pkg->_self_and_linked($type) } + $self->_part_pkg_link($type) + ); +} + +=item part_pkg_taxoverride [ CLASS ] Returns all associated FS::part_pkg_taxoverride objects (see -L). +L). 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). +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; - qsearch('part_pkg_taxoverride', { 'pkgpart' => $self->pkgpart } ); + 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_description + +=item taxproduct [ CLASS ] + +Returns the associated tax product for this package definition (see +L). CLASS may be one of 'setup', 'recur' or +the usage classnum (see L). 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). @@ -776,39 +965,75 @@ definition (see L). sub taxproduct_description { my $self = shift; - my $part_pkg_taxproduct = - qsearchs( 'part_pkg_taxproduct', - { 'taxproductnum' => $self->taxproductnum } - ); + my $part_pkg_taxproduct = $self->taxproduct(@_); $part_pkg_taxproduct ? $part_pkg_taxproduct->description : ''; } -=item part_pkg_taxrate DATA_PROVIDER, GEOCODE +=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 ). +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). =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 { my $self = shift; - my ($data_vendor, $geocode) = @_; + 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 - my $extra_sql = 'AND ('. + $extra_sql .= ' AND ('. join(' OR ', map{ 'geocode = '. $dbh->quote(substr($geocode, 0, $_)) } qw(10 5 2) ). ')'; - my $order_by = 'ORDER BY taxclassnum, length(geocode) desc'; - my $select = 'DISTINCT ON(taxclassnum) *'; + # 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 $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'; + # should qsearch preface columns with the table to facilitate joins? qsearch( { 'table' => 'part_pkg_taxrate', - 'select' => 'distinct on(taxclassnum) *', - 'hashref' => { 'data_vendor' => $data_vendor, - 'taxproductnum' => $self->taxproductnum, + 'select' => $select, + 'hashref' => { # 'data_vendor' => $data_vendor, + # 'taxproductnum' => $self->taxproductnum, }, + 'addl_from' => $addl_from, 'extra_sql' => $extra_sql, 'order_by' => $order_by, } ); @@ -873,11 +1098,50 @@ sub _calc_eval { sub calc_remain { 0; } sub calc_cancel { 0; } +sub calc_units { 0; } + +=item format OPTION DATA + +Returns data formatted according to the function 'format' described +in the plan info. Returns DATA if no such function exists. + +=cut + +sub format { + my ($self, $option, $data) = (shift, shift, shift); + if (exists($plans{$self->plan}->{fields}->{$option}{format})) { + &{$plans{$self->plan}->{fields}->{$option}{format}}($data); + }else{ + $data; + } +} + +=item parse OPTION DATA + +Returns data parsed according to the function 'parse' described +in the plan info. Returns DATA if no such function exists. + +=cut + +sub parse { + my ($self, $option, $data) = (shift, shift, shift); + if (exists($plans{$self->plan}->{fields}->{$option}{parse})) { + &{$plans{$self->plan}->{fields}->{$option}{parse}}($data); + }else{ + $data; + } +} =back =cut +=head1 CLASS METHODS + +=over 4 + +=cut + # _upgrade_data # # Used by FS::Upgrade to migrate to a new database. @@ -944,6 +1208,37 @@ sub _upgrade_data { # class method } +=item curuser_pkgs_sql + +Returns an SQL fragment for searching for packages the current user can +use, either via part_pkg.agentnum directly, or via agent type (see +L). + +=cut + +sub curuser_pkgs_sql { + #my($class) = shift; + + my $agentnums = join(',', $FS::CurrentUser::CurrentUser->agentnums); + + " + ( + agentnum IS NOT NULL + OR + 0 < ( SELECT COUNT(*) + FROM type_pkgs + LEFT JOIN agent_type USING ( typenum ) + LEFT JOIN agent AS typeagent USING ( typenum ) + WHERE type_pkgs.pkgpart = part_pkg.pkgpart + AND typeagent.agentnum IN ($agentnums) + ) + ) + "; + +} + +=back + =head1 SUBROUTINES =over 4 @@ -952,6 +1247,7 @@ sub _upgrade_data { # class method =cut +#false laziness w/part_export & cdr my %info; foreach my $INC ( @INC ) { warn "globbing $INC/FS/part_pkg/*.pm\n" if $DEBUG; @@ -969,8 +1265,7 @@ foreach my $INC ( @INC ) { next; } unless ( keys %$info ) { - warn "no %info hash found in FS::part_pkg::$mod, skipping\n" - unless $mod =~ /^(passwdfile|null)$/; #hack but what the heck + warn "no %info hash found in FS::part_pkg::$mod, skipping\n"; next; } warn "got plan info from FS::part_pkg::$mod: $info\n" if $DEBUG; @@ -983,7 +1278,7 @@ foreach my $INC ( @INC ) { } tie %plans, 'Tie::IxHash', - map { $_ => $info{$_} } + map { $_ => $info{$_} } sort { $info{$a}->{'weight'} <=> $info{$b}->{'weight'} } keys %info; @@ -991,38 +1286,6 @@ sub plan_info { \%plans; } -=item format OPTION DATA - -Returns data formatted according to the function 'format' described -in the plan info. Returns DATA if no such function exists. - -=cut - -sub format { - my ($self, $option, $data) = (shift, shift, shift); - if (exists($plans{$self->plan}->{fields}->{$option}{format})) { - &{$plans{$self->plan}->{fields}->{$option}{format}}($data); - }else{ - $data; - } -} - -=item parse OPTION DATA - -Returns data parsed according to the function 'parse' described -in the plan info. Returns DATA if no such function exists. - -=cut - -sub parse { - my ($self, $option, $data) = (shift, shift, shift); - if (exists($plans{$self->plan}->{fields}->{$option}{parse})) { - &{$plans{$self->plan}->{fields}->{$option}{parse}}($data); - }else{ - $data; - } -} - =back @@ -1041,6 +1304,8 @@ FS::cust_bill. hmm.). now they're deprecated and need to go. plandata should go +part_pkg_taxrate is Pg specific + =head1 SEE ALSO L, L, L, L, L.