From: Moritz Bunkus Date: Tue, 24 Jun 2014 11:15:43 +0000 (+0200) Subject: Generische Unterstützung für CTI: Click-to-dial X-Git-Tag: release-3.2.0beta~411^2~14 X-Git-Url: http://wagnertech.de/git?a=commitdiff_plain;h=53d80f2a01439ee73cf679c53326ec8fb32ab1ed;p=kivitendo-erp.git Generische Unterstützung für CTI: Click-to-dial --- diff --git a/SL/CTI.pm b/SL/CTI.pm new file mode 100644 index 000000000..8605ff163 --- /dev/null +++ b/SL/CTI.pm @@ -0,0 +1,52 @@ +package SL::CTI; + +use strict; + +use String::ShellQuote; + +use SL::MoreCommon qw(uri_encode); + +sub call { + my ($class, %params) = @_; + + my $config = $::lx_office_conf{cti} || {}; + my $command = $config->{dial_command} || die $::locale->text('Dial command missing in kivitendo configuration\'s [cti] section'); + my $external_prefix = $params{internal} ? '' : ($config->{external_prefix} // ''); + + my %command_args = ( + phone_extension => $::myconfig{phone_extension} || die($::locale->text('Phone extension missing in user configuration')), + phone_password => $::myconfig{phone_password} || die($::locale->text('Phone password missing in user configuration')), + number => $external_prefix . $class->sanitize_number(%params), + ); + + foreach my $key (keys %command_args) { + my $value = shell_quote($command_args{$key}); + $command =~ s{<\% ${key} \%>}{$value}gx; + } + + return `$command`; +} + +sub call_link { + my ($class, %params) = @_; + + return "controller.pl?action=CTI/call&number=" . uri_encode($class->sanitize_number(number => $params{number})) . ($params{internal} ? '&internal=1' : ''); +} + +sub sanitize_number { + my ($class, %params) = @_; + + my $config = $::lx_office_conf{cti} || {}; + my $idp = $config->{international_dialing_prefix} // '00'; + + my $number = $params{number} // ''; + $number =~ s/[^0-9+\.-]//g; # delete unsupported characters + my $countrycode = $number =~ s/^(?: $idp | \+ ) ( \d{2} )//x ? $1 : ''; # TODO: countrycodes can have more or less than 2 digits + $number =~ s/^0//x if $countrycode; # kill non standard optional zero after global identifier + + return '' unless $number; + + return ($countrycode ? $idp . $countrycode : '') . $number; +} + +1; diff --git a/SL/Controller/CTI.pm b/SL/Controller/CTI.pm new file mode 100644 index 000000000..24e3a8449 --- /dev/null +++ b/SL/Controller/CTI.pm @@ -0,0 +1,66 @@ +package SL::Controller::CTI; + +use strict; + +use SL::CTI; +use SL::DB::AuthUserConfig; +use SL::Helper::Flash; +use SL::Locale::String; + +use parent qw(SL::Controller::Base); + +use Rose::Object::MakeMethods::Generic ( + 'scalar --get_set_init' => [ qw(internal_extensions) ], +); + +sub action_call { + my ($self) = @_; + + eval { + my $res = SL::CTI->call(number => $::form->{number}, internal => $::form->{internal}); + flash('info', t8('Calling #1 now', $::form->{number})); + 1; + } or do { + flash('error', $@); + }; + + $self->render('cti/calling'); +} + +sub action_list_internal_extensions { + my ($self) = @_; + + $self->render('cti/list_internal_extensions', title => t8('Internal Phone List')); +} + +# +# filters +# + +sub init_internal_extensions { + my ($self) = @_; + + my $user_configs = SL::DB::Manager::AuthUserConfig->get_all( + where => [ + cfg_key => 'phone_extension', + '!cfg_value' => undef, + '!cfg_value' => '', + ], + with_objects => [ qw(user) ], + ); + + my %users; + foreach my $config (@{ $user_configs }) { + $users{$config->user_id} ||= { + name => $config->user->get_config_value('name') || $config->user->login, + phone_extension => $config->cfg_value, + call_link => SL::CTI->call_link(number => $config->cfg_value, internal => 1), + }; + } + + return [ + sort { lc($a->{name}) cmp lc($b->{name}) } values %users + ]; +} + +1; diff --git a/bin/mozilla/ct.pl b/bin/mozilla/ct.pl index 0793a4459..b5b83c19f 100644 --- a/bin/mozilla/ct.pl +++ b/bin/mozilla/ct.pl @@ -48,6 +48,7 @@ use POSIX qw(strftime); use SL::CT; +use SL::CTI; use SL::CVar; use SL::Request qw(flatten); use SL::DB::Business; @@ -268,6 +269,11 @@ sub list_names { my $column = $ref->{formtype} eq 'invoice' ? 'invnumber' : $ref->{formtype} eq 'order' ? 'ordnumber' : 'quonumber'; $row->{$column}->{data} = $ref->{$column}; + if (my $number = SL::CTI->sanitize_number(number => $ref->{phone})) { + $row->{phone}->{link} = SL::CTI->call_link(number => $number); + $row->{phone}->{link_class} = 'cti_call_action'; + } + $report->add_data($row); } @@ -392,6 +398,13 @@ sub list_contacts { $row->{$_}->{link} = 'mailto:' . E($ref->{$_}) if $ref->{$_}; } + for (qw(cp_phone1 cp_phone2 cp_mobile1)) { + next unless my $number = SL::CTI->sanitize_number(number => $ref->{$_}); + + $row->{$_}->{link} = SL::CTI->call_link(number => $number); + $row->{$_}->{link_class} = 'cti_call_action'; + } + $report->add_data($row); } diff --git a/config/kivitendo.conf.default b/config/kivitendo.conf.default index e6849a554..25a8d4c3b 100644 --- a/config/kivitendo.conf.default +++ b/config/kivitendo.conf.default @@ -314,3 +314,24 @@ auto_reload_resources = 0 # If set to 1 each exception will include a full stack backtrace. backtrace_on_die = 0 + +[cti] +# If you want phone numbers to be clickable then this must be set to a +# command that does the actually dialing. Within this command three +# variables are replaced before it is executed: +# +# 1. <%phone_extension%> and <%phone_password%> are taken from the user +# configuration (changeable in the admin interface). +# 2. <%number%> is the number to dial. It has already been sanitized +# and formatted correctly regarding e.g. the international dialing +# prefix. +# +# The following is an example that works with the OpenUC telephony +# server: +# dial_command = curl --insecure -X PUT https://<%phone_extension%>:<%phone_password%>@IP.AD.DR.ESS:8443/sipxconfig/rest/my/call/<%number%> +dial_command = +# If you need to dial something before the actual number then set +# external_prefix to it. +external_prefix = 0 +# The prefix for international calls (numbers starting with +). +international_dialing_prefix = 00 diff --git a/css/kivitendo/main.css b/css/kivitendo/main.css index 3b8599383..6fa432dc0 100644 --- a/css/kivitendo/main.css +++ b/css/kivitendo/main.css @@ -443,3 +443,15 @@ div.ppp_line span.ppp_block_sellprice { span.toggle_selected { font-weight: bold; } + +/* CTI */ +a.cti_call_action { + display: inline-block; + padding-left: 18px; + height: 16px; + position: relative; + top: 2px; + vertical-align: center; + background-image: url(../../image/icons/16x16/phone.png); + background-repeat: no-repeat; +} diff --git a/css/lx-office-erp/main.css b/css/lx-office-erp/main.css index f42c8724f..ab71226dc 100644 --- a/css/lx-office-erp/main.css +++ b/css/lx-office-erp/main.css @@ -494,3 +494,15 @@ div.ppp_line span.ppp_block_sellprice { span.toggle_selected { font-weight: bold; } + +/* CTI */ +a.cti_call_action { + display: inline-block; + padding-left: 18px; + height: 16px; + position: relative; + top: 2px; + vertical-align: center; + background-image: url(../../image/icons/16x16/phone.png); + background-repeat: no-repeat; +} diff --git a/image/icons/16x16/Program--Internal Phone List.png b/image/icons/16x16/Program--Internal Phone List.png new file mode 120000 index 000000000..282275d6d --- /dev/null +++ b/image/icons/16x16/Program--Internal Phone List.png @@ -0,0 +1 @@ +phone.png \ No newline at end of file diff --git a/image/icons/16x16/phone.png b/image/icons/16x16/phone.png new file mode 100644 index 000000000..f189191d7 Binary files /dev/null and b/image/icons/16x16/phone.png differ diff --git a/js/kivi.CustomerVendor.js b/js/kivi.CustomerVendor.js index 59c07354e..bfef730d4 100644 --- a/js/kivi.CustomerVendor.js +++ b/js/kivi.CustomerVendor.js @@ -160,4 +160,44 @@ namespace('kivi.CustomerVendor', function(ns) { var url = "common.pl?INPUT_ENCODING=UTF-8&action=show_history&longdescription=&input_name="+ encodeURIComponent(id); window.open(url, "_new_generic", parm); }; + + this.update_dial_action = function($input) { + var $action = $('#' + $input.prop('id') + '-dial-action'); + + if (!$action) + return true; + + var number = $input.val().replace(/\s+/g, ''); + if (number == '') + $action.hide(); + else + $action.prop('href', 'controller.pl?action=CTI/call&number=' + encodeURIComponent(number)).show(); + + return true; + }; + + this.init_dial_action = function(input) { + if ($('#_cti_enabled').val() != 1) + return false; + + var $input = $(input); + var action_id = $input.prop('id') + '-dial-action'; + + if (!$('#' + action_id).size()) { + var $action = $(''); + $input.wrap('').after($action); + + $input.change(function() { kivi.CustomerVendor.update_dial_action($input); }); + } + + kivi.CustomerVendor.update_dial_action($input); + + return true; + }; }); + +function local_reinit_widgets() { + $('#cv_phone,#shipto_shiptophone,#contact_cp_phone1,#contact_cp_phone2,#contact_cp_mobile1,#contact_cp_mobile2').each(function(idx, elt) { + kivi.CustomerVendor.init_dial_action($(elt)); + }); +} diff --git a/locale/de/all b/locale/de/all index 191f9f611..4d6c3c4c4 100755 --- a/locale/de/all +++ b/locale/de/all @@ -408,7 +408,9 @@ $self->{texts} = { 'CSV import: parts and services' => 'CSV-Import: Waren und Dienstleistungen', 'CSV import: projects' => 'CSV-Import: Projekte', 'CSV import: shipping addresses' => 'CSV-Import: Lieferadressen', + 'CTI settings' => 'CTI-Einstellungen', 'Calculate' => 'Berechnen', + 'Calling #1 now' => 'Wähle jetzt #1', 'Can not create that quantity with current stock' => 'Diese Anzahl kann mit dem gegenwärtigen Lagerbestand nicht hergestellt werden.', 'Cancel' => 'Abbrechen', 'Cancel Accounts Payables Transaction' => 'Kreditorenbuchung stornieren', @@ -803,6 +805,7 @@ $self->{texts} = { 'Destination warehouse and bin' => 'Ziellager und -lagerplatz', 'Detail view' => 'Detailanzeige', 'Details (one letter abbreviation)' => 'D', + 'Dial command missing in kivitendo configuration\'s [cti] section' => 'Wählbefehl fehlt im Abschnitt [cti] der kivitendo-Konfiguration', 'Difference' => 'Differenz', 'Dimensions' => 'Abmessungen', 'Directory' => 'Verzeichnis', @@ -1265,6 +1268,7 @@ $self->{texts} = { 'Interest' => 'Zinsen', 'Interest Rate' => 'Zinssatz', 'Internal Notes' => 'Interne Bemerkungen', + 'Internal Phone List' => 'Interne Telefonliste', 'Internal comment' => 'Interne Bemerkungen', 'Internet' => 'Internet', 'Introduction of clients' => 'Einführung von Mandanten', @@ -1535,6 +1539,7 @@ $self->{texts} = { 'No file has been uploaded yet.' => 'Es wurde noch keine Datei hochgeladen.', 'No function blocks have been created yet.' => 'Es wurden noch keine Funktionsblöcke angelegt.', 'No groups have been created yet.' => 'Es wurden noch keine Gruppen angelegt.', + 'No internal phone extensions have been configured yet.' => 'Es wurden noch keine internen Durchwahlen konfiguriert.', 'No or an unknown authenticantion module specified in "config/kivitendo.conf".' => 'Es wurde kein oder ein unbekanntes Authentifizierungsmodul in "config/kivitendo.conf" angegeben.', 'No part was found matching the search parameters.' => 'Es wurde kein Artikel gefunden, auf den die Suchparameter zutreffen.', 'No payment term has been created yet.' => 'Es wurden noch keine Zahlungsbedingungen angelegt.', @@ -1733,6 +1738,10 @@ $self->{texts} = { 'Person' => 'Person', 'Personal settings' => 'Persönliche Einstellungen', 'Phone' => 'Telefon', + 'Phone extension' => 'Durchwahl', + 'Phone extension missing in user configuration' => 'Durchwahl fehlt in der Benutzerkonfiguration', + 'Phone password' => 'Telefonpasswort', + 'Phone password missing in user configuration' => 'Telefonpasswort fehlt in der Benutzerkonfiguration', 'Phone1' => 'Telefon 1 ', 'Phone2' => 'Telefon 2', 'Pick List' => 'Sammelliste', diff --git a/menus/erp.ini b/menus/erp.ini index 637ac6431..3297621f9 100644 --- a/menus/erp.ini +++ b/menus/erp.ini @@ -787,6 +787,10 @@ action=Employee/list module=am.pl action=config +[Program--Internal Phone List] +module=controller.pl +action=CTI/list_internal_extensions + [Program--Version] module=login.pl action=company_logo diff --git a/t/cti/call_link.t b/t/cti/call_link.t new file mode 100644 index 000000000..ce762399b --- /dev/null +++ b/t/cti/call_link.t @@ -0,0 +1,22 @@ +use Test::More tests => 9; + +use strict; +use lib 't'; +use utf8; + +use_ok 'SL::CTI'; + +{ + no warnings 'once'; + $::lx_office_conf{cti}->{international_dialing_prefix} = '00'; +} + +is SL::CTI->call_link(number => '0371 5347 620'), 'controller.pl?action=CTI/call&number=03715347620'; +is SL::CTI->call_link(number => '0049(0)421-22232 22'), 'controller.pl?action=CTI/call&number=0049421-2223222'; +is SL::CTI->call_link(number => '+49(0)421-22232 22'), 'controller.pl?action=CTI/call&number=0049421-2223222'; +is SL::CTI->call_link(number => 'Tel: +49 40 809064 0'), 'controller.pl?action=CTI/call&number=0049408090640'; + +is SL::CTI->call_link(number => '0371 5347 620', internal => 1), 'controller.pl?action=CTI/call&number=03715347620&internal=1'; +is SL::CTI->call_link(number => '0049(0)421-22232 22', internal => 1), 'controller.pl?action=CTI/call&number=0049421-2223222&internal=1'; +is SL::CTI->call_link(number => '+49(0)421-22232 22', internal => 1), 'controller.pl?action=CTI/call&number=0049421-2223222&internal=1'; +is SL::CTI->call_link(number => 'Tel: +49 40 809064 0', internal => 1), 'controller.pl?action=CTI/call&number=0049408090640&internal=1'; diff --git a/t/cti/sanitize_number.t b/t/cti/sanitize_number.t new file mode 100644 index 000000000..639e7167b --- /dev/null +++ b/t/cti/sanitize_number.t @@ -0,0 +1,17 @@ +use Test::More tests => 5; + +use strict; +use lib 't'; +use utf8; + +use_ok 'SL::CTI'; + +{ + no warnings 'once'; + $::lx_office_conf{cti}->{international_dialing_prefix} = '00'; +} + +is SL::CTI->sanitize_number(number => '0371 5347 620'), '03715347620'; +is SL::CTI->sanitize_number(number => '0049(0)421-22232 22'), '0049421-2223222'; +is SL::CTI->sanitize_number(number => '+49(0)421-22232 22'), '0049421-2223222'; +is SL::CTI->sanitize_number(number => 'Tel: +49 40 809064 0'), '0049408090640'; diff --git a/templates/webpages/admin/edit_user.html b/templates/webpages/admin/edit_user.html index 94db96b12..3a8f12959 100644 --- a/templates/webpages/admin/edit_user.html +++ b/templates/webpages/admin/edit_user.html @@ -99,6 +99,20 @@ +

