X-Git-Url: http://git.freeside.biz/gitweb/?p=freeside.git;a=blobdiff_plain;f=FS%2FFS%2Fsvc_pbx.pm;h=a5e181d9d321442349126cbe2bdcbdaf337e3e2e;hp=49509b5ef7c39aafa5d91b49544baeff9666e4bb;hb=8f1500df332a86f1abe55a046778e42b21459430;hpb=9ad4f8407cc106ef5815e65bce2ee873cd0896c2 diff --git a/FS/FS/svc_pbx.pm b/FS/FS/svc_pbx.pm index 49509b5ef..a5e181d9d 100644 --- a/FS/FS/svc_pbx.pm +++ b/FS/FS/svc_pbx.pm @@ -1,9 +1,14 @@ package FS::svc_pbx; +use base qw( FS::o2m_Common FS::device_Common FS::svc_External_Common ); use strict; -use base qw( FS::svc_External_Common ); -#use FS::Record qw( qsearch qsearchs ); +use Tie::IxHash; +use FS::Record qw( qsearch qsearchs dbh ); +use FS::PagedSearch qw( psearch ); +use FS::Conf; use FS::cust_svc; +use FS::svc_phone; +use FS::svc_acct; =head1 NAME @@ -53,6 +58,15 @@ PBX name Maximum number of extensions +=item max_simultaneous + +Maximum number of simultaneous users + +=item ip_addr + +The IP address of this PBX, if that's relevant. This must be a valid IP +address (or blank), but it's not checked for block assignment or uniqueness. + =back =head1 METHODS @@ -72,36 +86,26 @@ points to. You can ask the object for a copy with the I method. sub table { 'svc_pbx'; } sub table_info { + + tie my %fields, 'Tie::IxHash', + 'svcnum' => 'PBX', + 'id' => 'PBX/Tenant ID', + 'uuid' => 'External UUID', + 'title' => 'Name', + 'max_extensions' => 'Maximum number of User Extensions', + 'max_simultaneous' => 'Maximum number of simultaneous users', + 'ip_addr' => 'IP address', + ; + { 'name' => 'PBX', - 'name_plural' => 'PBXs', #optional, - 'longname_plural' => 'PBXs', #optional + 'name_plural' => 'PBXs', + 'lcname_plural' => 'PBXs', + 'longname_plural' => 'PBXs', 'sorts' => 'svcnum', # optional sort field (or arrayref of sort fields, main first) 'display_weight' => 70, 'cancel_weight' => 90, - 'fields' => { - 'id' => 'Thirdlane ID', - 'title' => 'Description', - 'max_extensions' => 'Maximum number of User Extensions', -# 'field' => 'Description', -# 'another_field' => { -# 'label' => 'Description', -# 'def_label' => 'Description for service definitions', -# 'type' => 'text', -# 'disable_default' => 1, #disable switches -# 'disable_fixed' => 1, # -# 'disable_inventory' => 1, # -# }, -# 'foreign_key' => { -# 'label' => 'Description', -# 'def_label' => 'Description for service defs', -# 'type' => 'select', -# 'select_table' => 'foreign_table', -# 'select_key' => 'key_field_in_table', -# 'select_label' => 'label_field_in_table', -# }, - - }, + 'fields' => \%fields, }; } @@ -120,13 +124,13 @@ Class method which returns an SQL fragment to search for the given string. =item label -Returns a meaningful identifier for this PBX tenant. +Returns the title field for this PBX tenant. =cut sub label { my $self = shift; - $self->label_field; #or something more complicated if necessary + $self->title; } =item insert @@ -137,18 +141,6 @@ otherwise returns false. The additional fields pkgnum and svcpart (see L) should be defined. An FS::cust_svc record will be created and inserted. -=cut - -sub insert { - my $self = shift; - my $error; - - $error = $self->SUPER::insert; - return $error if $error; - - ''; -} - =item delete Delete this record from the database. @@ -157,11 +149,42 @@ Delete this record from the database. sub delete { my $self = shift; - my $error; - - $error = $self->SUPER::delete; - return $error if $error; + 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; + + foreach my $svc_phone (qsearch('svc_phone', { 'pbxsvc' => $self->svcnum } )) { + $svc_phone->pbxsvc(''); + my $error = $svc_phone->replace; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + + foreach my $svc_acct (qsearch('svc_acct', { 'pbxsvc' => $self->svcnum } )) { + my $error = $svc_acct->delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + } + + my $error = $self->SUPER::delete; + if ( $error ) { + $dbh->rollback if $oldAutoCommit; + return $error; + } + + $dbh->commit or die $dbh->errstr if $oldAutoCommit; ''; } @@ -171,18 +194,6 @@ sub delete { Replaces the OLD_RECORD with this one in the database. If there is an error, returns the error, otherwise returns false. -=cut - -sub replace { - my ( $new, $old ) = ( shift, shift ); - my $error; - - $error = $new->SUPER::replace($old); - return $error if $error; - - ''; -} - =item suspend Called by the suspend method of FS::cust_pkg (see L). @@ -209,9 +220,171 @@ sub check { my $x = $self->setfixed; return $x unless ref($x); my $part_svc = $x; + + return + $self->ut_ipn('ip_addr') + || $self->SUPER::check; +} + +sub _check_duplicate { + my $self = shift; + + my $conf = new FS::Conf; + + $self->lock_table; + + foreach my $field ('title', 'id') { + my $global_unique = $conf->config("global_unique-pbx_$field"); + # can be 'disabled', 'enabled', or empty. + # if empty, check per exports; if not empty or disabled, check + # globally. + next if $global_unique eq 'disabled'; + my @dup = $self->find_duplicates( + ($global_unique ? 'global' : 'export') , $field + ); + next if !@dup; + return "duplicate $field '".$self->getfield($field). + "': conflicts with svcnum ".$dup[0]->svcnum; + } + return ''; +} + +=item psearch_cdrs OPTIONS + +Returns a paged search (L) for Call Detail Records +associated with this service. By default, "associated with" means that +the "charged_party" field of the CDR matches the "title" field of the +service. To access the CDRs themselves, call "->fetch" on the resulting +object. + +=over 2 +Accepts the following options: - $self->SUPER::check; +=item for_update => 1: SELECT the CDRs "FOR UPDATE". + +=item status => "" (or "done"): Return only CDRs with that processing status. + +=item inbound => 1: No-op for svc_pbx CDR processing. + +=item default_prefix => "XXX": Also accept the phone number of the service prepended +with the chosen prefix. + +=item disable_src => 1: No-op for svc_pbx CDR processing. + +=item by_svcnum => 1: Select CDRs where the svcnum field matches, instead of +title/charged_party. Normally this field is set after processing. + +=item by_ip_addr => 'src' or 'dst': Select CDRs where the src_ip_addr or +dst_ip_addr field matches title. In this case, some special logic is applied +to allow title to indicate a range of IP addresses. + +=item begin, end: Start and end of date range, as unix timestamp. + +=item cdrtypenum: Only return CDRs with this type. + +=item calltypenum: Only return CDRs with this call type. + +=back + +=cut + +sub psearch_cdrs { + my($self, %options) = @_; + my %hash = (); + my @where = (); + + my @fields = ( 'charged_party' ); + $hash{'freesidestatus'} = $options{'status'} + if exists($options{'status'}); + + if ($options{'cdrtypenum'}) { + $hash{'cdrtypenum'} = $options{'cdrtypenum'}; + } + if ($options{'calltypenum'}) { + $hash{'calltypenum'} = $options{'calltypenum'}; + } + + my $for_update = $options{'for_update'} ? 'FOR UPDATE' : ''; + + if ( $options{'by_svcnum'} ) { + $hash{'svcnum'} = $self->svcnum; + } + elsif ( $options{'by_ip_addr'} =~ /^src|dst$/) { + my $field = 'cdr.'.$options{'by_ip_addr'}.'_ip_addr'; + push @where, FS::cdr->ip_addr_sql($field, $self->title); + } + else { + #matching by title + my $title = $self->title; + + my $prefix = $options{'default_prefix'}; + + my @orwhere = map " $_ = '$title' ", @fields; + push @orwhere, map " $_ = '$prefix$title' ", @fields + if length($prefix); + if ( $prefix =~ /^\+(\d+)$/ ) { + push @orwhere, map " $_ = '$1$title' ", @fields + } + + push @where, ' ( '. join(' OR ', @orwhere ). ' ) '; + } + + if ( $options{'begin'} ) { + push @where, 'startdate >= '. $options{'begin'}; + } + if ( $options{'end'} ) { + push @where, 'startdate < '. $options{'end'}; + } + + my $extra_sql = ( keys(%hash) ? ' AND ' : ' WHERE ' ). join(' AND ', @where ) + if @where; + + psearch( { + 'table' => 'cdr', + 'hashref' => \%hash, + 'extra_sql' => $extra_sql, + 'order_by' => "ORDER BY startdate $for_update", + } ); +} + +=item get_cdrs (DEPRECATED) + +Like psearch_cdrs, but returns all the L objects at once, in a +single list. Arguments are the same as for psearch_cdrs. This can take +an unreasonably large amount of memory and is best avoided. + +=cut + +sub get_cdrs { + my $self = shift; + my $psearch = $self->psearch_cdrs($_); + qsearch ( $psearch->{query} ) +} + +=item sum_cdrs + +Takes the same options as psearch_cdrs, but returns a single row containing +"count" (the number of CDRs) and the sums of the following fields: duration, +billsec, rated_price, rated_seconds, rated_minutes. + +Note that if any calls are not rated, their rated_* fields will be null. +If you want to use those fields, pass the 'status' option to limit to +calls that have been rated. This is intentional; please don't "fix" it. + +=cut + +sub sum_cdrs { + my $self = shift; + my $psearch = $self->psearch_cdrs(@_); + $psearch->{query}->{'select'} = join(',', + 'COUNT(*) AS count', + map { "SUM($_) AS $_" } + qw(duration billsec rated_price rated_seconds rated_minutes) + ); + # hack + $psearch->{query}->{'extra_sql'} =~ s/ ORDER BY.*$//; + qsearchs ( $psearch->{query} ); } =back