use FS::domain_record;
use FS::part_export;
use FS::cdr;
+use FS::UI::Web;
#most FS::svc_ classes are autoloaded in svc_x emthod
use FS::svc_acct; #this one is used in the cache stuff
=item svcpart - Service definition (see L<FS::part_svc>)
+=item agent_svcid - Optional legacy service ID
+
=item overlimit - date the service exceeded its usage limit
=back
error, otherwise returns false. Note that this only removes the cust_svc
record - you should probably use the B<cancel> method instead.
+=cut
+
+sub delete {
+ my $self = shift;
+ my $error = $self->SUPER::delete;
+ return $error if $error;
+
+ if ( FS::Conf->new->config('ticket_system') eq 'RT_Internal' ) {
+ FS::TicketSystem->init;
+ my $session = FS::TicketSystem->session;
+ my $links = RT::Links->new($session->{CurrentUser});
+ my $svcnum = $self->svcnum;
+ $links->Limit(FIELD => 'Target',
+ VALUE => 'freeside://freeside/cust_svc/'.$svcnum);
+ while ( my $l = $links->Next ) {
+ my ($val, $msg) = $l->Delete;
+ # can't do anything useful on error
+ warn "error unlinking ticket $svcnum: $msg\n" if !$val;
+ }
+ }
+}
+
=item cancel
Cancels the relevant service by calling the B<cancel> method of the associated
=cut
sub cancel {
- my $self = shift;
+ my($self,%opt) = @_;
local $SIG{HUP} = 'IGNORE';
local $SIG{INT} = 'IGNORE';
my $svc = $self->svc_x;
if ($svc) {
-
- my $error = $svc->cancel;
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "Error canceling service: $error";
- }
- $error = $svc->delete; #this deletes this cust_svc record as well
- if ( $error ) {
- $dbh->rollback if $oldAutoCommit;
- return "Error deleting service: $error";
+ if ( %opt && $opt{'date'} ) {
+ my $error = $svc->expire($opt{'date'});
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Error expiring service: $error";
+ }
+ } else {
+ my $error = $svc->cancel;
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Error canceling service: $error";
+ }
+ $error = $svc->delete; #this deletes this cust_svc record as well
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return "Error deleting service: $error";
+ }
}
- } else {
+ } elsif ( !%opt ) {
#huh?
warn "WARNING: no svc_ record found for svcnum ". $self->svcnum.
}
}
+# #trigger a re-export on pkgnum changes?
+# # (of prepaid packages), for Expiration RADIUS attribute
+# if ( $new->pkgnum != $old->pkgnum && $new->cust_pkg->part_pkg->is_prepaid ) {
+# my $svc_x = $new->svc_x;
+# local($FS::Record::nowarn_identical) = 1;
+# my $error = $svc_x->export('replace');
+# if ( $error ) {
+# $dbh->rollback if $oldAutoCommit;
+# return $error if $error;
+# }
+# }
+
+ #trigger a pkg_change export on pkgnum changes
+ if ( $new->pkgnum != $old->pkgnum ) {
+ my $error = $new->svc_x->export('pkg_change', $new->cust_pkg,
+ $old->cust_pkg,
+ );
+ if ( $error ) {
+ $dbh->rollback if $oldAutoCommit;
+ return $error if $error;
+ }
+ }
+
#my $error = $new->SUPER::replace($old, @_);
my $error = $new->SUPER::replace($old);
if ( $error ) {
$self->ut_numbern('svcnum')
|| $self->ut_numbern('pkgnum')
|| $self->ut_number('svcpart')
+ || $self->ut_numbern('agent_svcid')
|| $self->ut_numbern('overlimit')
;
return $error if $error;
if ( $self->pkgnum ) {
my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
return "Unknown pkgnum" unless $cust_pkg;
- my $pkg_svc = qsearchs( 'pkg_svc', {
- 'pkgpart' => $cust_pkg->pkgpart,
- 'svcpart' => $self->svcpart,
- });
- # or new FS::pkg_svc ( { 'pkgpart' => $cust_pkg->pkgpart,
- # 'svcpart' => $self->svcpart,
- # 'quantity' => 0 } );
- my $quantity = $pkg_svc ? $pkg_svc->quantity : 0;
-
- my @cust_svc = qsearch('cust_svc', {
- 'pkgnum' => $self->pkgnum,
- 'svcpart' => $self->svcpart,
- });
- return "Already ". scalar(@cust_svc). " ". $part_svc->svc.
+ ($part_svc) = grep { $_->svcpart == $self->svcpart } $cust_pkg->part_svc;
+ return "No svcpart ". $self->svcpart.
+ " services in pkgpart ". $cust_pkg->pkgpart
+ unless $part_svc || $ignore_quantity;
+ return "Already ". $part_svc->get('num_cust_svc'). " ". $part_svc->svc.
" services for pkgnum ". $self->pkgnum
- if scalar(@cust_svc) >= $quantity && !$ignore_quantity;
+ if !$ignore_quantity && $part_svc->get('num_avail') <= 0 ;
}
$self->SUPER::check;
}
+=item display_svcnum
+
+Returns the displayed service number for this service: agent_svcid if it has a
+value, svcnum otherwise
+
+=cut
+
+sub display_svcnum {
+ my $self = shift;
+ $self->agent_svcid || $self->svcnum;
+}
+
=item part_svc
Returns the definition for this service, as a FS::part_svc object (see
$self->h_date('insert');
}
+=item pkg_cancel_date
+
+Returns the date this service's package was canceled. This normally only
+exists for a service that's been preserved through cancellation with the
+part_pkg.preserve flag.
+
+=cut
+
+sub pkg_cancel_date {
+ my $self = shift;
+ my $cust_pkg = $self->cust_pkg or return;
+ return $cust_pkg->getfield('cancel') || '';
+}
+
=item label
Returns a list consisting of:
=item export_links
-Returns a list of html elements associated with this services exports.
+Returns a listref of html elements associated with this service's exports.
=cut
sub export_links {
my $self = shift;
my $svc_x = $self->svc_x
- or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
+ or return [ "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum ];
$svc_x->export_links;
}
+=item export_getsettings
+
+Returns two hashrefs of settings associated with this service's exports.
+
+=cut
+
+sub export_getsettings {
+ my $self = shift;
+ my $svc_x = $self->svc_x
+ or return "can't find ". $self->part_svc->svcdb. '.svcnum '. $self->svcnum;
+
+ $svc_x->export_getsettings;
+}
+
+
=item svc_x
Returns the FS::svc_XXX object for this service (i.e. an FS::svc_acct object or
=cut
-#note: implementation here, POD in FS::svc_acct
-sub seconds_since {
- my($self, $since) = @_;
- my $dbh = dbh;
- my $sth = $dbh->prepare(' SELECT SUM(logout-login) FROM session
- WHERE svcnum = ?
- AND login >= ?
- AND logout IS NOT NULL'
- ) or die $dbh->errstr;
- $sth->execute($self->svcnum, $since) or die $sth->errstr;
- $sth->fetchrow_arrayref->[0];
-}
+#internal session db deprecated (or at least on hold)
+sub seconds_since { 'internal session db deprecated'; };
+##note: implementation here, POD in FS::svc_acct
+#sub seconds_since {
+# my($self, $since) = @_;
+# my $dbh = dbh;
+# my $sth = $dbh->prepare(' SELECT SUM(logout-login) FROM session
+# WHERE svcnum = ?
+# AND login >= ?
+# AND logout IS NOT NULL'
+# ) or die $dbh->errstr;
+# $sth->execute($self->svcnum, $since) or die $sth->errstr;
+# $sth->fetchrow_arrayref->[0];
+#}
=item seconds_since_sqlradacct TIMESTAMP_START TIMESTAMP_END
warn "$mes finding closed sessions completely within the given range\n"
if $DEBUG;
+ my $realm = '';
+ my $realmparam = '';
+ if ($part_export->option('process_single_realm')) {
+ $realm = 'AND Realm = ?';
+ $realmparam = $part_export->option('realm');
+ }
+
my $sth = $dbh->prepare("SELECT SUM(acctsessiontime)
FROM radacct
WHERE UserName = ?
+ $realm
AND $str2time AcctStartTime) >= ?
AND $str2time AcctStopTime ) < ?
AND $str2time AcctStopTime ) > 0
AND AcctStopTime IS NOT NULL"
) or die $dbh->errstr;
- $sth->execute($username, $start, $end) or die $sth->errstr;
+ $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
+ or die $sth->errstr;
my $regular = $sth->fetchrow_arrayref->[0];
warn "$mes finding open sessions which start in the range\n"
$query = "SELECT SUM( ? - $str2time AcctStartTime ) )
FROM radacct
WHERE UserName = ?
+ $realm
AND $str2time AcctStartTime ) >= ?
AND $str2time AcctStartTime ) < ?
AND ( ? - $str2time AcctStartTime ) ) < 86400
AND ( $str2time AcctStopTime ) = 0
OR AcctStopTime IS NULL )";
$sth = $dbh->prepare($query) or die $dbh->errstr;
- $sth->execute($end, $username, $start, $end, $end)
+ $sth->execute( $end,
+ $username,
+ ($realm ? $realmparam : ()),
+ $start,
+ $end,
+ $end )
or die $sth->errstr. " executing query $query";
my $start_during = $sth->fetchrow_arrayref->[0];
$sth = $dbh->prepare("SELECT SUM( $str2time AcctStopTime ) - ? )
FROM radacct
WHERE UserName = ?
+ $realm
AND $str2time AcctStartTime ) < ?
AND $str2time AcctStopTime ) >= ?
AND $str2time AcctStopTime ) < ?
AND $str2time AcctStopTime ) > 0
AND AcctStopTime IS NOT NULL"
) or die $dbh->errstr;
- $sth->execute($start, $username, $start, $start, $end ) or die $sth->errstr;
+ $sth->execute( $start,
+ $username,
+ ($realm ? $realmparam : ()),
+ $start,
+ $start,
+ $end )
+ or die $sth->errstr;
my $end_during = $sth->fetchrow_arrayref->[0];
warn "$mes finding closed sessions which start before the range but stop after\n"
$sth = $dbh->prepare("SELECT COUNT(*)
FROM radacct
WHERE UserName = ?
+ $realm
AND $str2time AcctStartTime ) < ?
AND ( $str2time AcctStopTime ) >= ?
)"
# OR AcctStopTime = 0
# OR AcctStopTime IS NULL )"
) or die $dbh->errstr;
- $sth->execute($username, $start, $end ) or die $sth->errstr;
+ $sth->execute($username, ($realm ? $realmparam : ()), $start, $end )
+ or die $sth->errstr;
my $entire_range = ($end-$start) * $sth->fetchrow_arrayref->[0];
$seconds += $regular + $end_during + $start_during + $entire_range;
warn "$mes SUMing $attrib sessions\n"
if $DEBUG;
+ my $realm = '';
+ my $realmparam = '';
+ if ($part_export->option('process_single_realm')) {
+ $realm = 'AND Realm = ?';
+ $realmparam = $part_export->option('realm');
+ }
+
my $sth = $dbh->prepare("SELECT SUM($attrib)
FROM radacct
WHERE UserName = ?
+ $realm
AND $str2time AcctStopTime ) >= ?
AND $str2time AcctStopTime ) < ?
AND AcctStopTime IS NOT NULL"
) or die $dbh->errstr;
- $sth->execute($username, $start, $end) or die $sth->errstr;
+ $sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
+ or die $sth->errstr;
my $row = $sth->fetchrow_arrayref;
$sum += $row->[0] if defined($row->[0]);
}
-=item get_cdrs_for_update
+=item tickets [ STATUS ]
-Returns (and SELECTs "FOR UPDATE") all unprocessed (freesidestatus NULL) CDR
-objects (see L<FS::cdr>) associated with this service.
+Returns an array of hashes representing the tickets linked to this service.
-CDRs are associated with svc_phone services via svc_phone.phonenum
+An optional status (or arrayref or hashref of statuses) may be specified.
=cut
-sub get_cdrs_for_update {
+sub tickets {
my $self = shift;
- $self->get_cdrs( 'freesidestatus' => '',
- 'for_update' => 1,
- @_,
- );
+ my $status = ( @_ && $_[0] ) ? shift : '';
+
+ my $conf = FS::Conf->new;
+ my $num = $conf->config('cust_main-max_tickets') || 10;
+ my @tickets = ();
+
+ if ( $conf->config('ticket_system') ) {
+ unless ( $conf->config('ticket_system-custom_priority_field') ) {
+
+ @tickets = @{ FS::TicketSystem->service_tickets( $self->svcnum,
+ $num,
+ undef,
+ $status,
+ )
+ };
+
+ } else {
+
+ foreach my $priority (
+ $conf->config('ticket_system-custom_priority_field-values'), ''
+ ) {
+ last if scalar(@tickets) >= $num;
+ push @tickets,
+ @{ FS::TicketSystem->service_tickets( $self->svcnum,
+ $num - scalar(@tickets),
+ $priority,
+ $status,
+ )
+ };
+ }
+ }
+ }
+ (@tickets);
}
-sub get_cdrs {
- my($self, %options) = @_;
- my @fields = ( 'charged_party' );
- push @fields, 'src' unless $options{'disable_src'};
+=back
+
+=head1 SUBROUTINES
+
+=over 4
- my $for_update = $options{'for_update'} ? 'FOR UPDATE' : '';
+=item smart_search OPTION => VALUE ...
- my %hash = ();
- $hash{'freesidestatus'} = $options{'freesidestatus'}
- if exists($options{'freesidestatus'});
+Accepts the option I<search>, the string to search for. The string will
+be searched for as a username, email address, IP address, MAC address,
+phone number, and hardware serial number. Unlike the I<smart_search> on
+customers, this always requires an exact match.
- #CDRs are associated with svc_phone services via svc_phone.phonenum
+=cut
- #return () unless $self->svc_x->isa('FS::svc_phone');
- return () unless $self->part_svc->svcdb eq 'svc_phone';
- my $number = $self->svc_x->phonenum;
+# though perhaps it should be fuzzy in some cases?
- my $prefix = $options{'default_prefix'};
+sub smart_search {
+ my %param = __PACKAGE__->smart_search_param(@_);
+ qsearch(\%param);
+}
- my @orwhere = map " $_ = '$number' ", @fields;
- push @orwhere, map " $_ = '$prefix$number' ", @fields
- if length($prefix);
- if ( $prefix =~ /^\+(\d+)$/ ) {
- push @orwhere, map " $_ = '$1$number' ", @fields
- }
+sub smart_search_param {
+ my $class = shift;
+ my %opt = @_;
- my @where = ( ' ( '. join(' OR ', @orwhere ). ' ) ' );
+ my $string = $opt{'search'};
+ $string =~ s/(^\s+|\s+$)//; #trim leading & trailing whitespace
- if ( $options{'begin'} ) {
- push @where, 'startdate >= '. $options{'begin'};
- }
- if ( $options{'end'} ) {
- push @where, 'startdate < '. $options{'end'};
+ my @or =
+ map { my $table = $_;
+ my $search_sql = "FS::$table"->search_sql($string);
+
+ "SELECT $table.svcnum AS svcnum, '$table' AS svcdb ".
+ "FROM $table WHERE $search_sql";
+ }
+ FS::part_svc->svc_tables;
+
+ if ( $string =~ /^(\d+)$/ ) {
+ unshift @or, "SELECT cust_svc.svcnum, NULL as svcdb FROM cust_svc WHERE agent_svcid = $1";
}
- my $extra_sql = ( keys(%hash) ? ' AND ' : ' WHERE ' ). join(' AND ', @where );
+ my $addl_from = " RIGHT JOIN (\n" . join("\nUNION\n", @or) . "\n) AS svc_all ".
+ " ON (svc_all.svcnum = cust_svc.svcnum) ";
+
+ my @extra_sql;
+
+ push @extra_sql, $FS::CurrentUser::CurrentUser->agentnums_sql(
+ 'null_right' => 'View/link unlinked services'
+ );
+ my $extra_sql = ' WHERE '.join(' AND ', @extra_sql);
+ #for agentnum
+ $addl_from .= ' LEFT JOIN cust_pkg USING ( pkgnum )'.
+ FS::UI::Web::join_cust_main('cust_pkg', 'cust_pkg').
+ ' LEFT JOIN part_svc USING ( svcpart )';
+
+ (
+ 'table' => 'cust_svc',
+ 'select' => 'svc_all.svcnum AS svcnum, '.
+ 'COALESCE(svc_all.svcdb, part_svc.svcdb) AS svcdb, '.
+ 'cust_svc.*',
+ 'addl_from' => $addl_from,
+ 'hashref' => {},
+ 'extra_sql' => $extra_sql,
+ );
+}
- my @cdrs =
- qsearch( {
- 'table' => 'cdr',
- 'hashref' => \%hash,
- 'extra_sql' => $extra_sql,
- 'order_by' => "ORDER BY startdate $for_update",
- } );
+sub _upgrade_data {
+ my $class = shift;
+
+ # fix missing (deleted by mistake) svc_x records
+ warn "searching for missing svc_x records...\n";
+ my %search = (
+ 'table' => 'cust_svc',
+ 'select' => 'cust_svc.*',
+ 'addl_from' => ' LEFT JOIN ( ' .
+ join(' UNION ',
+ map { "SELECT svcnum FROM $_" }
+ FS::part_svc->svc_tables
+ ) . ' ) AS svc_all ON cust_svc.svcnum = svc_all.svcnum',
+ 'extra_sql' => ' WHERE svc_all.svcnum IS NULL',
+ );
+ my @svcs = qsearch(\%search);
+ warn "found ".scalar(@svcs)."\n";
+
+ local $FS::Record::nowarn_classload = 1; # for h_svc_
+ local $FS::svc_Common::noexport_hack = 1; # because we're inserting services
+
+ my %h_search = (
+ 'hashref' => { history_action => 'delete' },
+ 'order_by' => ' ORDER BY history_date DESC LIMIT 1',
+ );
+ foreach my $cust_svc (@svcs) {
+ my $svcnum = $cust_svc->svcnum;
+ my $svcdb = $cust_svc->part_svc->svcdb;
+ $h_search{'hashref'}{'svcnum'} = $svcnum;
+ $h_search{'table'} = "h_$svcdb";
+ my $h_svc_x = qsearchs(\%h_search)
+ or next;
+ my $class = "FS::$svcdb";
+ my $new_svc_x = $class->new({ $h_svc_x->hash });
+ my $error = $new_svc_x->insert;
+ warn "error repairing svcnum $svcnum ($svcdb) from history:\n$error\n"
+ if $error;
+ }
- @cdrs;
+ '';
}
=back