--- /dev/null
+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;
--- /dev/null
+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;
use POSIX qw(strftime);
use SL::CT;
+use SL::CTI;
use SL::CVar;
use SL::Request qw(flatten);
use SL::DB::Business;
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);
}
$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);
}
# 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
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;
+}
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;
+}
--- /dev/null
+phone.png
\ No newline at end of file
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 = $('<a href="" id="' + action_id + '" class="cti_call_action" target="_blank" tabindex="-1"></a>');
+ $input.wrap('<span nobr></span>').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));
+ });
+}
'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',
'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',
'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',
'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.',
'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',
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
--- /dev/null
+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';
--- /dev/null
+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';
</tr>
</table>
+ <h2>[%- LxERP.t8("CTI settings") %]</h2>
+
+ <table>
+ <tr>
+ <th align="right">[% LxERP.t8("Phone extension") %]</th>
+ <td>[% L.input_tag("user.config_values.phone_extension", props.phone_extension) %]</td>
+ </tr>
+
+ <tr>
+ <th align="right">[% LxERP.t8("Phone password") %]</th>
+ <td>[% L.input_tag("user.config_values.phone_password", props.phone_password) %]</td>
+ </tr>
+ </table>
+
<h2>[%- LxERP.t8("Access to clients") %]</h2>
[% IF SELF.all_clients.size %]
--- /dev/null
+<body>
+[% PROCESS 'common/flash.html' %]
+</body>
+</html>
--- /dev/null
+[%- USE HTML %][%- USE LxERP -%]
+<body>
+
+<h1>[% HTML.escape(title) %]</h1>
+
+[% IF !SELF.internal_extensions.size %]
+ <p>[% LxERP.t8("No internal phone extensions have been configured yet.") %]</p>
+
+[% ELSE %]
+ <table>
+ <tr class="listheading">
+ <th>[% LxERP.t8("Name") %]</th>
+ <th>[% LxERP.t8("Phone extension") %]</th>
+ </tr>
+
+ [%- FOREACH extension = SELF.internal_extensions %]
+ <tr class="listrow">
+ <td>[% HTML.escape(extension.name) %]</td>
+ <td><a href="[% HTML.escape(extension.call_link) %]" class="cti_call_action">[% HTML.escape(extension.phone_extension) %]</a></td>
+ </tr>
+ [%- END %]
+ </table>
+[% END %]
+</body>
+</html>
[%- USE LxERP %]
[%- USE L %]
+[% L.hidden_tag('_cti_enabled', !!LXCONFIG.cti.dial_command) %]
+
[% cv_cvars = SELF.cv.cvars_by_config %]
<form method="post" action="controller.pl">
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(); }});",
)
%]
</td>
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(); }});",
)
%]
</td>