package FS::cust_svc;
+use base qw( FS::cust_main_Mixin FS::option_Common ); #FS::Record );
use strict;
-use vars qw( @ISA $DEBUG $me $ignore_quantity );
+use vars qw( $DEBUG $me $ignore_quantity $conf $ticket_system );
use Carp;
#use Scalar::Util qw( blessed );
+use List::Util qw( max );
use FS::Conf;
-use FS::Record qw( qsearch qsearchs dbh str2time_sql );
-use FS::cust_pkg;
+use FS::Record qw( qsearch qsearchs dbh str2time_sql str2time_sql_closing );
use FS::part_pkg;
use FS::part_svc;
use FS::pkg_svc;
#most FS::svc_ classes are autoloaded in svc_x emthod
use FS::svc_acct; #this one is used in the cache stuff
-@ISA = qw( FS::cust_main_Mixin FS::option_Common ); #FS::Record );
$DEBUG = 0;
$me = '[cust_svc]';
$ignore_quantity = 0;
+#ask FS::UID to run this stuff for us later
+FS::UID->install_callback( sub {
+ $conf = new FS::Conf;
+ $ticket_system = $conf->config('ticket_system')
+});
+
sub _cache {
my $self = shift;
my ( $hashref, $cache ) = @_;
=cut
+my $rt_session;
+
sub delete {
my $self = shift;
+
+ my $cust_pkg = $self->cust_pkg;
+ my $custnum = $cust_pkg->custnum if $cust_pkg;
+
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});
+ if ( $ticket_system eq 'RT_Internal' ) {
+ unless ( $rt_session ) {
+ FS::TicketSystem->init;
+ $rt_session = FS::TicketSystem->session;
+ }
+ my $links = RT::Links->new($rt_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;
+ my ($val, $msg);
+ if ( $custnum ) {
+ # re-link to point to the customer instead
+ ($val, $msg) =
+ $l->SetTarget('freeside://freeside/cust_main/'.$custnum);
+ } else {
+ # unlinked service
+ ($val, $msg) = $l->Delete;
+ }
# can't do anything useful on error
warn "error unlinking ticket $svcnum: $msg\n" if !$val;
}
# }
# }
+ #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;
+ }
+ } # if pkgnum is changing
+
#my $error = $new->SUPER::replace($old, @_);
my $error = $new->SUPER::replace($old);
+
+ #trigger a relocate export on location changes
+ if ( $new->cust_pkg->locationnum != $old->cust_pkg->locationnum ) {
+ $error ||= $new->svc_x->export('relocate',
+ $new->cust_pkg->cust_location,
+ $old->cust_pkg->cust_location,
+ );
+ }
+
if ( $error ) {
$dbh->rollback if $oldAutoCommit;
return $error if $error;
my $part_svc = qsearchs( 'part_svc', { 'svcpart' => $self->svcpart } );
return "Unknown svcpart" unless $part_svc;
- if ( $self->pkgnum ) {
- my $cust_pkg = qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
- return "Unknown pkgnum" unless $cust_pkg;
- ($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.
+ if ( $self->pkgnum && ! $ignore_quantity ) {
+
+ #slightly inefficient since ->pkg_svc will also look it up, but fixing
+ # a much larger perf problem and have bigger fish to fry
+ my $cust_pkg = $self->cust_pkg;
+
+ my $pkg_svc = $self->pkg_svc
+ || new FS::pkg_svc { 'svcpart' => $self->svcpart,
+ 'pkgpart' => $cust_pkg->pkgpart,
+ 'quantity' => 0,
+ };
+
+ #service add-ons, kinda false laziness/reimplementation of part_pkg->pkg_svc
+ foreach my $part_pkg_link ( $cust_pkg->part_pkg->svc_part_pkg_link ) {
+ my $addon_pkg_svc = qsearchs('pkg_svc', {
+ pkgpart => $part_pkg_link->dst_pkgpart,
+ svcpart => $self->svcpart,
+ });
+ $pkg_svc->quantity( $pkg_svc->quantity + $addon_pkg_svc->quantity )
+ if $addon_pkg_svc;
+ }
+
+ #better error message? UI shouldn't get here
+ return "No svcpart ". $self->svcpart.
+ " services in pkgpart ". $cust_pkg->pkgpart
+ unless $pkg_svc->quantity > 0;
+
+ my $num_cust_svc = $cust_pkg->num_cust_svc( $self->svcpart );
+
+ #false laziness w/cust_pkg->part_svc
+ my $num_avail = max( 0, ($cust_pkg->quantity || 1) * $pkg_svc->quantity
+ - $num_cust_svc
+ );
+
+ #better error message? again, UI shouldn't get here
+ return "Already $num_cust_svc ". $pkg_svc->part_svc->svc.
" services for pkgnum ". $self->pkgnum
- if !$ignore_quantity && $part_svc->get('num_avail') <= 0 ;
+ if $num_avail <= 0;
+
}
$self->SUPER::check;
Returns the package this service belongs to, as a FS::cust_pkg object (see
L<FS::cust_pkg>).
-=cut
-
-sub cust_pkg {
- my $self = shift;
- qsearchs( 'cust_pkg', { 'pkgnum' => $self->pkgnum } );
-}
-
=item pkg_svc
Returns the pkg_svc record for for this service, if applicable.
#select a unix time conversion function based on database type
my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
+ my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
my $username = $part_export->export_username($svc_x);
FROM radacct
WHERE UserName = ?
$realm
- AND $str2time AcctStartTime) >= ?
- AND $str2time AcctStopTime ) < ?
- AND $str2time AcctStopTime ) > 0
+ AND $str2time AcctStartTime $closing >= ?
+ AND $str2time AcctStopTime $closing < ?
+ AND $str2time AcctStopTime $closing > 0
AND AcctStopTime IS NOT NULL"
) or die $dbh->errstr;
$sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
if $DEBUG;
# count session start->range end
- $query = "SELECT SUM( ? - $str2time AcctStartTime ) )
+ $query = "SELECT SUM( ? - $str2time AcctStartTime $closing )
FROM radacct
WHERE UserName = ?
$realm
- AND $str2time AcctStartTime ) >= ?
- AND $str2time AcctStartTime ) < ?
- AND ( ? - $str2time AcctStartTime ) ) < 86400
- AND ( $str2time AcctStopTime ) = 0
+ AND $str2time AcctStartTime $closing >= ?
+ AND $str2time AcctStartTime $closing < ?
+ AND ( ? - $str2time AcctStartTime $closing ) < 86400
+ AND ( $str2time AcctStopTime $closing = 0
OR AcctStopTime IS NULL )";
$sth = $dbh->prepare($query) or die $dbh->errstr;
$sth->execute( $end,
if $DEBUG;
#count range start->session end
- $sth = $dbh->prepare("SELECT SUM( $str2time AcctStopTime ) - ? )
+ $sth = $dbh->prepare("SELECT SUM( $str2time AcctStopTime $closing - ? )
FROM radacct
WHERE UserName = ?
$realm
- AND $str2time AcctStartTime ) < ?
- AND $str2time AcctStopTime ) >= ?
- AND $str2time AcctStopTime ) < ?
- AND $str2time AcctStopTime ) > 0
+ AND $str2time AcctStartTime $closing < ?
+ AND $str2time AcctStopTime $closing >= ?
+ AND $str2time AcctStopTime $closing < ?
+ AND $str2time AcctStopTime $closing > 0
AND AcctStopTime IS NOT NULL"
) or die $dbh->errstr;
$sth->execute( $start,
FROM radacct
WHERE UserName = ?
$realm
- AND $str2time AcctStartTime ) < ?
- AND ( $str2time AcctStopTime ) >= ?
+ AND $str2time AcctStartTime $closing < ?
+ AND ( $str2time AcctStopTime $closing >= ?
)"
# OR AcctStopTime = 0
# OR AcctStopTime IS NULL )"
#select a unix time conversion function based on database type
my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
+ my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
my $username = $part_export->export_username($svc_x);
FROM radacct
WHERE UserName = ?
$realm
- AND $str2time AcctStopTime ) >= ?
- AND $str2time AcctStopTime ) < ?
+ AND $str2time AcctStopTime $closing >= ?
+ AND $str2time AcctStopTime $closing < ?
AND AcctStopTime IS NOT NULL"
) or die $dbh->errstr;
$sth->execute($username, ($realm ? $realmparam : ()), $start, $end)
}
+#note: implementation here, POD in FS::svc_acct
+# false laziness w/above
+sub attribute_last_sqlradacct {
+ my($self, $attrib) = @_;
+
+ my $mes = "$me attribute_last_sqlradacct:";
+
+ my $svc_x = $self->svc_x;
+
+ my @part_export = $self->part_svc->part_export_usage;
+ die "no accounting-capable exports are enabled for ". $self->part_svc->svc.
+ " service definition"
+ unless @part_export;
+ #or return undef;
+
+ my $value = '';
+ my $AcctStartTime = 0;
+
+ foreach my $part_export ( @part_export ) {
+
+ next if $part_export->option('ignore_accounting');
+
+ warn "$mes connecting to sqlradius database\n"
+ if $DEBUG;
+
+ my $dbh = DBI->connect( map { $part_export->option($_) }
+ qw(datasrc username password) )
+ or die "can't connect to sqlradius database: ". $DBI::errstr;
+
+ warn "$mes connected to sqlradius database\n"
+ if $DEBUG;
+
+ #select a unix time conversion function based on database type
+ my $str2time = str2time_sql( $dbh->{Driver}->{Name} );
+ my $closing = str2time_sql_closing( $dbh->{Driver}->{Name} );
+
+ my $username = $part_export->export_username($svc_x);
+
+ warn "$mes finding most-recent $attrib\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 $attrib, $str2time AcctStartTime $closing
+ FROM radacct
+ WHERE UserName = ?
+ $realm
+ ORDER BY AcctStartTime DESC LIMIT 1
+ ") or die $dbh->errstr;
+ $sth->execute($username, ($realm ? $realmparam : ()) )
+ or die $sth->errstr;
+
+ my $row = $sth->fetchrow_arrayref;
+ if ( defined($row->[0]) && $row->[1] > $AcctStartTime ) {
+ $value = $row->[0];
+ $AcctStartTime = $row->[1];
+ }
+
+ warn "$mes done\n"
+ if $DEBUG;
+
+ }
+
+ $value;
+
+}
+
=item get_session_history TIMESTAMP_START TIMESTAMP_END
See L<FS::svc_acct/get_session_history>. Equivalent to
}
-=item tickets
+=item tickets [ STATUS ]
Returns an array of hashes representing the tickets linked to this service.
+An optional status (or arrayref or hashref of statuses) may be specified.
+
=cut
sub tickets {
my $self = shift;
+ my $status = ( @_ && $_[0] ) ? shift : '';
my $conf = FS::Conf->new;
my $num = $conf->config('cust_main-max_tickets') || 10;
if ( $conf->config('ticket_system') ) {
unless ( $conf->config('ticket_system-custom_priority_field') ) {
- @tickets = @{ FS::TicketSystem->service_tickets($self->svcnum, $num) };
+ @tickets = @{ FS::TicketSystem->service_tickets( $self->svcnum,
+ $num,
+ undef,
+ $status,
+ )
+ };
} else {
last if scalar(@tickets) >= $num;
push @tickets,
@{ FS::TicketSystem->service_tickets( $self->svcnum,
- $num - scalar(@tickets),
- $priority,
- )
- };
+ $num - scalar(@tickets),
+ $priority,
+ $status,
+ )
+ };
}
}
}
(@tickets);
}
+sub API_getinfo {
+ my $self = shift;
+ my $svc_x = $self->svc_x;
+ +{ ( map { $_=>$self->$_ } $self->fields ),
+ ( map { $svc_x=>$svc_x->$_ } $svc_x->fields ),
+ };
+}
=back
(
'table' => 'cust_svc',
'select' => 'svc_all.svcnum AS svcnum, '.
- 'COALESCE(svc_all.svcdb, part_svc.svcdb) AS svcdb',
+ 'COALESCE(svc_all.svcdb, part_svc.svcdb) AS svcdb, '.
+ 'cust_svc.*',
'addl_from' => $addl_from,
'hashref' => {},
'extra_sql' => $extra_sql,