Revision 2504ebe1
Von Sven Schöling vor mehr als 11 Jahren hinzugefügt
SL/Controller/Part.pm | ||
---|---|---|
3 | 3 |
use strict; |
4 | 4 |
use parent qw(SL::Controller::Base); |
5 | 5 |
|
6 |
use Clone qw(clone); |
|
6 | 7 |
use SL::DB::Part; |
8 |
use SL::Controller::Helper::GetModels; |
|
9 |
use SL::Controller::Helper::Filtered; |
|
10 |
use SL::Controller::Helper::Sorted; |
|
11 |
use SL::Controller::Helper::Paginated; |
|
12 |
use SL::Controller::Helper::Filtered; |
|
13 |
use SL::Locale::String qw(t8); |
|
14 |
|
|
15 |
use Rose::Object::MakeMethods::Generic ( |
|
16 |
'scalar --get_set_init' => [ qw(parts) ], |
|
17 |
); |
|
7 | 18 |
|
8 | 19 |
# safety |
9 | 20 |
__PACKAGE__->run_before(sub { $::auth->assert('part_service_assembly_edit') }); |
10 | 21 |
|
22 |
__PACKAGE__->make_filtered( |
|
23 |
ONLY => [ qw(part_picker_search part_picker_result) ], |
|
24 |
LAUNDER_TO => 'filter', |
|
25 |
); |
|
26 |
__PACKAGE__->make_paginated( |
|
27 |
ONLY => [ qw(part_picker_search part_picker_result) ], |
|
28 |
); |
|
29 |
|
|
30 |
__PACKAGE__->make_sorted( |
|
31 |
ONLY => [ qw(part_picker_search part_picker_result) ], |
|
32 |
|
|
33 |
DEFAULT_BY => 'partnumber', |
|
34 |
DEFAULT_DIR => 1, |
|
35 |
|
|
36 |
partnumber => t8('Partnumber'), |
|
37 |
); |
|
38 |
|
|
11 | 39 |
sub action_ajax_autocomplete { |
12 | 40 |
my ($self, %params) = @_; |
13 | 41 |
|
... | ... | |
26 | 54 |
$self->render('part/ajax_autocomplete', { layout => 0, type => 'json' }); |
27 | 55 |
} |
28 | 56 |
|
57 |
sub action_test_page { |
|
58 |
$::request->{layout}->add_javascripts('autocomplete_part.js'); |
|
59 |
|
|
60 |
$_[0]->render('part/test_page'); |
|
61 |
} |
|
62 |
|
|
63 |
sub action_part_picker_search { |
|
64 |
$_[0]->render('part/part_picker_search', { layout => 0 }, parts => $_[0]->parts); |
|
65 |
} |
|
66 |
|
|
67 |
sub action_part_picker_result { |
|
68 |
$_[0]->render('part/_part_picker_result', { layout => 0 }); |
|
69 |
} |
|
70 |
|
|
71 |
sub init_parts { |
|
72 |
$_[0]->get_models; |
|
73 |
} |
|
29 | 74 |
|
30 | 75 |
1; |
SL/DB/Manager/Part.pm | ||
---|---|---|
85 | 85 |
return %qty_by_id; |
86 | 86 |
} |
87 | 87 |
|
88 |
sub _sort_spec { |
|
89 |
( |
|
90 |
default => [ 'partnumber', 1 ], |
|
91 |
columns => { |
|
92 |
SIMPLE => 'ALL', |
|
93 |
}, |
|
94 |
nulls => {}, |
|
95 |
); |
|
96 |
} |
|
97 |
|
|
88 | 98 |
1; |
89 | 99 |
__END__ |
90 | 100 |
|
SL/Form.pm | ||
---|---|---|
468 | 468 |
|
469 | 469 |
$layout->use_javascript("$_.js") for (qw( |
470 | 470 |
jquery jquery-ui jquery.cookie jqModal jquery.checkall jquery.download |
471 |
common part_selection switchmenuframe |
|
471 |
common part_selection switchmenuframe autocomplete_part
|
|
472 | 472 |
), "jquery/ui/i18n/jquery.ui.datepicker-$::myconfig{countrycode}"); |
473 | 473 |
|
474 | 474 |
$self->{favicon} ||= "favicon.ico"; |
SL/Presenter.pm | ||
---|---|---|
12 | 12 |
use SL::Presenter::EscapedText; |
13 | 13 |
use SL::Presenter::Invoice; |
14 | 14 |
use SL::Presenter::Order; |
15 |
use SL::Presenter::Part; |
|
15 | 16 |
use SL::Presenter::Project; |
16 | 17 |
use SL::Presenter::Record; |
17 | 18 |
use SL::Presenter::SepaExport; |
SL/Presenter/Part.pm | ||
---|---|---|
1 |
package SL::Presenter::Part; |
|
2 |
|
|
3 |
use strict; |
|
4 |
|
|
5 |
use Exporter qw(import); |
|
6 |
our @EXPORT = qw(part_picker); |
|
7 |
|
|
8 |
sub part_picker { |
|
9 |
my ($self, $name, $value, %params) = @_; |
|
10 |
my $name_e = $self->escape($name); |
|
11 |
|
|
12 |
my $ret = |
|
13 |
$self->input_tag($name, (ref $value && $value->can('id') ? $value->id : ''), class => 'part_autocomplete', type => 'hidden') . |
|
14 |
$self->input_tag("", delete $params{type}, id => $self->name_to_id("$name_e\_type"), type => 'hidden') . |
|
15 |
$self->input_tag("", (ref $value && $value->can('description')) ? $value->description : '', id => $self->name_to_id("$name_e\_name"), %params) . |
|
16 |
$self->input_tag("", delete $params{column}, id => $self->name_to_id("$name_e\_column"), type => 'hidden'); |
|
17 |
|
|
18 |
$self->html_tag('span', $ret, class => 'part_picker'); |
|
19 |
} |
|
20 |
|
|
21 |
1; |
SL/Template/Plugin/L.pm | ||
---|---|---|
491 | 491 |
return SL::Presenter->get->render('common/paginate', %template_params); |
492 | 492 |
} |
493 | 493 |
|
494 |
sub part_picker { |
|
495 |
my ($self, $name, $value, %params) = _hashify(3, @_); |
|
496 |
my $name_e = _H($name); |
|
497 |
|
|
498 |
my $ret = $self->hidden_tag($name, (ref $value && $value->can('id') ? $value->id : ''), class => 'part_autocomplete') . |
|
499 |
$self->hidden_tag("", delete $params{type}, id => $self->name_to_id("$name_e\_type")) . |
|
500 |
$self->input_tag("", (ref $value && $value->can('description')) ? $value->description : '', id => $self->name_to_id("$name_e\_name"), %params) . |
|
501 |
$self->hidden_tag("", delete $params{column}, id => $self->name_to_id("$name_e\_column")); |
|
502 |
|
|
503 |
$self->html_tag('span', $ret, class => 'part_picker'); |
|
504 |
} |
|
505 |
|
|
494 | 506 |
1; |
495 | 507 |
|
496 | 508 |
__END__ |
css/kivitendo/main.css | ||
---|---|---|
380 | 380 |
.small-text { |
381 | 381 |
font-size: 0.75em; |
382 | 382 |
} |
383 |
|
|
384 |
.float-left { |
|
385 |
float: left; |
|
386 |
} |
|
387 |
|
|
388 |
.block-context { |
|
389 |
overflow: hidden; |
|
390 |
} |
|
391 |
|
|
392 |
.position-relative { |
|
393 |
position: relative; |
|
394 |
} |
|
395 |
|
|
396 |
.position-absolute { |
|
397 |
position: absolute; |
|
398 |
} |
|
399 |
|
|
400 |
div.part_picker_part { |
|
401 |
float:left; width: 350px; |
|
402 |
padding: 5px; |
|
403 |
margin: 5px; |
|
404 |
overflow:hidden; |
|
405 |
border: 1px; |
|
406 |
border-color: darkgray; |
|
407 |
border-style: solid; |
|
408 |
-webkit-border-radius: 4px; |
|
409 |
-moz-border-radius: 4px; |
|
410 |
border-radius: 4px; |
|
411 |
background-color: whitesmoke; |
|
412 |
cursor: pointer; |
|
413 |
} |
|
414 |
|
|
415 |
div.part_picker_part:hover { |
|
416 |
background-color: #CCCCCC; |
|
417 |
color: #FE5F14; |
|
418 |
border-color: gray; |
|
419 |
} |
css/lx-office-erp/main.css | ||
---|---|---|
432 | 432 |
.small-text { |
433 | 433 |
font-size: 0.75em; |
434 | 434 |
} |
435 |
|
|
436 |
.float-left { |
|
437 |
float: left; |
|
438 |
} |
|
439 |
|
|
440 |
.block-context { |
|
441 |
overflow: hidden; |
|
442 |
} |
|
443 |
|
|
444 |
.position-relative { |
|
445 |
position: relative; |
|
446 |
} |
|
447 |
|
|
448 |
.position-absolute { |
|
449 |
position: absolute; |
|
450 |
} |
|
451 |
|
|
452 |
div.part_picker_part { |
|
453 |
float:left; width: 350px; |
|
454 |
padding: 5px; |
|
455 |
margin: 5px; |
|
456 |
overflow:hidden; |
|
457 |
border: 1px; |
|
458 |
border-color: darkgray; |
|
459 |
border-style: solid; |
|
460 |
-webkit-border-radius: 2px; |
|
461 |
-moz-border-radius: 2px; |
|
462 |
border-radius: 2px; |
|
463 |
background-color: whitesmoke; |
|
464 |
cursor: pointer; |
|
465 |
} |
|
466 |
|
|
467 |
div.part_picker_part:hover { |
|
468 |
background-color: lightgray; |
|
469 |
border-color: gray; |
|
470 |
} |
js/autocomplete_part.js | ||
---|---|---|
1 |
$(function(){ |
|
2 |
$('input.part_autocomplete').each(function(i,real){ |
|
3 |
var $dummy = $('#' + real.id + '_name'); |
|
4 |
var $type = $('#' + real.id + '_type'); |
|
5 |
var $column = $('#' + real.id + '_column'); |
|
6 |
$dummy.autocomplete({ |
|
7 |
source: function(req, rsp) { |
|
8 |
$.ajax({ |
|
9 |
url: 'controller.pl?action=Part/ajax_autocomplete', |
|
10 |
dataType: "json", |
|
11 |
data: { |
|
12 |
term: req.term, |
|
13 |
type: function() { return $type.val() }, |
|
14 |
column: function() { return $column.val()===undefined ? '' : $column.val() }, |
|
15 |
current: function() { return real.value }, |
|
16 |
obsolete: 0, |
|
17 |
}, |
|
18 |
success: function (data){ rsp(data) } |
|
19 |
}); |
|
20 |
}, |
|
21 |
limit: 20, |
|
22 |
delay: 50, |
|
23 |
select: function(event, ui) { |
|
24 |
$(real).val(ui.item.id); |
|
25 |
$dummy.val(ui.item.name); |
|
26 |
}, |
|
27 |
}); |
|
28 |
/* In case users are impatient and want to skip ahead: |
|
29 |
* Capture <enter> key events and check if it's a unique hit. |
|
30 |
* If it is, go ahead and assume it was selected. If it wasn't don't do |
|
31 |
* anything so that autocompletion kicks in. For <tab> don't prevent |
|
32 |
* propagation. It would be nice to catch it, but javascript is too stupid |
|
33 |
* to fire a tab event later on, so we'd have to reimplement the "find |
|
34 |
* next active element in tabindex order and focus it". |
|
35 |
*/ |
|
36 |
$dummy.keypress(function(event){ |
|
37 |
if (event.keyCode == 13 || event.keyCode == 9) { // enter or tab or tab |
|
38 |
// if string is empty asume they want to delete |
|
39 |
if ($dummy.val() == '') { |
|
40 |
$(real).val(''); |
|
41 |
return true; |
|
42 |
} |
|
43 |
$.ajax({ |
|
44 |
url: 'controller.pl?action=Part/ajax_autocomplete', |
|
45 |
dataType: "json", |
|
46 |
data: { |
|
47 |
term: $dummy.val(), |
|
48 |
type: function() { return $type.val() }, |
|
49 |
column: function() { return $column.val()===undefined ? '' : $column.val() }, |
|
50 |
current: function() { return real.value }, |
|
51 |
obsolete: 0, |
|
52 |
}, |
|
53 |
success: function (data){ |
|
54 |
// only one |
|
55 |
if (data.length == 1) { |
|
56 |
$(real).val(data[0].id); |
|
57 |
$dummy.val(data[0].description); |
|
58 |
if (event.keyCode == 13) |
|
59 |
$('#update_button').click(); |
|
60 |
} |
|
61 |
} |
|
62 |
}); |
|
63 |
if (event.keyCode == 13) |
|
64 |
return false; |
|
65 |
}; |
|
66 |
}); |
|
67 |
|
|
68 |
$dummy.blur(function(){ |
|
69 |
if ($dummy.val() == '') |
|
70 |
$(real).val(''); |
|
71 |
}) |
|
72 |
|
|
73 |
// now add a picker div after the original input |
|
74 |
var pcont = $('<span>').addClass('position-absolute'); |
|
75 |
var picker = $('<div>'); |
|
76 |
$dummy.after(pcont); |
|
77 |
pcont.append(picker); |
|
78 |
picker.addClass('icon16 CRM--Schnellsuche').click(function(){ |
|
79 |
open_jqm_window({ |
|
80 |
url: 'controller.pl', |
|
81 |
data: { |
|
82 |
action: 'Part/part_picker_search', |
|
83 |
real_id: function() { return $(real).attr('id') }, |
|
84 |
'filter.all:substr::ilike': function(){ return $dummy.val() }, |
|
85 |
'filter.type': function(){ return $type.val() }, |
|
86 |
'column': function(){ return $column.val() }, |
|
87 |
'real_id': function() { return real.id }, |
|
88 |
}, |
|
89 |
id: 'part_selection', |
|
90 |
}); |
|
91 |
return true; |
|
92 |
}); |
|
93 |
}); |
|
94 |
}) |
js/common.js | ||
---|---|---|
185 | 185 |
return true; |
186 | 186 |
} |
187 | 187 |
|
188 |
function close_jqm_window(params) { |
|
189 |
params = params || { }; |
|
190 |
var url = params.url; |
|
191 |
var id = params.id ? params.id : 'jqm_popup_dialog'; |
|
192 |
|
|
193 |
$('#' + id).jqmClose()(); |
|
194 |
} |
|
195 |
|
|
188 | 196 |
$(document).ready(function () { |
189 | 197 |
// initialize all jQuery UI tab elements: |
190 | 198 |
$(".tabwidget").each(function(idx, element) { $(element).tabs(); }); |
locale/de/all | ||
---|---|---|
1422 | 1422 |
'Part Notes' => 'Bemerkungen', |
1423 | 1423 |
'Part Number' => 'Artikelnummer', |
1424 | 1424 |
'Part Number missing!' => 'Artikelnummer fehlt!', |
1425 |
'Part picker' => 'Artikelauswahl', |
|
1425 | 1426 |
'Partnumber' => 'Artikelnummer', |
1426 | 1427 |
'Partnumber must not be set to empty!' => 'Die Artikelnummer darf nicht auf leer geändert werden.', |
1427 | 1428 |
'Partnumber not unique!' => 'Artikelnummer bereits vorhanden!', |
templates/webpages/part/_part_picker_result.html | ||
---|---|---|
1 |
[%- USE T8 %] |
|
2 |
[%- USE HTML %] |
|
3 |
[%- USE L %] |
|
4 |
[%- USE LxERP %] |
|
5 |
|
|
6 |
[%# L.dump(SELF.parts) %] |
|
7 |
|
|
8 |
[% FOREACH part = SELF.parts %] |
|
9 |
[% PROCESS part_block %] |
|
10 |
[% END %] |
|
11 |
|
|
12 |
[%- BLOCK part_block %] |
|
13 |
<div class='part_picker_part'> |
|
14 |
<input type='hidden' class='part_picker_id' value='[% part.id %]'> |
|
15 |
<input type='hidden' class='part_picker_partnumber' value='[% part.partnumber %]'> |
|
16 |
<input type='hidden' class='part_picker_description' value='[% part.description %]'> |
|
17 |
<span style='float:left'>[% part.partnumber | html %]</span> |
|
18 |
<span style='float:right; font-weight:bold'>[% part.description | html %]</span> |
|
19 |
<div style='clear:both;'></div> |
|
20 |
[% 'Sellprice' | $T8 %]: [% part.sellprice_as_number | html %] |
|
21 |
</div> |
|
22 |
[%- END %] |
|
23 |
|
|
24 |
<div style='clear:both'></div> |
|
25 |
|
|
26 |
[% L.paginate_controls(target='#part_picker_result', selector='#part_picker_result') %] |
|
27 |
|
|
28 |
<script type='text/javascript'> |
|
29 |
$('div.part_picker_part').each(function(){ |
|
30 |
$(this).click(function(){ |
|
31 |
var real_id = $('#part_picker_real_id').val(); |
|
32 |
var $dummy = $('#' + real_id + '_name'); |
|
33 |
var $real = $('#' + real_id); |
|
34 |
|
|
35 |
$dummy.val($(this).children('input.part_picker_description').val()); |
|
36 |
$real.val($(this).children('input.part_picker_id').val()); |
|
37 |
|
|
38 |
$('#part_selection').jqmClose(); |
|
39 |
|
|
40 |
return true; |
|
41 |
}); |
|
42 |
}); |
|
43 |
</script> |
templates/webpages/part/part_picker_search.html | ||
---|---|---|
1 |
[%- USE HTML %] |
|
2 |
[%- USE L %] |
|
3 |
[%- USE LxERP %] |
|
4 |
[%- USE T8 %] |
|
5 |
|
|
6 |
<h1>[% 'Part picker' | $T8 %]</h1> |
|
7 |
<div style='overflow:hidden'> |
|
8 |
|
|
9 |
[% L.input_tag('part_picker_filter', SELF.filter.all_substr__ilike, class='part_picker_filter') %] |
|
10 |
[% L.hidden_tag('part_picker_real_id', FORM.real_id) %] |
|
11 |
|
|
12 |
<div style='clear:both'></div> |
|
13 |
<div id='part_picker_result'></div> |
|
14 |
</div> |
|
15 |
|
|
16 |
<script type='text/javascript'> |
|
17 |
var timer; |
|
18 |
var update_results = function(){ |
|
19 |
var $type = $('#[% FORM.real_id %]_type'); |
|
20 |
var $column = $('#[% FORM.real_id %]_column'); |
|
21 |
$.ajax({ |
|
22 |
url: 'controller.pl?action=Part/part_picker_result', |
|
23 |
data: { |
|
24 |
'filter.all:substr::ilike': function(){ var val = $('#part_picker_filter').val(); return val === undefined ? '' : val }, |
|
25 |
'filter.type': function(){ return $type.val() }, |
|
26 |
'column': function(){ return $column.val() }, |
|
27 |
'real_id': [% FORM.real_id.json %], |
|
28 |
}, |
|
29 |
success: function(data){ $('#part_picker_result').html(data) } |
|
30 |
}); |
|
31 |
}; |
|
32 |
$(function(){ |
|
33 |
$('#part_picker_filter').focus(); |
|
34 |
update_results(); |
|
35 |
}); |
|
36 |
$('#part_picker_filter').keypress(function (event){ |
|
37 |
window.clearTimeout(timer); |
|
38 |
timer = window.setTimeout(update_results, 100); |
|
39 |
}); |
|
40 |
</script> |
templates/webpages/part/test_page.html | ||
---|---|---|
1 |
[% USE L %] |
|
2 |
|
|
3 |
<h1>Waren Picker Testpage</h1> |
|
4 |
|
|
5 |
<br> |
|
6 |
Alle: <br> |
|
7 |
[% L.part_picker('part_id') %] <br> |
|
8 |
Nur Waren: <br> |
|
9 |
[% L.part_picker('part_id2', undef, type='part') %]<br> |
|
10 |
Nur Dienstleistungen: <br> |
|
11 |
[% L.part_picker('part_id3', undef, type='service') %]<br> |
Auch abrufbar als: Unified diff
Part Picker