X-Git-Url: http://git.freeside.biz/gitweb/?a=blobdiff_plain;f=FS%2FFS%2Fcust_pkg.pm;h=64736ba50551556788aee354c3cc409d22b10321;hb=28c3a49c00c1c142cfbaf685b189b3f6a88c73f5;hp=347c04bb110a4351cfb5b456cdaf4eed943524a8;hpb=6f5b68a5dc6a6b145c88aa1b2bad3848cc6f8788;p=freeside.git diff --git a/FS/FS/cust_pkg.pm b/FS/FS/cust_pkg.pm index 347c04bb1..64736ba50 100644 --- a/FS/FS/cust_pkg.pm +++ b/FS/FS/cust_pkg.pm @@ -12,7 +12,7 @@ use Time::Local qw( timelocal timelocal_nocheck ); use MIME::Entity; use FS::UID qw( getotaker dbh ); use FS::Misc qw( send_email ); -use FS::Record qw( qsearch qsearchs ); +use FS::Record qw( qsearch qsearchs fields ); use FS::CurrentUser; use FS::cust_svc; use FS::part_pkg; @@ -283,7 +283,7 @@ sub insert { } } - my $free_days = $part_pkg->option('free_days'); + my $free_days = $part_pkg->option('free_days',1); if ( $free_days && $part_pkg->option('delay_setup',1) ) { #&& !$self->start_date my ($mday,$mon,$year) = (localtime(time) )[3,4,5]; #my $start_date = ($self->start_date || timelocal(0,0,0,$mday,$mon,$year)) + 86400 * $free_days; @@ -605,6 +605,7 @@ sub check { || $self->ut_numbern('susp') || $self->ut_numbern('cancel') || $self->ut_numbern('adjourn') + || $self->ut_numbern('resume') || $self->ut_numbern('expire') || $self->ut_numbern('dundate') || $self->ut_enum('no_auto', [ '', 'Y' ]) @@ -618,6 +619,9 @@ sub check { return "A package with both start date (future start) and setup date (already started) will never bill" if $self->start_date && $self->setup; + return "A future unsuspend date can only be set for a package with a suspend date" + if $self->resume and !$self->susp and !$self->adjourn; + $self->usernum($FS::CurrentUser::CurrentUser->usernum) unless $self->usernum; if ( $self->dbdef_table->column('manual_flag') ) { @@ -775,7 +779,7 @@ sub cancel { #schwartz map { $_->[0] } sort { $a->[1] <=> $b->[1] } - map { [ $_, $_->svc_x->table_info->{'cancel_weight'} ]; } + map { [ $_, $_->svc_x ? $_->svc_x->table_info->{'cancel_weight'} : -1 ]; } qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } ) ) { my $part_svc = $cust_svc->part_svc; @@ -879,6 +883,145 @@ sub cancel_if_expired { ''; } +=item uncancel + +"Un-cancels" this package: Orders a new package with the same custnum, pkgpart, +locationnum, (other fields?). Attempts to re-provision cancelled services +using history information (errors at this stage are not fatal). + +cust_pkg: pass a scalar reference, will be filled in with the new cust_pkg object + +svc_fatal: service provisioning errors are fatal + +svc_errors: pass an array reference, will be filled in with any provisioning errors + +=cut + +sub uncancel { + my( $self, %options ) = @_; + + #in case you try do do $uncancel-date = $cust_pkg->uncacel + return '' unless $self->get('cancel'); + + ## + # Transaction-alize + ## + + local $SIG{HUP} = 'IGNORE'; + local $SIG{INT} = 'IGNORE'; + local $SIG{QUIT} = 'IGNORE'; + local $SIG{TERM} = 'IGNORE'; + local $SIG{TSTP} = 'IGNORE'; + local $SIG{PIPE} = 'IGNORE'; + + my $oldAutoCommit = $FS::UID::AutoCommit; + local $FS::UID::AutoCommit = 0; + my $dbh = dbh; + + ## + # insert the new package + ## + + my $cust_pkg = new FS::cust_pkg { + last_bill => ( $options{'last_bill'} || $self->get('last_bill') ), + bill => ( $options{'bill'} || $self->get('bill') ), + uncancel => time, + uncancel_pkgnum => $self->pkgnum, + map { $_ => $self->get($_) } qw( + custnum pkgpart locationnum + setup + susp adjourn resume expire start_date contract_end dundate + change_date change_pkgpart change_locationnum + manual_flag no_auto quantity agent_pkgid recur_show_zero setup_show_zero + ), + }; + + my $error = $cust_pkg->insert( + 'change' => 1, #supresses any referral credit to a referring customer + ); + if ($error) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + ## + # insert services + ## + + #find historical services within this timeframe before the package cancel + # (incompatible with "time" option to cust_pkg->cancel?) + my $fuzz = 2 * 60; #2 minutes? too much? (might catch separate unprovision) + # too little? (unprovisioing export delay?) + my($end, $start) = ( $self->get('cancel'), $self->get('cancel') - $fuzz ); + my @h_cust_svc = $self->h_cust_svc( $end, $start ); + + my @svc_errors; + foreach my $h_cust_svc (@h_cust_svc) { + my $h_svc_x = $h_cust_svc->h_svc_x( $end, $start ); + #next unless $h_svc_x; #should this happen? + (my $table = $h_svc_x->table) =~ s/^h_//; + require "FS/$table.pm"; + my $class = "FS::$table"; + my $svc_x = $class->new( { + 'pkgnum' => $cust_pkg->pkgnum, + 'svcpart' => $h_cust_svc->svcpart, + map { $_ => $h_svc_x->get($_) } fields($table) + } ); + + # radius_usergroup + if ( $h_svc_x->isa('FS::h_svc_Radius_Mixin') ) { + $svc_x->usergroup( [ $h_svc_x->h_usergroup($end, $start) ] ); + } + + my $svc_error = $svc_x->insert; + if ( $svc_error && $options{svc_fatal} ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + push @svc_errors, $svc_error if $svc_error; + } + + #these are pretty rare, but should handle them + # - dsl_device (mac addresses) + # - phone_device (mac addresses) + # - dsl_note (ikano notes) + # - domain_record (i.e. restore DNS information w/domains) + # - inventory_item(?) (inventory w/un-cancelling service?) + # - nas (svc_broaband nas stuff) + #this stuff is unused in the wild afaik + # - mailinglistmember + # - router.svcnum? + # - svc_domain.parent_svcnum? + # - acct_snarf (ancient mail fetching config) + # - cgp_rule (communigate) + # - cust_svc_option (used by our Tron stuff) + # - acct_rt_transaction (used by our time worked stuff) + + ## + # also move over any services that didn't unprovision at cancellation + ## + + foreach my $cust_svc ( qsearch('cust_svc', { pkgnum => $self->pkgnum } ) ) { + $cust_svc->pkgnum( $cust_pkg->pkgnum ); + my $error = $cust_svc->replace; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + + ## + # Finish + ## + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + + ${ $options{cust_pkg} } = $cust_pkg if ref($options{cust_pkg}); + @{ $options{svc_errors} } = @svc_errors if ref($options{svc_errors}); + + ''; +} + =item unexpire Cancels any pending expiration (sets the expire field to null). @@ -940,9 +1083,21 @@ Available options are: =over 4 -=item reason - can be set to a cancellation reason (see L), either a reasonnum of an existing reason, or passing a hashref will create a new reason. The hashref should have the following keys: typenum - Reason type (see L, reason - Text of the new reason. +=item reason - can be set to a cancellation reason (see L), +either a reasonnum of an existing reason, or passing a hashref will create +a new reason. The hashref should have the following keys: +- typenum - Reason type (see L +- reason - Text of the new reason. + +=item date - can be set to a unix style timestamp to specify when to +suspend (adjourn) + +=item time - can be set to override the current time, for calculation +of final invoices or unused-time credits -=item date - can be set to a unix style timestamp to specify when to suspend (adjourn) +=item resume_date - can be set to a time when the package should be +unsuspended. This may be more convenient than calling C +separately. =back @@ -978,16 +1133,16 @@ sub suspend { return ""; # no error # complain on adjourn? } + my $suspend_time = $options{'time'} || time; + my $date = $options{date} if $options{date}; # adjourn/suspend later - $date = '' if ($date && $date <= time); # complain instead? + $date = '' if ($date && $date <= $suspend_time); # complain instead? if ( $date && $old->get('expire') && $old->get('expire') < $date ) { dbh->rollback if $oldAutoCommit; return "Package $pkgnum expires before it would be suspended."; } - my $suspend_time = $options{'time'} || time; - if ( $options{'reason'} ) { $error = $self->insert_reason( 'reason' => $options{'reason'}, 'action' => $date ? 'adjourn' : 'suspend', @@ -1000,6 +1155,25 @@ sub suspend { } } + my %hash = $self->hash; + if ( $date ) { + $hash{'adjourn'} = $date; + } else { + $hash{'susp'} = $suspend_time; + } + + my $resume_date = $options{'resume_date'} || 0; + if ( $resume_date > ($date || $suspend_time) ) { + $hash{'resume'} = $resume_date; + } + + my $new = new FS::cust_pkg ( \%hash ); + $error = $new->replace( $self, options => { $self->options } ); + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + unless ( $date ) { my @labels = (); @@ -1055,19 +1229,6 @@ sub suspend { } - my %hash = $self->hash; - if ( $date ) { - $hash{'adjourn'} = $date; - } else { - $hash{'susp'} = $suspend_time; - } - my $new = new FS::cust_pkg ( \%hash ); - $error = $new->replace( $self, options => { $self->options } ); - if ( $error ) { - $dbh->rollback if $oldAutoCommit; - return $error; - } - $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; #no errors @@ -1083,6 +1244,11 @@ Available options are: =over 4 +=item date + +Can be set to a date to unsuspend the package in the future (the 'resume' +field). + =item adjust_next_bill Can be set true to adjust the next bill date forward by @@ -1117,15 +1283,40 @@ sub unsuspend { my $pkgnum = $old->pkgnum; if ( $old->get('cancel') || $self->get('cancel') ) { - dbh->rollback if $oldAutoCommit; + $dbh->rollback if $oldAutoCommit; return "Can't unsuspend cancelled package $pkgnum"; } unless ( $old->get('susp') && $self->get('susp') ) { - dbh->rollback if $oldAutoCommit; + $dbh->rollback if $oldAutoCommit; return ""; # no error # complain instead? } + my $date = $opt{'date'}; + if ( $date and $date > time ) { # return an error if $date <= time? + + if ( $old->get('expire') && $old->get('expire') < $date ) { + $dbh->rollback if $oldAutoCommit; + return "Package $pkgnum expires before it would be unsuspended."; + } + + my $new = new FS::cust_pkg { $self->hash }; + $new->set('resume', $date); + $error = $new->replace($self, options => $self->options); + + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + else { + $dbh->commit or die $dbh->errstr if $oldAutoCommit; + return ''; + } + + } #if $date + + my @labels = (); + foreach my $cust_svc ( qsearch('cust_svc',{'pkgnum'=> $self->pkgnum } ) ) { @@ -1145,6 +1336,8 @@ sub unsuspend { $dbh->rollback if $oldAutoCommit; return $error; } + my( $label, $value ) = $cust_svc->label; + push @labels, "$label: $value"; } } @@ -1166,7 +1359,8 @@ sub unsuspend { } $hash{'susp'} = ''; - $hash{'adjourn'} = '' if $hash{'adjourn'} < time; + $hash{'adjourn'} = '' if $hash{'adjourn'} and $hash{'adjourn'} < time; + $hash{'resume'} = '' if !$hash{'adjourn'}; my $new = new FS::cust_pkg ( \%hash ); $error = $new->replace( $self, options => { $self->options } ); if ( $error ) { @@ -1174,6 +1368,29 @@ sub unsuspend { return $error; } + if ( $conf->config('unsuspend_email_admin') ) { + + my $error = send_email( + 'from' => $conf->config('invoice_from', $self->cust_main->agentnum), + #invoice_from ??? well as good as any + 'to' => $conf->config('unsuspend_email_admin'), + 'subject' => 'FREESIDE NOTIFICATION: Customer package unsuspended', 'body' => [ + "This is an automatic message from your Freeside installation\n", + "informing you that the following customer package has been unsuspended:\n", + "\n", + 'Customer: #'. $self->custnum. ' '. $self->cust_main->name. "\n", + 'Package : #'. $self->pkgnum. " (". $self->part_pkg->pkg_comment. ")\n", + ( map { "Service : $_\n" } @labels ), + ], + ); + + if ( $error ) { + warn "WARNING: can't send unsuspension admin email (unsuspending anyway): ". + "$error\n"; + } + + } + $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; #no errors @@ -1224,6 +1441,7 @@ sub unadjourn { my %hash = $self->hash; $hash{'adjourn'} = ''; + $hash{'resume'} = ''; my $new = new FS::cust_pkg ( \%hash ); $error = $new->replace( $self, options => { $self->options } ); if ( $error ) { @@ -1335,7 +1553,7 @@ sub change { my $unused_credit = 0; if ( $opt->{'keep_dates'} ) { foreach my $date ( qw(setup bill last_bill susp adjourn cancel expire - start_date contract_end ) ) { + resume start_date contract_end ) ) { $hash{$date} = $self->getfield($date); } } @@ -1350,12 +1568,16 @@ sub change { $hash{$_} = '' foreach qw(setup bill last_bill); } + # allow $opt->{'locationnum'} = '' to specifically set it to null + # (i.e. customer default location) + $opt->{'locationnum'} = $self->locationnum if !exists($opt->{'locationnum'}); + # Create the new package. my $cust_pkg = new FS::cust_pkg { custnum => $self->custnum, pkgpart => ( $opt->{'pkgpart'} || $self->pkgpart ), refnum => ( $opt->{'refnum'} || $self->refnum ), - locationnum => ( $opt->{'locationnum'} || $self->locationnum ), + locationnum => ( $opt->{'locationnum'} ), %hash, }; @@ -1736,11 +1958,13 @@ sub num_cust_event { $sth->fetchrow_arrayref->[0]; } -=item cust_svc [ SVCPART ] +=item cust_svc [ SVCPART ] (old, deprecated usage) + +=item cust_svc [ OPTION => VALUE ... ] (current usage) Returns the services for this package, as FS::cust_svc objects (see -L). If a svcpart is specified, return only the matching -services. +L). Available options are svcpart and svcdb. If either is +spcififed, returns only the matching services. =cut @@ -1749,9 +1973,25 @@ sub cust_svc { return () unless $self->num_cust_svc(@_); - if ( @_ ) { - return qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum, - 'svcpart' => shift, } ); + my %opt = (); + if ( @_ && $_[0] =~ /^\d+/ ) { + $opt{svcpart} = shift; + } elsif ( @_ && ref($_[0]) eq 'HASH' ) { + %opt = %{ $_[0] }; + } elsif ( @_ ) { + %opt = @_; + } + + my %search = ( + 'table' => 'cust_svc', + 'hashref' => { 'pkgnum' => $self->pkgnum }, + ); + if ( $opt{svcpart} ) { + $search{hashref}->{svcpart} = $opt{'svcpart'}; + } + if ( $opt{'svcdb'} ) { + $search{addl_from} = ' LEFT JOIN part_svc USING ( svcpart ) '; + $search{hashref}->{svcdb} = $opt{'svcdb'}; } cluck "cust_pkg->cust_svc called" if $DEBUG > 2; @@ -1759,9 +1999,7 @@ sub cust_svc { #if ( $self->{'_svcnum'} ) { # values %{ $self->{'_svcnum'}->cache }; #} else { - $self->_sort_cust_svc( - [ qsearch( 'cust_svc', { 'pkgnum' => $self->pkgnum } ) ] - ); + $self->_sort_cust_svc( [ qsearch(\%search) ] ); #} } @@ -1829,10 +2067,12 @@ sub _sort_cust_svc { } -=item num_cust_svc [ SVCPART ] +=item num_cust_svc [ SVCPART ] (old, deprecated usage) -Returns the number of provisioned services for this package. If a svcpart is -specified, counts only the matching services. +=item num_cust_svc [ OPTION => VALUE ... ] (current usage) + +Returns the number of services for this package. Available options are svcpart +and svcdb. If either is spcififed, returns only the matching services. =cut @@ -1847,11 +2087,31 @@ sub num_cust_svc { cluck "cust_pkg->num_cust_svc called, _num_cust_svc:".$self->{'_num_cust_svc'} if $DEBUG > 2; - my $sql = 'SELECT COUNT(*) FROM cust_svc WHERE pkgnum = ?'; - $sql .= ' AND svcpart = ?' if @_; + my %opt = (); + if ( @_ && $_[0] =~ /^\d+/ ) { + $opt{svcpart} = shift; + } elsif ( @_ && ref($_[0]) eq 'HASH' ) { + %opt = %{ $_[0] }; + } elsif ( @_ ) { + %opt = @_; + } + + my $select = 'SELECT COUNT(*) FROM cust_svc '; + my $where = ' WHERE pkgnum = ? '; + my @param = ($self->pkgnum); + + if ( $opt{'svcpart'} ) { + $where .= ' AND svcpart = ? '; + push @param, $opt{'svcpart'}; + } + if ( $opt{'svcdb'} ) { + $select .= ' LEFT JOIN part_svc USING ( svcpart ) '; + $where .= ' AND svcdb = ? '; + push @param, $opt{'svcdb'}; + } - my $sth = dbh->prepare($sql) or die dbh->errstr; - $sth->execute($self->pkgnum, @_) or die $sth->errstr; + my $sth = dbh->prepare("$select $where") or die dbh->errstr; + $sth->execute(@param) or die $sth->errstr; $sth->fetchrow_arrayref->[0]; } @@ -1882,7 +2142,7 @@ sub available_part_svc { $self->part_pkg->pkg_svc; } -=item part_svc +=item part_svc [ OPTION => VALUE ... ] Returns a list of FS::part_svc objects representing provisioned and available services included in this package. Each FS::part_svc object also has the @@ -1896,15 +2156,20 @@ following extra fields: =item cust_pkg_svc (services) - array reference containing the provisioned services, as cust_svc objects -svcnum -label -> ($cust_svc->label)[1] - =back +Accepts one option: summarize_size. If specified and non-zero, will omit the +extra cust_pkg_svc option for objects where num_cust_svc is this size or +greater. + =cut +#svcnum +#label -> ($cust_svc->label)[1] + sub part_svc { my $self = shift; + my %opt = @_; #XXX some sort of sort order besides numeric by svcpart... my @part_svc = sort { $a->svcpart <=> $b->svcpart } map { @@ -1915,7 +2180,9 @@ sub part_svc { $part_svc->{'Hash'}{'num_avail'} = max( 0, $pkg_svc->quantity - $num_cust_svc ); $part_svc->{'Hash'}{'cust_pkg_svc'} = - $num_cust_svc ? [ $self->cust_svc($part_svc->svcpart) ] : []; + $num_cust_svc ? [ $self->cust_svc($part_svc->svcpart) ] : [] + unless exists($opt{summarize_size}) && $opt{summarize_size} > 0 + && $num_cust_svc >= $opt{summarize_size}; $part_svc->{'Hash'}{'hidden'} = $pkg_svc->hidden; $part_svc; } $self->part_pkg->pkg_svc; @@ -2645,7 +2912,8 @@ All svc_accts which are part of this package have their values reset. sub set_usage { my ($self, $valueref, %opt) = @_; - foreach my $cust_svc ($self->cust_svc){ + #only svc_acct can set_usage for now + foreach my $cust_svc ( $self->cust_svc( 'svcdb'=>'svc_acct' ) ) { my $svc_x = $cust_svc->svc_x; $svc_x->set_usage($valueref, %opt) if $svc_x->can("set_usage"); @@ -2665,7 +2933,8 @@ All svc_accts which are part of this package have their values incremented. sub recharge { my ($self, $valueref) = @_; - foreach my $cust_svc ($self->cust_svc){ + #only svc_acct can set_usage for now + foreach my $cust_svc ( $self->cust_svc( 'svcdb'=>'svc_acct' ) ) { my $svc_x = $cust_svc->svc_x; $svc_x->recharge($valueref) if $svc_x->can("recharge"); @@ -2987,7 +3256,6 @@ sub search { } elsif ( @c_where ) { push @where, ' ( '. join(' OR ', @c_where). ' ) '; } - warn $where[-1]; } @@ -3018,16 +3286,13 @@ sub search { } @report_option; } - foreach my $any ( grep /^report_option_any/ keys %$params ) { + foreach my $any ( grep /^report_option_any/, keys %$params ) { my @report_option_any = (); - if ( exists($params->{$any}) ) { - if ( ref($params->{$any}) eq 'ARRAY' ) { - @report_option_any = @{ $params->{$any} }; - } elsif ( $params->{$any} =~ /^([,\d]*)$/ ) { - @report_option_any = split(',', $1); - } - + if ( ref($params->{$any}) eq 'ARRAY' ) { + @report_option_any = @{ $params->{$any} }; + } elsif ( $params->{$any} =~ /^([,\d]*)$/ ) { + @report_option_any = split(',', $1); } if (@report_option_any) { @@ -3131,7 +3396,7 @@ sub search { "NOT (".FS::cust_pkg->onetime_sql . ")"; } else { - foreach my $field (qw( setup last_bill bill adjourn susp expire contract_end cancel )) { + foreach my $field (qw( setup last_bill bill adjourn susp expire contract_end change_date cancel )) { next unless exists($params->{$field}); @@ -3222,7 +3487,8 @@ sub search { $params->{'cust_fields'} ), ), - 'extra_sql' => "$extra_sql $orderby", + 'extra_sql' => $extra_sql, + 'order_by' => $orderby, 'addl_from' => $addl_from, 'count_query' => $count_query, }; @@ -3275,7 +3541,13 @@ sub location_sql { # '?' placeholders in _location_sql_where my $x = $ornull ? 3 : 2; - my @bill_param = ( ('city')x3, ('county')x$x, ('state')x$x, 'country' ); + my @bill_param = ( + ('district')x3, + ('city')x3, + ('county')x$x, + ('state')x$x, + 'country' + ); my $main_where; my @main_param; @@ -3334,16 +3606,17 @@ 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 ) "; + 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}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 = ? + ( $table.district = ? OR ? = '' OR CAST(? AS text) IS NULL ) + AND ( $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 = ? "; }