Revision 53d80f2a
Von Moritz Bunkus vor mehr als 10 Jahren hinzugefügt
SL/CTI.pm | ||
---|---|---|
1 |
package SL::CTI; |
|
2 |
|
|
3 |
use strict; |
|
4 |
|
|
5 |
use String::ShellQuote; |
|
6 |
|
|
7 |
use SL::MoreCommon qw(uri_encode); |
|
8 |
|
|
9 |
sub call { |
|
10 |
my ($class, %params) = @_; |
|
11 |
|
|
12 |
my $config = $::lx_office_conf{cti} || {}; |
|
13 |
my $command = $config->{dial_command} || die $::locale->text('Dial command missing in kivitendo configuration\'s [cti] section'); |
|
14 |
my $external_prefix = $params{internal} ? '' : ($config->{external_prefix} // ''); |
|
15 |
|
|
16 |
my %command_args = ( |
|
17 |
phone_extension => $::myconfig{phone_extension} || die($::locale->text('Phone extension missing in user configuration')), |
|
18 |
phone_password => $::myconfig{phone_password} || die($::locale->text('Phone password missing in user configuration')), |
|
19 |
number => $external_prefix . $class->sanitize_number(%params), |
|
20 |
); |
|
21 |
|
|
22 |
foreach my $key (keys %command_args) { |
|
23 |
my $value = shell_quote($command_args{$key}); |
|
24 |
$command =~ s{<\% ${key} \%>}{$value}gx; |
|
25 |
} |
|
26 |
|
|
27 |
return `$command`; |
|
28 |
} |
|
29 |
|
|
30 |
sub call_link { |
|
31 |
my ($class, %params) = @_; |
|
32 |
|
|
33 |
return "controller.pl?action=CTI/call&number=" . uri_encode($class->sanitize_number(number => $params{number})) . ($params{internal} ? '&internal=1' : ''); |
|
34 |
} |
|
35 |
|
|
36 |
sub sanitize_number { |
|
37 |
my ($class, %params) = @_; |
|
38 |
|
|
39 |
my $config = $::lx_office_conf{cti} || {}; |
|
40 |
my $idp = $config->{international_dialing_prefix} // '00'; |
|
41 |
|
|
42 |
my $number = $params{number} // ''; |
|
43 |
$number =~ s/[^0-9+\.-]//g; # delete unsupported characters |
|
44 |
my $countrycode = $number =~ s/^(?: $idp | \+ ) ( \d{2} )//x ? $1 : ''; # TODO: countrycodes can have more or less than 2 digits |
|
45 |
$number =~ s/^0//x if $countrycode; # kill non standard optional zero after global identifier |
|
46 |
|
|
47 |
return '' unless $number; |
|
48 |
|
|
49 |
return ($countrycode ? $idp . $countrycode : '') . $number; |
|
50 |
} |
|
51 |
|
|
52 |
1; |
SL/Controller/CTI.pm | ||
---|---|---|
1 |
package SL::Controller::CTI; |
|
2 |
|
|
3 |
use strict; |
|
4 |
|
|
5 |
use SL::CTI; |
|
6 |
use SL::DB::AuthUserConfig; |
|
7 |
use SL::Helper::Flash; |
|
8 |
use SL::Locale::String; |
|
9 |
|
|
10 |
use parent qw(SL::Controller::Base); |
|
11 |
|
|
12 |
use Rose::Object::MakeMethods::Generic ( |
|
13 |
'scalar --get_set_init' => [ qw(internal_extensions) ], |
|
14 |
); |
|
15 |
|
|
16 |
sub action_call { |
|
17 |
my ($self) = @_; |
|
18 |
|
|
19 |
eval { |
|
20 |
my $res = SL::CTI->call(number => $::form->{number}, internal => $::form->{internal}); |
|
21 |
flash('info', t8('Calling #1 now', $::form->{number})); |
|
22 |
1; |
|
23 |
} or do { |
|
24 |
flash('error', $@); |
|
25 |
}; |
|
26 |
|
|
27 |
$self->render('cti/calling'); |
|
28 |
} |
|
29 |
|
|
30 |
sub action_list_internal_extensions { |
|
31 |
my ($self) = @_; |
|
32 |
|
|
33 |
$self->render('cti/list_internal_extensions', title => t8('Internal Phone List')); |
|
34 |
} |
|
35 |
|
|
36 |
# |
|
37 |
# filters |
|
38 |
# |
|
39 |
|
|
40 |
sub init_internal_extensions { |
|
41 |
my ($self) = @_; |
|
42 |
|
|
43 |
my $user_configs = SL::DB::Manager::AuthUserConfig->get_all( |
|
44 |
where => [ |
|
45 |
cfg_key => 'phone_extension', |
|
46 |
'!cfg_value' => undef, |
|
47 |
'!cfg_value' => '', |
|
48 |
], |
|
49 |
with_objects => [ qw(user) ], |
|
50 |
); |
|
51 |
|
|
52 |
my %users; |
|
53 |
foreach my $config (@{ $user_configs }) { |
|
54 |
$users{$config->user_id} ||= { |
|
55 |
name => $config->user->get_config_value('name') || $config->user->login, |
|
56 |
phone_extension => $config->cfg_value, |
|
57 |
call_link => SL::CTI->call_link(number => $config->cfg_value, internal => 1), |
|
58 |
}; |
|
59 |
} |
|
60 |
|
|
61 |
return [ |
|
62 |
sort { lc($a->{name}) cmp lc($b->{name}) } values %users |
|
63 |
]; |
|
64 |
} |
|
65 |
|
|
66 |
1; |
bin/mozilla/ct.pl | ||
---|---|---|
48 | 48 |
use POSIX qw(strftime); |
49 | 49 |
|
50 | 50 |
use SL::CT; |
51 |
use SL::CTI; |
|
51 | 52 |
use SL::CVar; |
52 | 53 |
use SL::Request qw(flatten); |
53 | 54 |
use SL::DB::Business; |
... | ... | |
268 | 269 |
my $column = $ref->{formtype} eq 'invoice' ? 'invnumber' : $ref->{formtype} eq 'order' ? 'ordnumber' : 'quonumber'; |
269 | 270 |
$row->{$column}->{data} = $ref->{$column}; |
270 | 271 |
|
272 |
if (my $number = SL::CTI->sanitize_number(number => $ref->{phone})) { |
|
273 |
$row->{phone}->{link} = SL::CTI->call_link(number => $number); |
|
274 |
$row->{phone}->{link_class} = 'cti_call_action'; |
|
275 |
} |
|
276 |
|
|
271 | 277 |
$report->add_data($row); |
272 | 278 |
} |
273 | 279 |
|
... | ... | |
392 | 398 |
$row->{$_}->{link} = 'mailto:' . E($ref->{$_}) if $ref->{$_}; |
393 | 399 |
} |
394 | 400 |
|
401 |
for (qw(cp_phone1 cp_phone2 cp_mobile1)) { |
|
402 |
next unless my $number = SL::CTI->sanitize_number(number => $ref->{$_}); |
|
403 |
|
|
404 |
$row->{$_}->{link} = SL::CTI->call_link(number => $number); |
|
405 |
$row->{$_}->{link_class} = 'cti_call_action'; |
|
406 |
} |
|
407 |
|
|
395 | 408 |
$report->add_data($row); |
396 | 409 |
} |
397 | 410 |
|
config/kivitendo.conf.default | ||
---|---|---|
314 | 314 |
|
315 | 315 |
# If set to 1 each exception will include a full stack backtrace. |
316 | 316 |
backtrace_on_die = 0 |
317 |
|
|
318 |
[cti] |
|
319 |
# If you want phone numbers to be clickable then this must be set to a |
|
320 |
# command that does the actually dialing. Within this command three |
|
321 |
# variables are replaced before it is executed: |
|
322 |
# |
|
323 |
# 1. <%phone_extension%> and <%phone_password%> are taken from the user |
|
324 |
# configuration (changeable in the admin interface). |
|
325 |
# 2. <%number%> is the number to dial. It has already been sanitized |
|
326 |
# and formatted correctly regarding e.g. the international dialing |
|
327 |
# prefix. |
|
328 |
# |
|
329 |
# The following is an example that works with the OpenUC telephony |
|
330 |
# server: |
|
331 |
# dial_command = curl --insecure -X PUT https://<%phone_extension%>:<%phone_password%>@IP.AD.DR.ESS:8443/sipxconfig/rest/my/call/<%number%> |
|
332 |
dial_command = |
|
333 |
# If you need to dial something before the actual number then set |
|
334 |
# external_prefix to it. |
|
335 |
external_prefix = 0 |
|
336 |
# The prefix for international calls (numbers starting with +). |
|
337 |
international_dialing_prefix = 00 |
css/kivitendo/main.css | ||
---|---|---|
443 | 443 |
span.toggle_selected { |
444 | 444 |
font-weight: bold; |
445 | 445 |
} |
446 |
|
|
447 |
/* CTI */ |
|
448 |
a.cti_call_action { |
|
449 |
display: inline-block; |
|
450 |
padding-left: 18px; |
|
451 |
height: 16px; |
|
452 |
position: relative; |
|
453 |
top: 2px; |
|
454 |
vertical-align: center; |
|
455 |
background-image: url(../../image/icons/16x16/phone.png); |
|
456 |
background-repeat: no-repeat; |
|
457 |
} |
css/lx-office-erp/main.css | ||
---|---|---|
494 | 494 |
span.toggle_selected { |
495 | 495 |
font-weight: bold; |
496 | 496 |
} |
497 |
|
|
498 |
/* CTI */ |
|
499 |
a.cti_call_action { |
|
500 |
display: inline-block; |
|
501 |
padding-left: 18px; |
|
502 |
height: 16px; |
|
503 |
position: relative; |
|
504 |
top: 2px; |
|
505 |
vertical-align: center; |
|
506 |
background-image: url(../../image/icons/16x16/phone.png); |
|
507 |
background-repeat: no-repeat; |
|
508 |
} |
image/icons/16x16/Program--Internal Phone List.png | ||
---|---|---|
1 |
phone.png |
js/kivi.CustomerVendor.js | ||
---|---|---|
160 | 160 |
var url = "common.pl?INPUT_ENCODING=UTF-8&action=show_history&longdescription=&input_name="+ encodeURIComponent(id); |
161 | 161 |
window.open(url, "_new_generic", parm); |
162 | 162 |
}; |
163 |
|
|
164 |
this.update_dial_action = function($input) { |
|
165 |
var $action = $('#' + $input.prop('id') + '-dial-action'); |
|
166 |
|
|
167 |
if (!$action) |
|
168 |
return true; |
|
169 |
|
|
170 |
var number = $input.val().replace(/\s+/g, ''); |
|
171 |
if (number == '') |
|
172 |
$action.hide(); |
|
173 |
else |
|
174 |
$action.prop('href', 'controller.pl?action=CTI/call&number=' + encodeURIComponent(number)).show(); |
|
175 |
|
|
176 |
return true; |
|
177 |
}; |
|
178 |
|
|
179 |
this.init_dial_action = function(input) { |
|
180 |
if ($('#_cti_enabled').val() != 1) |
|
181 |
return false; |
|
182 |
|
|
183 |
var $input = $(input); |
|
184 |
var action_id = $input.prop('id') + '-dial-action'; |
|
185 |
|
|
186 |
if (!$('#' + action_id).size()) { |
|
187 |
var $action = $('<a href="" id="' + action_id + '" class="cti_call_action" target="_blank" tabindex="-1"></a>'); |
|
188 |
$input.wrap('<span nobr></span>').after($action); |
|
189 |
|
|
190 |
$input.change(function() { kivi.CustomerVendor.update_dial_action($input); }); |
|
191 |
} |
|
192 |
|
|
193 |
kivi.CustomerVendor.update_dial_action($input); |
|
194 |
|
|
195 |
return true; |
|
196 |
}; |
|
163 | 197 |
}); |
198 |
|
|
199 |
function local_reinit_widgets() { |
|
200 |
$('#cv_phone,#shipto_shiptophone,#contact_cp_phone1,#contact_cp_phone2,#contact_cp_mobile1,#contact_cp_mobile2').each(function(idx, elt) { |
|
201 |
kivi.CustomerVendor.init_dial_action($(elt)); |
|
202 |
}); |
|
203 |
} |
locale/de/all | ||
---|---|---|
408 | 408 |
'CSV import: parts and services' => 'CSV-Import: Waren und Dienstleistungen', |
409 | 409 |
'CSV import: projects' => 'CSV-Import: Projekte', |
410 | 410 |
'CSV import: shipping addresses' => 'CSV-Import: Lieferadressen', |
411 |
'CTI settings' => 'CTI-Einstellungen', |
|
411 | 412 |
'Calculate' => 'Berechnen', |
413 |
'Calling #1 now' => 'Wähle jetzt #1', |
|
412 | 414 |
'Can not create that quantity with current stock' => 'Diese Anzahl kann mit dem gegenwärtigen Lagerbestand nicht hergestellt werden.', |
413 | 415 |
'Cancel' => 'Abbrechen', |
414 | 416 |
'Cancel Accounts Payables Transaction' => 'Kreditorenbuchung stornieren', |
... | ... | |
803 | 805 |
'Destination warehouse and bin' => 'Ziellager und -lagerplatz', |
804 | 806 |
'Detail view' => 'Detailanzeige', |
805 | 807 |
'Details (one letter abbreviation)' => 'D', |
808 |
'Dial command missing in kivitendo configuration\'s [cti] section' => 'Wählbefehl fehlt im Abschnitt [cti] der kivitendo-Konfiguration', |
|
806 | 809 |
'Difference' => 'Differenz', |
807 | 810 |
'Dimensions' => 'Abmessungen', |
808 | 811 |
'Directory' => 'Verzeichnis', |
... | ... | |
1265 | 1268 |
'Interest' => 'Zinsen', |
1266 | 1269 |
'Interest Rate' => 'Zinssatz', |
1267 | 1270 |
'Internal Notes' => 'Interne Bemerkungen', |
1271 |
'Internal Phone List' => 'Interne Telefonliste', |
|
1268 | 1272 |
'Internal comment' => 'Interne Bemerkungen', |
1269 | 1273 |
'Internet' => 'Internet', |
1270 | 1274 |
'Introduction of clients' => 'Einführung von Mandanten', |
... | ... | |
1535 | 1539 |
'No file has been uploaded yet.' => 'Es wurde noch keine Datei hochgeladen.', |
1536 | 1540 |
'No function blocks have been created yet.' => 'Es wurden noch keine Funktionsblöcke angelegt.', |
1537 | 1541 |
'No groups have been created yet.' => 'Es wurden noch keine Gruppen angelegt.', |
1542 |
'No internal phone extensions have been configured yet.' => 'Es wurden noch keine internen Durchwahlen konfiguriert.', |
|
1538 | 1543 |
'No or an unknown authenticantion module specified in "config/kivitendo.conf".' => 'Es wurde kein oder ein unbekanntes Authentifizierungsmodul in "config/kivitendo.conf" angegeben.', |
1539 | 1544 |
'No part was found matching the search parameters.' => 'Es wurde kein Artikel gefunden, auf den die Suchparameter zutreffen.', |
1540 | 1545 |
'No payment term has been created yet.' => 'Es wurden noch keine Zahlungsbedingungen angelegt.', |
... | ... | |
1733 | 1738 |
'Person' => 'Person', |
1734 | 1739 |
'Personal settings' => 'Persönliche Einstellungen', |
1735 | 1740 |
'Phone' => 'Telefon', |
1741 |
'Phone extension' => 'Durchwahl', |
|
1742 |
'Phone extension missing in user configuration' => 'Durchwahl fehlt in der Benutzerkonfiguration', |
|
1743 |
'Phone password' => 'Telefonpasswort', |
|
1744 |
'Phone password missing in user configuration' => 'Telefonpasswort fehlt in der Benutzerkonfiguration', |
|
1736 | 1745 |
'Phone1' => 'Telefon 1 ', |
1737 | 1746 |
'Phone2' => 'Telefon 2', |
1738 | 1747 |
'Pick List' => 'Sammelliste', |
menus/erp.ini | ||
---|---|---|
787 | 787 |
module=am.pl |
788 | 788 |
action=config |
789 | 789 |
|
790 |
[Program--Internal Phone List] |
|
791 |
module=controller.pl |
|
792 |
action=CTI/list_internal_extensions |
|
793 |
|
|
790 | 794 |
[Program--Version] |
791 | 795 |
module=login.pl |
792 | 796 |
action=company_logo |
t/cti/call_link.t | ||
---|---|---|
1 |
use Test::More tests => 9; |
|
2 |
|
|
3 |
use strict; |
|
4 |
use lib 't'; |
|
5 |
use utf8; |
|
6 |
|
|
7 |
use_ok 'SL::CTI'; |
|
8 |
|
|
9 |
{ |
|
10 |
no warnings 'once'; |
|
11 |
$::lx_office_conf{cti}->{international_dialing_prefix} = '00'; |
|
12 |
} |
|
13 |
|
|
14 |
is SL::CTI->call_link(number => '0371 5347 620'), 'controller.pl?action=CTI/call&number=03715347620'; |
|
15 |
is SL::CTI->call_link(number => '0049(0)421-22232 22'), 'controller.pl?action=CTI/call&number=0049421-2223222'; |
|
16 |
is SL::CTI->call_link(number => '+49(0)421-22232 22'), 'controller.pl?action=CTI/call&number=0049421-2223222'; |
|
17 |
is SL::CTI->call_link(number => 'Tel: +49 40 809064 0'), 'controller.pl?action=CTI/call&number=0049408090640'; |
|
18 |
|
|
19 |
is SL::CTI->call_link(number => '0371 5347 620', internal => 1), 'controller.pl?action=CTI/call&number=03715347620&internal=1'; |
|
20 |
is SL::CTI->call_link(number => '0049(0)421-22232 22', internal => 1), 'controller.pl?action=CTI/call&number=0049421-2223222&internal=1'; |
|
21 |
is SL::CTI->call_link(number => '+49(0)421-22232 22', internal => 1), 'controller.pl?action=CTI/call&number=0049421-2223222&internal=1'; |
|
22 |
is SL::CTI->call_link(number => 'Tel: +49 40 809064 0', internal => 1), 'controller.pl?action=CTI/call&number=0049408090640&internal=1'; |
t/cti/sanitize_number.t | ||
---|---|---|
1 |
use Test::More tests => 5; |
|
2 |
|
|
3 |
use strict; |
|
4 |
use lib 't'; |
|
5 |
use utf8; |
|
6 |
|
|
7 |
use_ok 'SL::CTI'; |
|
8 |
|
|
9 |
{ |
|
10 |
no warnings 'once'; |
|
11 |
$::lx_office_conf{cti}->{international_dialing_prefix} = '00'; |
|
12 |
} |
|
13 |
|
|
14 |
is SL::CTI->sanitize_number(number => '0371 5347 620'), '03715347620'; |
|
15 |
is SL::CTI->sanitize_number(number => '0049(0)421-22232 22'), '0049421-2223222'; |
|
16 |
is SL::CTI->sanitize_number(number => '+49(0)421-22232 22'), '0049421-2223222'; |
|
17 |
is SL::CTI->sanitize_number(number => 'Tel: +49 40 809064 0'), '0049408090640'; |
templates/webpages/admin/edit_user.html | ||
---|---|---|
99 | 99 |
</tr> |
100 | 100 |
</table> |
101 | 101 |
|
102 |
<h2>[%- LxERP.t8("CTI settings") %]</h2> |
|
103 |
|
|
104 |
<table> |
|
105 |
<tr> |
|
106 |
<th align="right">[% LxERP.t8("Phone extension") %]</th> |
|
107 |
<td>[% L.input_tag("user.config_values.phone_extension", props.phone_extension) %]</td> |
|
108 |
</tr> |
|
109 |
|
|
110 |
<tr> |
|
111 |
<th align="right">[% LxERP.t8("Phone password") %]</th> |
|
112 |
<td>[% L.input_tag("user.config_values.phone_password", props.phone_password) %]</td> |
|
113 |
</tr> |
|
114 |
</table> |
|
115 |
|
|
102 | 116 |
<h2>[%- LxERP.t8("Access to clients") %]</h2> |
103 | 117 |
|
104 | 118 |
[% IF SELF.all_clients.size %] |
templates/webpages/cti/calling.html | ||
---|---|---|
1 |
<body> |
|
2 |
[% PROCESS 'common/flash.html' %] |
|
3 |
</body> |
|
4 |
</html> |
templates/webpages/cti/list_internal_extensions.html | ||
---|---|---|
1 |
[%- USE HTML %][%- USE LxERP -%] |
|
2 |
<body> |
|
3 |
|
|
4 |
<h1>[% HTML.escape(title) %]</h1> |
|
5 |
|
|
6 |
[% IF !SELF.internal_extensions.size %] |
|
7 |
<p>[% LxERP.t8("No internal phone extensions have been configured yet.") %]</p> |
|
8 |
|
|
9 |
[% ELSE %] |
|
10 |
<table> |
|
11 |
<tr class="listheading"> |
|
12 |
<th>[% LxERP.t8("Name") %]</th> |
|
13 |
<th>[% LxERP.t8("Phone extension") %]</th> |
|
14 |
</tr> |
|
15 |
|
|
16 |
[%- FOREACH extension = SELF.internal_extensions %] |
|
17 |
<tr class="listrow"> |
|
18 |
<td>[% HTML.escape(extension.name) %]</td> |
|
19 |
<td><a href="[% HTML.escape(extension.call_link) %]" class="cti_call_action">[% HTML.escape(extension.phone_extension) %]</a></td> |
|
20 |
</tr> |
|
21 |
[%- END %] |
|
22 |
</table> |
|
23 |
[% END %] |
|
24 |
</body> |
|
25 |
</html> |
templates/webpages/customer_vendor/form.html | ||
---|---|---|
2 | 2 |
[%- USE LxERP %] |
3 | 3 |
[%- USE L %] |
4 | 4 |
|
5 |
[% L.hidden_tag('_cti_enabled', !!LXCONFIG.cti.dial_command) %] |
|
6 |
|
|
5 | 7 |
[% cv_cvars = SELF.cv.cvars_by_config %] |
6 | 8 |
|
7 | 9 |
<form method="post" action="controller.pl"> |
templates/webpages/customer_vendor/tabs/contacts.html | ||
---|---|---|
18 | 18 |
empty_title = LxERP.t8('New contact'), |
19 | 19 |
value_key = 'cp_id', |
20 | 20 |
title_key = 'full_name', |
21 |
onchange = "kivi.CustomerVendor.selectContact({onFormSet: function(){contactsMapWidget.testInputs();}});",
|
|
21 |
onchange = "kivi.CustomerVendor.selectContact({onFormSet: function(){ contactsMapWidget.testInputs(); local_reinit_widgets(); }});",
|
|
22 | 22 |
) |
23 | 23 |
%] |
24 | 24 |
</td> |
templates/webpages/customer_vendor/tabs/shipto.html | ||
---|---|---|
16 | 16 |
title_key = 'displayable_id', |
17 | 17 |
with_empty = 1, |
18 | 18 |
empty_title = LxERP.t8('New shipto'), |
19 |
onchange = "kivi.CustomerVendor.selectShipto({onFormSet: function(){shiptoMapWidget.testInputs();}});",
|
|
19 |
onchange = "kivi.CustomerVendor.selectShipto({onFormSet: function(){ shiptoMapWidget.testInputs(); local_reinit_widgets(); }});",
|
|
20 | 20 |
) |
21 | 21 |
%] |
22 | 22 |
</td> |
Auch abrufbar als: Unified diff
Generische Unterstützung für CTI: Click-to-dial