From e3503e19a5a6c876f410903a3946dd9f1597aa46 Mon Sep 17 00:00:00 2001 From: Mark Wells Date: Sun, 28 Dec 2014 23:27:48 -0800 Subject: [PATCH] voip.ms export, #31834 --- FS/FS/Schema.pm | 1 + FS/FS/part_export.pm | 72 ++- FS/FS/part_export/voip_ms.pm | 648 ++++++++++++++++++++++++++ FS/FS/svc_phone.pm | 14 + httemplate/browse/part_export.cgi | 24 +- httemplate/edit/part_export.cgi | 16 +- httemplate/edit/svc_phone.cgi | 3 + httemplate/elements/select-did.html | 24 +- httemplate/elements/tr-select-sip_server.html | 48 ++ httemplate/view/svc_phone.cgi | 3 + 10 files changed, 813 insertions(+), 40 deletions(-) create mode 100644 FS/FS/part_export/voip_ms.pm create mode 100644 httemplate/elements/tr-select-sip_server.html diff --git a/FS/FS/Schema.pm b/FS/FS/Schema.pm index 8b362a7a6..f55956469 100644 --- a/FS/FS/Schema.pm +++ b/FS/FS/Schema.pm @@ -5656,6 +5656,7 @@ sub tables_hashref { 'e911_class', 'char', 'NULL', 1, '', '', 'e911_type', 'char', 'NULL', 1, '', '', 'circuit_svcnum', 'int', 'NULL', '', '', '', + 'sip_server', 'varchar', 'NULL', $char_d, '', '', ], 'primary_key' => 'svcnum', 'unique' => [ [ 'sms_carrierid', 'sms_account'] ], diff --git a/FS/FS/part_export.pm b/FS/FS/part_export.pm index 7819a7c86..f3d977480 100644 --- a/FS/FS/part_export.pm +++ b/FS/FS/part_export.pm @@ -535,23 +535,6 @@ sub default_export_machine { die "no default export hostname for export ".$self->exportnum; } -=item svc_role SVC_X - -Returns the role that SVC_X occupies with respect to this export, if any. -This is part of the part_svc's export configuration. - -=cut - -sub svc_role { - my $self = shift; - my $svc_x = shift; - my $cust_svc = $svc_x->cust_svc or return ''; - my $export_svc = qsearchs('export_svc', { exportnum => $self->exportnum, - svcpart => $cust_svc->svcpart }) - or return ''; - $export_svc->role; -} - #these should probably all go away, just let the subclasses define em =item export_insert SVC_OBJECT @@ -753,6 +736,61 @@ sub get_dids_npa_select { 1; } # change the phone number for a service. if false, then they can't (have to # reprovision completely). +=item svc_role SVC + +Returns the role that SVC occupies with respect to this export, if any. +This is part of the part_svc's export configuration. + +=cut + +sub svc_role { + my $self = shift; + my $svc_x = shift; + my $cust_svc = $svc_x->cust_svc or return ''; + my $export_svc = qsearchs('export_svc', { exportnum => $self->exportnum, + svcpart => $cust_svc->svcpart }) + or return ''; + $export_svc->role; +} + +=item svc_with_role { SVC | PKGNUM }, ROLE + +Given a svc_* object SVC or pkgnum PKG, and a role name ROLE, finds the +service(s) in the same package that are linked to this export with ROLE. + +=cut + +sub svc_with_role { + my $self = shift; + my $svc_or_pkgnum = shift; + my $role = shift; + my $pkgnum; + if ( ref $svc_or_pkgnum ) { + $pkgnum = $svc_or_pkgnum->cust_svc->pkgnum or return ''; + } else { + $pkgnum = $svc_or_pkgnum; + } + my $role_info = $self->info->{roles}->{$role} + or die "role '$role' does not exist for export '".$self->exporttype."'\n"; + my $svcdb = $role_info->{svcdb}; + + my @svcs = qsearch({ + 'table' => $svcdb, + 'addl_from' => ' JOIN cust_svc USING (svcnum)' . + ' JOIN export_svc USING (svcpart)', + 'extra_sql' => " WHERE cust_svc.pkgnum = $pkgnum" . + " AND export_svc.exportnum = ".$self->exportnum . + " AND export_svc.role = '$role'", + }); + if ( $role_info->{multiple} ) { + return @svcs; + } else { + if ( @svcs > 1 ) { + warn "multiple $role services in pkgnum $pkgnum; returning the first one.\n"; + } + return $svcs[0]; + } +} =back diff --git a/FS/FS/part_export/voip_ms.pm b/FS/FS/part_export/voip_ms.pm new file mode 100644 index 000000000..44ce908eb --- /dev/null +++ b/FS/FS/part_export/voip_ms.pm @@ -0,0 +1,648 @@ +package FS::part_export::voip_ms; + +use base qw( FS::part_export ); +use strict; + +use Tie::IxHash; +use LWP::UserAgent; +use URI; +use URI::Escape; +use JSON; +use HTTP::Request::Common; +use Cache::FileCache; + +our $me = '[voip.ms]'; +our $DEBUG = 2; +our $base_url = 'https://voip.ms/api/v1/rest.php'; + +# cache cities and provinces +our $CACHE; # a FileCache; their API is not as quick as I'd like +our $cache_timeout = 86400; # seconds + +tie my %options, 'Tie::IxHash', + 'account' => { label => 'Main account ID' }, + 'username' => { label => 'API username', }, + 'password' => { label => 'API password', }, + 'debug' => { label => 'Enable debugging', type => 'checkbox', value => 1 }, + # could dynamically pull this from the API... + 'protocol' => { + label => 'Protocol', + type => 'select', + options => [ 1, 3 ], + option_labels => { 1 => 'SIP', 3 => 'IAX' }, + }, + 'auth_type' => { + label => 'Authorization type', + type => 'select', + options => [ 1, 2 ], + option_labels => { 1 => 'User/Password', 2 => 'Static IP' }, + }, + 'billing_type' => { + label => 'DID billing mode', + type => 'select', + options => [ 1, 2 ], + option_labels => { 1 => 'Per minute', 2 => 'Flat rate' }, + }, + 'device_type' => { + label => 'Device type', + type => 'select', + options => [ 1, 2 ], + option_labels => { 1 => 'IP PBX, e.g. Asterisk', + 2 => 'IP phone or softphone', + }, + }, + 'canada_routing' => { + label => 'Canada routing policy', + type => 'select', + options => [ 1, 2 ], + option_labels => { 1 => 'Value (lowest price)', + 2 => 'Premium (highest quality)' + }, + }, + 'international_route' => { # yes, 'route' + label => 'International routing policy', + type => 'select', + options => [ 0, 1, 2 ], + option_labels => { 0 => 'Disable international calls', + 1 => 'Value (lowest price)', + 2 => 'Premium (highest quality)' + }, + }, + 'cnam_lookup' => { + label => 'Enable CNAM lookup on incoming calls', + type => 'checkbox', + }, + +; + +tie my %roles, 'Tie::IxHash', + 'subacct' => { label => 'SIP client', + svcdb => 'svc_acct', + }, + 'did' => { label => 'DID', + svcdb => 'svc_phone', + multiple => 1, + }, +; + +our %info = ( + 'svc' => [qw( svc_acct svc_phone )], + 'desc' => + 'Provision subaccounts and DIDs to voip.ms wholesale', + 'options' => \%options, + 'roles' => \%roles, + 'no_machine' => 1, + 'notes' => <<'END' +

