X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_pkg.pm;h=7632d9ae525c13d884db7e44567eb62a898c7dfc;hb=ce1fa204e25ae8605bdb4f3dbfdf9bd1bff1a590;hp=d2f0690eb14eeb3dc6e34900eea7854a27e9927e;hpb=0376f47e1ec2ec9b8702a0e6c5146af9c66beb5e;p=freeside.git diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index d2f0690eb..7632d9ae5 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -560,6 +560,8 @@ Available options are: =item date - can be set to a unix style timestamp to specify when to cancel (expire) +=item nobill - can be set true to skip billing if it might otherwise be done. + =back If there is an error, returns the error, otherwise returns false. @@ -570,6 +572,8 @@ sub cancel { my( $self, %options ) = @_; my $error; + my $conf = new FS::Conf; + warn "cust_pkg::cancel called with options". join(', ', map { "$_: $options{$_}" } keys %options ). "\n" if $DEBUG; @@ -595,6 +599,20 @@ sub cancel { my $date = $options{date} if $options{date}; # expire/cancel later $date = '' if ($date && $date <= time); # complain instead? + #race condition: usage could be ongoing until unprovisioned + #resolved by performing a change package instead (which unprovisions) and + #later cancelling + if ( !$options{nobill} && !$date && $conf->exists('bill_usage_on_cancel') ) { + my $copy = $self->new({$self->hash}); + my $error = + $copy->cust_main->bill( pkg_list => [ $copy ], cancel => 1 ); + warn "Error billing during cancel, custnum ". + #$self->cust_main->custnum. ": $error" + ": $error" + if $error; + } + + my $cancel_time = $options{'time'} || time; if ( $options{'reason'} ) { @@ -630,7 +648,6 @@ sub cancel { # Add a credit for remaining service my $remaining_value = $self->calc_remain(time=>$cancel_time); if ( $remaining_value > 0 && !$options{'no_credit'} ) { - my $conf = new FS::Conf; my $error = $self->cust_main->credit( $remaining_value, 'Credit for unused time on '. $self->part_pkg->pkg, @@ -656,10 +673,8 @@ sub cancel { $dbh->commit or die $dbh->errstr if $oldAutoCommit; return '' if $date; #no errors - my $conf = new FS::Conf; my @invoicing_list = grep { $_ !~ /^(POST|FAX)$/ } $self->cust_main->invoicing_list; if ( !$options{'quiet'} && $conf->exists('emailcancel') && @invoicing_list ) { - my $conf = new FS::Conf; my $error = send_email( 'from' => $conf->config('invoice_from', $self->cust_main->agentnum), 'to' => \@invoicing_list, @@ -1174,13 +1189,14 @@ sub change { } #reset usage if changing pkgpart + # AND usage rollover is off (otherwise adds twice, now and at package bill) if ($self->pkgpart != $cust_pkg->pkgpart) { my $part_pkg = $cust_pkg->part_pkg; $error = $part_pkg->reset_usage($cust_pkg, $part_pkg->is_prepaid ? () : ( 'null' => 1 ) ) - if $part_pkg->can('reset_usage'); + if $part_pkg->can('reset_usage') && ! $part_pkg->option('usage_rollover'); if ($error) { $dbh->rollback if $oldAutoCommit; @@ -1191,11 +1207,21 @@ sub change { #Good to go, cancel old package. $error = $self->cancel( quiet=>1 ); if ($error) { - $dbh->rollback; + $dbh->rollback if $oldAutoCommit; return $error; } + if ( $conf->exists('cust_pkg-change_pkgpart-bill_now') ) { + #$self->cust_main + my $error = $cust_pkg->cust_main->bill( 'pkg_list' => [ $cust_pkg ] ); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + $cust_pkg; } @@ -1736,6 +1762,63 @@ sub statuscolor { $statuscolor{$self->status}; } +=item pkg_label + +Returns a label for this package. (Currently "pkgnum: pkg - comment" or +"pkg-comment" depending on user preference). + +=cut + +sub pkg_label { + my $self = shift; + my $label = $self->part_pkg->pkg_comment( 'nopkgpart' => 1 ); + $label = $self->pkgnum. ": $label" + if $FS::CurrentUser::CurrentUser->option('show_pkgnum'); + $label; +} + +=item pkg_label_long + +Returns a long label for this package, adding the primary service's label to +pkg_label. + +=cut + +sub pkg_label_long { + my $self = shift; + my $label = $self->pkg_label; + my $cust_svc = $self->primary_cust_svc; + $label .= ' ('. ($cust_svc->label)[1]. ')' if $cust_svc; + $label; +} + +=item primary_cust_svc + +Returns a primary service (as FS::cust_svc object) if one can be identified. + +=cut + +#for labeling purposes - might not 100% match up with part_pkg->svcpart's idea + +sub primary_cust_svc { + my $self = shift; + + my @cust_svc = $self->cust_svc; + + return '' unless @cust_svc; #no serivces - irrelevant then + + return $cust_svc[0] if scalar(@cust_svc) == 1; #always return a single service + + # primary service as specified in the package definition + # or exactly one service definition with quantity one + my $svcpart = $self->part_pkg->svcpart; + @cust_svc = grep { $_->svcpart == $svcpart } @cust_svc; + return $cust_svc[0] if scalar(@cust_svc) == 1; + + #couldn't identify one thing.. + return ''; +} + =item labels Returns a list of lists, calling the label method for all services @@ -1764,6 +1847,19 @@ sub h_labels { map { [ $_->label(@_) ] } $self->h_cust_svc(@_); } +=item labels_short + +Like labels, except returns a simple flat list, and shortens long +(currently >5 or the cust_bill-max_same_services configuration value) lists of +identical services to one line that lists the service label and the number of +individual services rather than individual items. + +=cut + +sub labels_short { + shift->_labels_short( 'labels', @_ ); +} + =item h_labels_short END_TIMESTAMP [ START_TIMESTAMP ] Like h_labels, except returns a simple flat list, and shortens long @@ -1774,7 +1870,11 @@ individual services rather than individual items. =cut sub h_labels_short { - my $self = shift; + shift->_labels_short( 'h_labels', @_ ); +} + +sub _labels_short { + my( $self, $method ) = ( shift, shift ); my $conf = new FS::Conf; my $max_same_services = $conf->config('cust_bill-max_same_services') || 5; @@ -1791,7 +1891,18 @@ sub h_labels_short { if ( $num > $max_same_services ) { push @labels, "$label ($num)"; } else { - push @labels, map { "$label: $_" } @values; + if ( $conf->exists('cust_bill-consolidate_services') ) { + # push @labels, "$label: ". join(', ', @values); + while ( @values ) { + my $detail = "$label: "; + $detail .= shift(@values). ', ' + while @values && length($detail.$values[0]) < 78; + $detail =~ s/, $//; + push @labels, $detail; + } + } else { + push @labels, map { "$label: $_" } @values; + } } } @@ -2120,6 +2231,7 @@ Returns an SQL expression identifying active packages. sub active_sql { " ". $_[0]->recurring_sql(). " + AND cust_pkg.setup IS NOT NULL AND cust_pkg.setup != 0 AND ( cust_pkg.cancel IS NULL OR cust_pkg.cancel = 0 ) AND ( cust_pkg.susp IS NULL OR cust_pkg.susp = 0 ) "; } @@ -2179,7 +2291,7 @@ sub cancel_sql { "cust_pkg.cancel IS NOT NULL AND cust_pkg.cancel != 0"; } -=item search_sql HASHREF +=item search HASHREF (Class method) @@ -2206,7 +2318,7 @@ active, inactive, suspended, one-time charge, inactive, cancel (or cancelled) =item pkgpart -list specified how? +pkgpart or arrayref or hashref of pkgparts =item setup @@ -2252,7 +2364,7 @@ specifies the user for agent virtualization =cut -sub search_sql { +sub search { my ($class, $params) = @_; my @where = (); @@ -2266,6 +2378,15 @@ sub search_sql { } ## + # parse custnum + ## + + if ( $params->{'custnum'} =~ /^(\d+)$/ and $1 ) { + push @where, + "cust_pkg.custnum = $1"; + } + + ## # parse status ## @@ -2274,8 +2395,8 @@ sub search_sql { push @where, FS::cust_pkg->active_sql(); - } elsif ( $params->{'magic'} eq 'not yet billed' - || $params->{'status'} eq 'not yet billed' ) { + } elsif ( $params->{'magic'} =~ /^not[ _]yet[ _]billed$/ + || $params->{'status'} =~ /^not[ _]yet[ _]billed$/ ) { push @where, FS::cust_pkg->not_yet_billed_sql(); @@ -2309,7 +2430,7 @@ sub search_sql { { $classnum = $1; if ( $classnum ) { #a specific class - push @where, "classnum = $classnum"; + push @where, "part_pkg.classnum = $classnum"; #@pkg_class = ( qsearchs('pkg_class', { 'classnum' => $classnum } ) ); #die "classnum $classnum not found!" unless $pkg_class[0]; @@ -2317,7 +2438,7 @@ sub search_sql { } elsif ( $classnum eq '' ) { #the empty class - push @where, "classnum IS NULL"; + push @where, "part_pkg.classnum IS NULL"; #$title .= 'Empty class '; #@pkg_class = ( '(empty class)' ); } elsif ( $classnum eq '0' ) { @@ -2363,17 +2484,35 @@ sub search_sql { # parse censustract ### - if ( $params->{'censustract'} =~ /^([.\d]+)$/ and $1 ) { - push @where, "cust_main.censustract = '". $params->{censustract}. "'"; + if ( exists($params->{'censustract'}) ) { + $params->{'censustract'} =~ /^([.\d]*)$/; + my $censustract = "cust_main.censustract = '$1'"; + $censustract .= ' OR cust_main.censustract is NULL' unless $1; + push @where, "( $censustract )"; } ### # parse part_pkg ### - my $pkgpart = join (' OR pkgpart=', - grep {$_} map { /^(\d+)$/; } ($params->{'pkgpart'})); - push @where, '(pkgpart=' . $pkgpart . ')' if $pkgpart; + if ( ref($params->{'pkgpart'}) ) { + + my @pkgpart = (); + if ( ref($params->{'pkgpart'}) eq 'HASH' ) { + @pkgpart = grep $params->{'pkgpart'}{$_}, keys %{ $params->{'pkgpart'} }; + } elsif ( ref($params->{'pkgpart'}) eq 'ARRAY' ) { + @pkgpart = @{ $params->{'pkgpart'} }; + } else { + die 'unhandled pkgpart ref '. $params->{'pkgpart'}; + } + + @pkgpart = grep /^(\d+)$/, @pkgpart; + + push @where, 'pkgpart IN ('. join(',', @pkgpart). ')' if scalar(@pkgpart); + + } elsif ( $params->{'pkgpart'} =~ /^(\d+)$/ ) { + push @where, "pkgpart = $1"; + } ### # parse dates @@ -2454,10 +2593,10 @@ sub search_sql { if ($access_user) { push @where, $access_user->agentnums_sql('table'=>'cust_main'); - }else{ + } else { push @where, "1=0"; } - }else{ + } else { push @where, $FS::CurrentUser::CurrentUser->agentnums_sql('table'=>'cust_main'); } @@ -2465,7 +2604,7 @@ sub search_sql { my $addl_from = 'LEFT JOIN cust_main USING ( custnum ) '. 'LEFT JOIN part_pkg USING ( pkgpart ) '. - 'LEFT JOIN pkg_class USING ( classnum ) '; + 'LEFT JOIN pkg_class ON ( part_pkg.classnum = pkg_class.classnum ) '; my $count_query = "SELECT COUNT(*) FROM cust_pkg $addl_from $extra_sql"; @@ -2504,13 +2643,8 @@ sub location_sql { my $conf = new FS::Conf; # '?' placeholders in _location_sql_where - my @bill_param; - if ( $ornull ) { - @bill_param = qw( county county state state state country ); - } else { - @bill_param = qw( county state state country ); - } - unshift @bill_param, 'county'; # unless $nec; + my $x = $ornull ? 3 : 2; + my @bill_param = ( ('city')x3, ('county')x$x, ('state')x$x, 'country' ); my $main_where; my @main_param; @@ -2569,11 +2703,14 @@ sub _location_sql_where { $ornull = $ornull ? ' OR ? IS NULL ' : ''; + my $or_empty_city = " OR ( ? = '' AND $table.${prefix}city IS NULL ) "; my $or_empty_county = " OR ( ? = '' AND $table.${prefix}county IS NULL ) "; my $or_empty_state = " OR ( ? = '' AND $table.${prefix}state IS NULL ) "; +# ( $table.${prefix}city = ? $or_empty_city $ornull ) " - ( $table.${prefix}county = ? $or_empty_county $ornull ) + ( $table.${prefix}city = ? OR ? = '' OR CAST(? AS text) IS NULL ) + AND ( $table.${prefix}county = ? $or_empty_county $ornull ) AND ( $table.${prefix}state = ? $or_empty_state $ornull ) AND $table.${prefix}country = ? ";