[%- LxERP.t8("CTI settings") %]

+ + + + + + + + + + + +
[% LxERP.t8("Phone extension") %][% L.input_tag("user.config_values.phone_extension", props.phone_extension) %]
[% LxERP.t8("Phone password") %][% L.input_tag("user.config_values.phone_password", props.phone_password) %]
+

[%- LxERP.t8("Access to clients") %]

[% IF SELF.all_clients.size %] diff --git a/templates/webpages/cti/calling.html b/templates/webpages/cti/calling.html new file mode 100644 index 000000000..60bbcd729 --- /dev/null +++ b/templates/webpages/cti/calling.html @@ -0,0 +1,4 @@ + +[% PROCESS 'common/flash.html' %] + + diff --git a/templates/webpages/cti/list_internal_extensions.html b/templates/webpages/cti/list_internal_extensions.html new file mode 100644 index 000000000..1a5dfafd4 --- /dev/null +++ b/templates/webpages/cti/list_internal_extensions.html @@ -0,0 +1,25 @@ +[%- USE HTML %][%- USE LxERP -%] + + +

[% HTML.escape(title) %]

+ +[% IF !SELF.internal_extensions.size %] +

[% LxERP.t8("No internal phone extensions have been configured yet.") %]

+ +[% ELSE %] + + + + + + + [%- FOREACH extension = SELF.internal_extensions %] + + + + + [%- END %] +
[% LxERP.t8("Name") %][% LxERP.t8("Phone extension") %]
[% HTML.escape(extension.name) %][% HTML.escape(extension.phone_extension) %]
+[% END %] + + diff --git a/templates/webpages/customer_vendor/form.html b/templates/webpages/customer_vendor/form.html index ad2da05ab..e56b3e584 100644 --- a/templates/webpages/customer_vendor/form.html +++ b/templates/webpages/customer_vendor/form.html @@ -2,6 +2,8 @@ [%- USE LxERP %] [%- USE L %] +[% L.hidden_tag('_cti_enabled', !!LXCONFIG.cti.dial_command) %] + [% cv_cvars = SELF.cv.cvars_by_config %]
diff --git a/templates/webpages/customer_vendor/tabs/contacts.html b/templates/webpages/customer_vendor/tabs/contacts.html index aca56cd00..8d03d8b60 100644 --- a/templates/webpages/customer_vendor/tabs/contacts.html +++ b/templates/webpages/customer_vendor/tabs/contacts.html @@ -18,7 +18,7 @@ empty_title = LxERP.t8('New contact'), value_key = 'cp_id', title_key = 'full_name', - onchange = "kivi.CustomerVendor.selectContact({onFormSet: function(){contactsMapWidget.testInputs();}});", + onchange = "kivi.CustomerVendor.selectContact({onFormSet: function(){ contactsMapWidget.testInputs(); local_reinit_widgets(); }});", ) %] diff --git a/templates/webpages/customer_vendor/tabs/shipto.html b/templates/webpages/customer_vendor/tabs/shipto.html index 8f4997772..ded32f3d1 100644 --- a/templates/webpages/customer_vendor/tabs/shipto.html +++ b/templates/webpages/customer_vendor/tabs/shipto.html @@ -16,7 +16,7 @@ title_key = 'displayable_id', with_empty = 1, empty_title = LxERP.t8('New shipto'), - onchange = "kivi.CustomerVendor.selectShipto({onFormSet: function(){shiptoMapWidget.testInputs();}});", + onchange = "kivi.CustomerVendor.selectShipto({onFormSet: function(){ shiptoMapWidget.testInputs(); local_reinit_widgets(); }});", ) %]