Export to voip.ms hosted PBX service.

+

This requires two service definitions to be configured on the same package: +

    +
  1. An account service for the subaccount (the "login" used by the + customer's PBX or IP phone, and the call routing service). This should + be attached to the export in the "subacct" role. If you are using + password authentication, the username and _password will + be used to authenticate to voip.ms. If you are using static IP + authentication, the slipip (IP address) field should be set to + the address.
  2. +
  3. A phone service for a DID, attached to the export in the DID role. + You must select a server for the "SIP Host" field. Calls from this DID + will be routed to the customer via that server.
  4. +
+

+

Export options: +

+ The other options correspond to options in either the subaccount or DID + configuration menu in the voip.ms portal; see documentation there for + details. +

+END +); + +sub export_insert { + my($self, $svc_x) = (shift, shift); + + my $role = $self->svc_role($svc_x); + if ( $role eq 'subacct' ) { + + my $error = $self->insert_subacct($svc_x); + return "$me $error" if $error; + + my @existing_dids = ( $self->svc_with_role($svc_x, 'did') ); + + foreach my $svc_phone (@existing_dids) { + $error = $self->insert_did($svc_phone, $svc_x); + return "$me $error ordering DID ".$svc_phone->phonenum + if $error; + } + + } elsif ( $role eq 'did' ) { + + my $svc_acct = $self->svc_with_role($svc_x, 'subacct'); + return if !$svc_acct; + + my $error = $self->insert_did($svc_x, $svc_acct); + return "$me $error" if $error; + + } + ''; +} + +sub export_replace { + my ($self, $svc_new, $svc_old) = @_; + my $role = $self->svc_role($svc_new); + my $error; + if ( $role eq 'subacct' ) { + $error = $self->replace_subacct($svc_new, $svc_old); + } elsif ( $role eq 'did' ) { + $error = $self->replace_did($svc_new, $svc_old); + } + return "$me $error" if $error; + ''; +} + +sub export_delete { + my ($self, $svc_x) = (shift, shift); + my $role = $self->svc_role($svc_x); + if ( $role eq 'subacct' ) { + + my @existing_dids = ( $self->svc_with_role($svc_x, 'did') ); + + my $error; + foreach my $svc_phone (@existing_dids) { + $error = $self->delete_did($svc_phone); + return "$me $error canceling DID ".$svc_phone->phonenum + if $error; + } + + $error = $self->delete_subacct($svc_x); + return "$me $error" if $error; + + } elsif ( $role eq 'did' ) { + + my $svc_acct = $self->svc_with_role($svc_x, 'subacct'); + return if !$svc_acct; + + my $error = $self->delete_did($svc_x); + return "$me $error" if $error; + + } + ''; +} + +sub export_suspend { + my $self = shift; + my $svc_x = shift; + my $role = $self->svc_role($svc_x); + return if $role ne 'subacct'; # can't suspend DIDs directly + + my $error = $self->replace_subacct($svc_x, $svc_x); # will disable it + return "$me $error" if $error; + ''; +} + +sub export_unsuspend { + my $self = shift; + my $svc_x = shift; + my $role = $self->svc_role($svc_x); + return if $role ne 'subacct'; # can't suspend DIDs directly + + $svc_x->set('unsuspended', 1); # hack to tell replace_subacct to do it + my $error = $self->replace_subacct($svc_x, $svc_x); #same + return "$me $error" if $error; + ''; +} + + +sub insert_subacct { + my ($self, $svc_acct) = @_; + my $method = 'createSubAccount'; + my $content = $self->subacct_content($svc_acct); + + my $result = $self->api_request($method, $content); + if ( $result->{status} ne 'success' ) { + return $result->{status}; # or look up the error message string? + } + + # result includes the account ID and the full username, but we don't + # really need to keep those; we can look them up later + ''; +} + +sub insert_did { + my ($self, $svc_phone, $svc_acct) = @_; + my $method = 'orderDID'; + my $content = $self->did_content($svc_phone, $svc_acct); + my $result = $self->api_request($method, $content); + if ( $result->{status} ne 'success' ) { + return $result->{status}; # or look up the error message string? + } + ''; +} + +sub delete_subacct { + my ($self, $svc_acct) = @_; + my $account = $self->option('account') . '_' . $svc_acct->username; + + my $id = $self->subacct_id($svc_acct); + if ( $id =~ /\D/ ) { + + return $id; # it's an error + + } elsif ( $id eq '' ) { + + return ''; # account doesn't exist, don't need to delete + + } # else it's numeric + + warn "$me deleting account $account with ID $id\n" if $DEBUG; + my $result = $self->api_request('delSubAccount', { id => $id }); + if ( $result->{status} ne 'success' ) { + return $result->{status}; + } + ''; +} + +sub delete_did { + my ($self, $svc_phone) = @_; + my $phonenum = $svc_phone->phonenum; + + my $result = $self->api_request('cancelDID', { did => $phonenum }); + if ( $result->{status} ne 'success' and $result->{status} ne 'invalid_did' ) + { + return $result->{status}; + } + ''; +} + +sub replace_subacct { + my ($self, $svc_new, $svc_old) = @_; + if ( $svc_new->username ne $svc_old->username ) { + return "can't change account username; delete and recreate the account instead"; + } + + my $id = $self->subacct_id($svc_new); + if ( $id =~ /\D/ ) { + + return $id; + + } elsif ( $id eq '' ) { + + # account doesn't exist; provision it anew + return $self->insert_subacct($svc_new); + + } + + my $content = $self->subacct_content($svc_new); + delete $content->{username}; + $content->{id} = $id; + + my $result = $self->api_request('setSubAccount', $content); + if ( $result->{status} ne 'success' ) { + return $result->{status}; + } + + ''; +} + +sub replace_did { + my ($self, $svc_new, $svc_old) = @_; + if ( $svc_new->phonenum ne $svc_old->phonenum ) { + return "can't change DID phone number"; + } + # check that there's a subacct set up + my $svc_acct = $self->svc_with_role($svc_new, 'subacct') + or return ''; + + # check for the existing DID + my $result = $self->api_request('getDIDsInfo', + { did => $svc_new->phonenum } + ); + if ( $result->{status} eq 'invalid_did' ) { + + # provision the DID + return $self->insert_did($svc_new, $svc_acct); + + } elsif ( $result->{status} ne 'success' ) { + + return $result->{status}; + + } + + my $existing = $result->{dids}[0]; + + my $content = $self->did_content($svc_new, $svc_acct); + if ( $content->{billing_type} == $existing->{billing_type} ) { + delete $content->{billing_type}; # confuses the server otherwise + } + $result = $self->api_request('setDIDInfo', $content); + if ( $result->{status} ne 'success' ) { + return $result->{status}; + } + + return ''; +} + +####################### +# CONVENIENCE METHODS # +####################### + +sub subacct_id { + my ($self, $svc_acct) = @_; + my $account = $self->option('account') . '_' . $svc_acct->username; + + # look up the subaccount's numeric ID + my $result = $self->api_request('getSubAccounts', { account => $account }); + if ( $result->{status} eq 'invalid_account' ) { + return ''; + } elsif ( $result->{status} ne 'success' ) { + return "$result->{status} looking up account ID"; + } else { + return $result->{accounts}[0]{id}; + } +} + +sub subacct_content { + my ($self, $svc_acct) = @_; + + my $cust_pkg = $svc_acct->cust_svc->cust_pkg; + + my $desc = $svc_acct->finger || $svc_acct->username; + my $intl = $self->option('international_route'); + my $lockintl = 0; + if ($intl == 0) { + $intl = 1; # can't send zero + $lockintl = 1; + } + + my %auth; + if ( $cust_pkg and $cust_pkg->susp > 0 and !$svc_acct->get('unsuspended') ) { + # we can't explicitly suspend their account, so just set its password to + # a partially random string that satisfies the password rules + # (we still have their real password in the svc_acct record) + %auth = ( auth_type => 1, + password => sprintf('Suspend-%08d', int(rand(100000000)) ), + ); + } else { + %auth = ( auth_type => $self->option('auth_type'), + password => $svc_acct->_password, + ip => $svc_acct->slipip, + ); + } + return { + username => $svc_acct->username, + description => $desc, + %auth, + device_type => $self->option('device_type'), + canada_routing => $self->option('canada_routing'), + lock_international => $lockintl, + international_route => $intl, + # sensible defaults for these + music_on_hold => 'default', # silence + allowed_codecs => 'ulaw;g729;gsm', + dtmf_mode => 'AUTO', + nat => 'yes', + }; +} + +sub did_content { + my ($self, $svc_phone, $svc_acct) = @_; + + my $account = $self->option('account') . '_' . $svc_acct->username; + my $phonenum = $svc_phone->phonenum; + # look up POP number (for some reason this is assigned per DID...) + my $sip_server = $svc_phone->sip_server + or return "SIP server required"; + my $popnum = $self->cache('server_popnum')->{ $svc_phone->sip_server } + or return "SIP server '$sip_server' is unknown"; + return { + did => $phonenum, + routing => "account:$account", + # secondary routing options (failovers, voicemail) are outside our + # scope here + # though we could support them using the "forwarddst" field? + pop => $popnum, + dialtime => 60, # sensible default, add an option if needed + cnam => ($self->option('cnam_lookup') ? 1 : 0), + note => $svc_phone->phone_name, + billing_type => $self->option('billing_type'), + }; +} + +################# +# DID SELECTION # +################# + +sub get_dids_npa_select { 0 } # all Canadian VoIP providers seem to have this + +sub get_dids { + my $self = shift; + my %opt = @_; + + my ($exportnum) = $self->exportnum =~ /^(\d+)$/; + + if ( $opt{'region'} ) { + + # return numbers (probably shouldn't cache this) + my ($ratecenter, $province) = $opt{'region'} =~ /^(.*), (..)$/; + my $country = $self->cache('province_country')->{ $province }; + my $result; + if ( $country eq 'CAN' ) { + $result = $self->api_insist('getDIDsCAN', + { province => $province, + ratecenter => $ratecenter + } + ); + } elsif ( $country eq 'USA' ) { + $result = $self->api_insist('getDIDsUSA', + { state => $province, + ratecenter => $ratecenter + } + ); + } + my @return = map { $_->{did} } @{ $result->{dids} }; + return \@return; + } else { + + if ( $opt{'state'} ) { + my $province = $opt{'state'}; + + # cache() will refresh the cache if necessary, and die on failure. + # default here is only in case someone gives us a state that + # doesn't exist. + return $self->cache('province_city', $province) || []; + + } else { + + # return a list of provinces + return [ + @{ $self->cache('country_province')->{CAN} }, + @{ $self->cache('country_province')->{USA} }, + ]; + } + } +} + +sub get_sip_servers { + my $self = shift; + return [ sort keys %{ $self->cache('server_popnum') } ]; +} + +sub cache { + my $self = shift; + my $element = shift or return; + my $province = shift; + + $CACHE ||= Cache::FileCache->new({ + 'cache_root' => $FS::UID::cache_dir.'/cache'.$FS::UID::datasrc, + 'namespace' => __PACKAGE__, + 'default_expires_in' => $cache_timeout, + }); + + if ( $element eq 'province_city' ) { + $element .= ".$province"; + } + return $CACHE->get($element) || $self->reload_cache($element); +} + +sub reload_cache { + my $self = shift; + my $element = shift; + if ( $element eq 'province_country' or $element eq 'country_province' ) { + # populate provinces/states + + my %province_country; + my %country_province = ( CAN => [], USA => [] ); + + my $result = $self->api_insist('getProvinces'); + foreach my $province (map { $_->{province} } @{ $result->{provinces} }) { + $province_country{$province} = 'CAN'; + push @{ $country_province{CAN} }, $province; + } + + $result = $self->api_insist('getStates'); + foreach my $state (map { $_->{state} } @{ $result->{states} }) { + $province_country{$state} = 'USA'; + push @{ $country_province{USA} }, $state; + } + + $CACHE->set('province_country', \%province_country); + $CACHE->set('country_province', \%country_province); + return $CACHE->get($element); + + } elsif ( $element eq 'server_popnum' ) { + + my $result = $self->api_insist('getServersInfo'); + my %server_popnum; + foreach (@{ $result->{servers} }) { + $server_popnum{ $_->{server_hostname} } = $_->{server_pop}; + } + + $CACHE->set('server_popnum', \%server_popnum); + return \%server_popnum; + + } elsif ( $element =~ /^province_city\.(\w+)$/ ) { + + my $province = $1; + + # then get the ratecenters for that province + my $country = $self->cache('province_country')->{$province}; + my @ratecenters; + + if ( $country eq 'CAN' ) { + + my $result = $self->api_insist('getRateCentersCAN', + { province => $province }); + + foreach (@{ $result->{ratecenters} }) { + my $ratecenter = $_->{ratecenter} . ", $province"; # disambiguate + push @ratecenters, $ratecenter; + } + + } elsif ( $country eq 'USA' ) { + + my $result = $self->api_insist('getRateCentersUSA', + { state => $province }); + foreach (@{ $result->{ratecenters} }) { + my $ratecenter = $_->{ratecenter} . ", $province"; + push @ratecenters, $ratecenter; + } + + } + + $CACHE->set($element, \@ratecenters); + return \@ratecenters; + + } else { + return; + } +} + +############## +# API ACCESS # +############## + +=item api_request METHOD, CONTENT + +Makes a REST request with method name METHOD, and POST content CONTENT (as +a hashref). + +=cut + +sub api_request { + my $self = shift; + my ($method, $content) = @_; + $DEBUG ||= 1 if $self->option('debug'); + my $url = URI->new($base_url); + $url->query_form( + 'method' => $method, + 'api_username' => $self->option('username'), + 'api_password' => $self->option('password'), + %$content + ); + + my $request = GET($url, + 'Accept' => 'text/json', + ); + + warn "$me $method\n" . $request->as_string ."\n" if $DEBUG; + my $ua = LWP::UserAgent->new; + my $response = $ua->request($request); + warn "$me received\n" . $response->as_string ."\n" if $DEBUG; + if ( !$response->is_success ) { + return { status => $response->content }; + } + + return decode_json($response->content); +} + +=item api_insist METHOD, CONTENT + +Exactly like L, but if the returned "status" is not "success", +throws an exception. + +=cut + +sub api_insist { + my $self = shift; + my $method = $_[0]; + my $result = $self->api_request(@_); + if ( $result->{status} eq 'success' ) { + return $result; + } elsif ( $result->{status} ) { + die "$me $method: $result->{status}\n"; + } else { + die "$me $method: no status returned\n"; + } +} + +1; diff --git a/FS/FS/svc_phone.pm b/FS/FS/svc_phone.pm index bd35cbac4..06ce94848 100644 --- a/FS/FS/svc_phone.pm +++ b/FS/FS/svc_phone.pm @@ -136,6 +136,15 @@ Class of Service for E911 service (per the NENA 2.1 standard). Type of Service for E911 service. +=item circuit_svcnum + +The L record for the physical circuit that transports this +phone line. + +=item sip_server + +The hostname of the SIP server that this phone number is routed to. + =back =head1 METHODS @@ -253,6 +262,10 @@ sub table_info { disable_inventory => 1, multiple => 1, }, + 'sip_server' => { + label => 'SIP Host', + %dis2, + }, }, }; } @@ -548,6 +561,7 @@ sub check { 'native', 'portin-reject', 'portout-reject']) || $self->ut_enumn('portable', ['','Y']) || $self->ut_textn('lnp_reject_reason') + || $self->ut_domainn('sip_server') ; return $error if $error; diff --git a/httemplate/browse/part_export.cgi b/httemplate/browse/part_export.cgi index 876633afc..1f835d729 100755 --- a/httemplate/browse/part_export.cgi +++ b/httemplate/browse/part_export.cgi @@ -60,21 +60,25 @@ function part_export_areyousure(href) { % my %opt = $part_export->options; % my $defs = $part_export->info->{options}; % my %multiples; -% foreach my $opt (keys %$defs) { # is a Tie::IxHash -% my $group = $defs->{$opt}->{multiple}; +% foreach my $optname (keys %$defs) { # is a Tie::IxHash +% my $def = $defs->{$optname}; +% my $group = $def->{multiple}; % if ( $group ) { -% my @values = split("\n", $opt{$opt}); +% my @values = split("\n", $opt{$optname}); % $multiples{$group} ||= []; -% push @{ $multiples{$group} }, [ $opt, @values ] if @values; -% delete $opt{$opt}; -% } elsif (length($opt{$opt})) { # the normal case -%# foreach my $opt ( keys %opt ) { +% push @{ $multiples{$group} }, [ $optname, @values ] if @values; +% delete $opt{$optname}; +% } elsif (length($opt{$optname})) { # the normal case +% my $value = $opt{$optname}; +% if ( $def->{option_labels} ) { +% $value = $def->{option_labels}->{$value} || $value; +% } - <% $opt %>:  - <% encode_entities($opt{$opt}) %> + <% $optname %>:  + <% encode_entities($value) %> -% delete $opt{$opt}; +% delete $opt{$optname}; % } % } % # now any that are somehow not in the options list diff --git a/httemplate/edit/part_export.cgi b/httemplate/edit/part_export.cgi index 2897cf39d..0e53e29d0 100644 --- a/httemplate/edit/part_export.cgi +++ b/httemplate/edit/part_export.cgi @@ -201,6 +201,15 @@ my $widget = new HTML::Widgets::SelectLayers( $html .= qq!$label!; } if ( $type eq 'select' ) { + + # 'select' options can specify options one of two ways: + # the "preferred" way: + # options: arrayref of allowed option values + # option_labels: hashref of option value => label + # OR the weird and semi-deprecated way: + # option_values: coderef to return a list of allowed option values + # option_label: coderef to take an option value and return its label + my $size = defined($optinfo->{size}) ? " SIZE=" . $optinfo->{size} : ''; my $multi = ($optinfo->{multi} || $optinfo->{multiple}) ? ' MULTIPLE' : ''; @@ -218,10 +227,15 @@ my $widget = new HTML::Widgets::SelectLayers( #} else { my $selected = ($multi ? grep {$_ eq $select_option} @values : $select_option eq $value ) ? ' SELECTED' : ''; my $label = $select_option; - if (defined($optinfo->{option_label})) { + if ( defined $optinfo->{option_label} ) { my $labelsub = $optinfo->{option_label}; $label = &$labelsub($select_option); + } elsif ( defined $optinfo->{option_labels} ) { + if (exists $optinfo->{option_labels}->{$select_option}) { + $label = $optinfo->{option_labels}->{$select_option}; + } } + $html .= qq!!; #} diff --git a/httemplate/edit/svc_phone.cgi b/httemplate/edit/svc_phone.cgi index f9c0d4005..f1471e283 100644 --- a/httemplate/edit/svc_phone.cgi +++ b/httemplate/edit/svc_phone.cgi @@ -132,6 +132,9 @@ my $begin_callback = sub { value => 'Carrier Information', colspan => 8, }, + { field => 'sip_server', + type => 'select-sip_server', + }, { field => 'sms_carrierid', label => 'SMS Carrier', type => 'select-cdr_carrier', diff --git a/httemplate/elements/select-did.html b/httemplate/elements/select-did.html index c39603156..8a91d7a61 100644 --- a/httemplate/elements/select-did.html +++ b/httemplate/elements/select-did.html @@ -81,18 +81,18 @@ Example: % # if/when other folks need an areacode-less DID selector that goes % # directly from state to region - - <% include('/elements/select.html', - 'field' => 'phonenum_state', - 'id' => 'phonenum_state', - 'options' => [ '', @{ $export->get_dids } ], - 'labels' => { '' => 'Select province' }, - 'onchange' => 'phonenum_state_changed(this);', - 'disabled' => ( $manual_checked ? 1 : 0 ), - ) - %> -
>Province - + + <% include('/elements/select.html', + 'field' => 'phonenum_state', + 'id' => 'phonenum_state', + 'options' => [ '', @{ $export->get_dids } ], + 'labels' => { '' => 'Select province' }, + 'onchange' => 'phonenum_state_changed(this);', + 'disabled' => ( $manual_checked ? 1 : 0 ), + ) + %> +
>Province + <% include('/elements/select-region.html', diff --git a/httemplate/elements/tr-select-sip_server.html b/httemplate/elements/tr-select-sip_server.html new file mode 100644 index 000000000..8df1b6288 --- /dev/null +++ b/httemplate/elements/tr-select-sip_server.html @@ -0,0 +1,48 @@ +% if ( $columnflag eq 'F' ) { +<& fixed.html, %opt &> +% } elsif ( $use_selector ) { +% my $servers = $exports[0]->get_sip_servers; +% # pretty simple selector, they're all just hostnames/IP addresses +<& tr-select.html, + %opt, + options => $servers, +&> +% } else { +<& tr-input-text.html, %opt &> +% } + + +<%init> + +my %opt = @_; +my $cell_style = $opt{'cell_style'} ? 'STYLE="'. $opt{'cell_style'}. '"' : ''; + +$opt{'field'} ||= 'sip_server'; + +#false laziness w/select-did.html +#XXX make sure this comes through on errors too +my $svcpart = $opt{'svcpart'} + || $opt{'object'}->svcpart + || $opt{'object'}->cust_svc->svcpart; + +my $part_svc = qsearchs('part_svc', { 'svcpart'=>$svcpart } ); +die "unknown svcpart $svcpart" unless $part_svc; + +my $columnflag; +my $psc = $part_svc->part_svc_column($opt{'field'}); +if ( $psc ) { + $columnflag = $psc->columnflag; +} + +my @exports = $part_svc->part_export_did; +if ( scalar(@exports) > 1 ) { + die "more than one DID-providing export attached to svcpart $svcpart"; +} + +my $use_selector = 0; + +if ( $exports[0] and $exports[0]->can('get_sip_servers') ) { + $use_selector = 1; +} + + diff --git a/httemplate/view/svc_phone.cgi b/httemplate/view/svc_phone.cgi index 1c0fb39c5..aca412969 100644 --- a/httemplate/view/svc_phone.cgi +++ b/httemplate/view/svc_phone.cgi @@ -19,6 +19,7 @@ my %labels = map { $_ => ( ref($fields->{$_}) my @fields = qw( countrycode phonenum sim_imsi ); push @fields, 'domain' if $conf->exists('svc_phone-domain'); push @fields, qw( pbx_title ); +$labels{pbx_title} = 'PBX'; if ( $conf->exists('showpasswords') ) { push @fields, qw( sip_password ); @@ -58,6 +59,8 @@ push @fields, { field => 'circuit_label', link => [ $p.'view/svc_circuit.html?', 'circuit_svcnum' ] }; +push @fields, 'sip_server'; + my $html_foot = sub { my $svc_phone = shift; -- 2.11.0