use strict;
use parent qw(SL::Controller::Base);
+use Clone qw(clone);
use SL::DB::Part;
+use SL::Controller::Helper::GetModels;
+use SL::Controller::Helper::Filtered;
+use SL::Controller::Helper::Sorted;
+use SL::Controller::Helper::Paginated;
+use SL::Controller::Helper::Filtered;
+use SL::Locale::String qw(t8);
+
+use Rose::Object::MakeMethods::Generic (
+ 'scalar --get_set_init' => [ qw(parts) ],
+);
# safety
__PACKAGE__->run_before(sub { $::auth->assert('part_service_assembly_edit') });
+__PACKAGE__->make_filtered(
+ ONLY => [ qw(part_picker_search part_picker_result) ],
+ LAUNDER_TO => 'filter',
+);
+__PACKAGE__->make_paginated(
+ ONLY => [ qw(part_picker_search part_picker_result) ],
+);
+
+__PACKAGE__->make_sorted(
+ ONLY => [ qw(part_picker_search part_picker_result) ],
+
+ DEFAULT_BY => 'partnumber',
+ DEFAULT_DIR => 1,
+
+ partnumber => t8('Partnumber'),
+);
+
sub action_ajax_autocomplete {
my ($self, %params) = @_;
$self->render('part/ajax_autocomplete', { layout => 0, type => 'json' });
}
+sub action_test_page {
+ $::request->{layout}->add_javascripts('autocomplete_part.js');
+
+ $_[0]->render('part/test_page');
+}
+
+sub action_part_picker_search {
+ $_[0]->render('part/part_picker_search', { layout => 0 }, parts => $_[0]->parts);
+}
+
+sub action_part_picker_result {
+ $_[0]->render('part/_part_picker_result', { layout => 0 });
+}
+
+sub init_parts {
+ $_[0]->get_models;
+}
1;
return %qty_by_id;
}
+sub _sort_spec {
+ (
+ default => [ 'partnumber', 1 ],
+ columns => {
+ SIMPLE => 'ALL',
+ },
+ nulls => {},
+ );
+}
+
1;
__END__
$layout->use_javascript("$_.js") for (qw(
jquery jquery-ui jquery.cookie jqModal jquery.checkall jquery.download
- common part_selection switchmenuframe
+ common part_selection switchmenuframe autocomplete_part
), "jquery/ui/i18n/jquery.ui.datepicker-$::myconfig{countrycode}");
$self->{favicon} ||= "favicon.ico";
use SL::Presenter::EscapedText;
use SL::Presenter::Invoice;
use SL::Presenter::Order;
+use SL::Presenter::Part;
use SL::Presenter::Project;
use SL::Presenter::Record;
use SL::Presenter::SepaExport;
--- /dev/null
+package SL::Presenter::Part;
+
+use strict;
+
+use Exporter qw(import);
+our @EXPORT = qw(part_picker);
+
+sub part_picker {
+ my ($self, $name, $value, %params) = @_;
+ my $name_e = $self->escape($name);
+
+ my $ret =
+ $self->input_tag($name, (ref $value && $value->can('id') ? $value->id : ''), class => 'part_autocomplete', type => 'hidden') .
+ $self->input_tag("", delete $params{type}, id => $self->name_to_id("$name_e\_type"), type => 'hidden') .
+ $self->input_tag("", (ref $value && $value->can('description')) ? $value->description : '', id => $self->name_to_id("$name_e\_name"), %params) .
+ $self->input_tag("", delete $params{column}, id => $self->name_to_id("$name_e\_column"), type => 'hidden');
+
+ $self->html_tag('span', $ret, class => 'part_picker');
+}
+
+1;
return SL::Presenter->get->render('common/paginate', %template_params);
}
+sub part_picker {
+ my ($self, $name, $value, %params) = _hashify(3, @_);
+ my $name_e = _H($name);
+
+ my $ret = $self->hidden_tag($name, (ref $value && $value->can('id') ? $value->id : ''), class => 'part_autocomplete') .
+ $self->hidden_tag("", delete $params{type}, id => $self->name_to_id("$name_e\_type")) .
+ $self->input_tag("", (ref $value && $value->can('description')) ? $value->description : '', id => $self->name_to_id("$name_e\_name"), %params) .
+ $self->hidden_tag("", delete $params{column}, id => $self->name_to_id("$name_e\_column"));
+
+ $self->html_tag('span', $ret, class => 'part_picker');
+}
+
1;
__END__
.small-text {
font-size: 0.75em;
}
+
+.float-left {
+ float: left;
+}
+
+.block-context {
+ overflow: hidden;
+}
+
+.position-relative {
+ position: relative;
+}
+
+.position-absolute {
+ position: absolute;
+}
+
+div.part_picker_part {
+ float:left; width: 350px;
+ padding: 5px;
+ margin: 5px;
+ overflow:hidden;
+ border: 1px;
+ border-color: darkgray;
+ border-style: solid;
+ -webkit-border-radius: 4px;
+ -moz-border-radius: 4px;
+ border-radius: 4px;
+ background-color: whitesmoke;
+ cursor: pointer;
+}
+
+div.part_picker_part:hover {
+ background-color: #CCCCCC;
+ color: #FE5F14;
+ border-color: gray;
+}
.small-text {
font-size: 0.75em;
}
+
+.float-left {
+ float: left;
+}
+
+.block-context {
+ overflow: hidden;
+}
+
+.position-relative {
+ position: relative;
+}
+
+.position-absolute {
+ position: absolute;
+}
+
+div.part_picker_part {
+ float:left; width: 350px;
+ padding: 5px;
+ margin: 5px;
+ overflow:hidden;
+ border: 1px;
+ border-color: darkgray;
+ border-style: solid;
+ -webkit-border-radius: 2px;
+ -moz-border-radius: 2px;
+ border-radius: 2px;
+ background-color: whitesmoke;
+ cursor: pointer;
+}
+
+div.part_picker_part:hover {
+ background-color: lightgray;
+ border-color: gray;
+}
--- /dev/null
+$(function(){
+ $('input.part_autocomplete').each(function(i,real){
+ var $dummy = $('#' + real.id + '_name');
+ var $type = $('#' + real.id + '_type');
+ var $column = $('#' + real.id + '_column');
+ $dummy.autocomplete({
+ source: function(req, rsp) {
+ $.ajax({
+ url: 'controller.pl?action=Part/ajax_autocomplete',
+ dataType: "json",
+ data: {
+ term: req.term,
+ type: function() { return $type.val() },
+ column: function() { return $column.val()===undefined ? '' : $column.val() },
+ current: function() { return real.value },
+ obsolete: 0,
+ },
+ success: function (data){ rsp(data) }
+ });
+ },
+ limit: 20,
+ delay: 50,
+ select: function(event, ui) {
+ $(real).val(ui.item.id);
+ $dummy.val(ui.item.name);
+ },
+ });
+ /* In case users are impatient and want to skip ahead:
+ * Capture <enter> key events and check if it's a unique hit.
+ * If it is, go ahead and assume it was selected. If it wasn't don't do
+ * anything so that autocompletion kicks in. For <tab> don't prevent
+ * propagation. It would be nice to catch it, but javascript is too stupid
+ * to fire a tab event later on, so we'd have to reimplement the "find
+ * next active element in tabindex order and focus it".
+ */
+ $dummy.keypress(function(event){
+ if (event.keyCode == 13 || event.keyCode == 9) { // enter or tab or tab
+ // if string is empty asume they want to delete
+ if ($dummy.val() == '') {
+ $(real).val('');
+ return true;
+ }
+ $.ajax({
+ url: 'controller.pl?action=Part/ajax_autocomplete',
+ dataType: "json",
+ data: {
+ term: $dummy.val(),
+ type: function() { return $type.val() },
+ column: function() { return $column.val()===undefined ? '' : $column.val() },
+ current: function() { return real.value },
+ obsolete: 0,
+ },
+ success: function (data){
+ // only one
+ if (data.length == 1) {
+ $(real).val(data[0].id);
+ $dummy.val(data[0].description);
+ if (event.keyCode == 13)
+ $('#update_button').click();
+ }
+ }
+ });
+ if (event.keyCode == 13)
+ return false;
+ };
+ });
+
+ $dummy.blur(function(){
+ if ($dummy.val() == '')
+ $(real).val('');
+ })
+
+ // now add a picker div after the original input
+ var pcont = $('<span>').addClass('position-absolute');
+ var picker = $('<div>');
+ $dummy.after(pcont);
+ pcont.append(picker);
+ picker.addClass('icon16 CRM--Schnellsuche').click(function(){
+ open_jqm_window({
+ url: 'controller.pl',
+ data: {
+ action: 'Part/part_picker_search',
+ real_id: function() { return $(real).attr('id') },
+ 'filter.all:substr::ilike': function(){ return $dummy.val() },
+ 'filter.type': function(){ return $type.val() },
+ 'column': function(){ return $column.val() },
+ 'real_id': function() { return real.id },
+ },
+ id: 'part_selection',
+ });
+ return true;
+ });
+ });
+})
return true;
}
+function close_jqm_window(params) {
+ params = params || { };
+ var url = params.url;
+ var id = params.id ? params.id : 'jqm_popup_dialog';
+
+ $('#' + id).jqmClose()();
+}
+
$(document).ready(function () {
// initialize all jQuery UI tab elements:
$(".tabwidget").each(function(idx, element) { $(element).tabs(); });
'Part Notes' => 'Bemerkungen',
'Part Number' => 'Artikelnummer',
'Part Number missing!' => 'Artikelnummer fehlt!',
+ 'Part picker' => 'Artikelauswahl',
'Partnumber' => 'Artikelnummer',
'Partnumber must not be set to empty!' => 'Die Artikelnummer darf nicht auf leer geändert werden.',
'Partnumber not unique!' => 'Artikelnummer bereits vorhanden!',
--- /dev/null
+[%- USE T8 %]
+[%- USE HTML %]
+[%- USE L %]
+[%- USE LxERP %]
+
+[%# L.dump(SELF.parts) %]
+
+[% FOREACH part = SELF.parts %]
+ [% PROCESS part_block %]
+[% END %]
+
+[%- BLOCK part_block %]
+<div class='part_picker_part'>
+ <input type='hidden' class='part_picker_id' value='[% part.id %]'>
+ <input type='hidden' class='part_picker_partnumber' value='[% part.partnumber %]'>
+ <input type='hidden' class='part_picker_description' value='[% part.description %]'>
+ <span style='float:left'>[% part.partnumber | html %]</span>
+ <span style='float:right; font-weight:bold'>[% part.description | html %]</span>
+ <div style='clear:both;'></div>
+ [% 'Sellprice' | $T8 %]: [% part.sellprice_as_number | html %]
+</div>
+[%- END %]
+
+<div style='clear:both'></div>
+
+[% L.paginate_controls(target='#part_picker_result', selector='#part_picker_result') %]
+
+<script type='text/javascript'>
+ $('div.part_picker_part').each(function(){
+ $(this).click(function(){
+ var real_id = $('#part_picker_real_id').val();
+ var $dummy = $('#' + real_id + '_name');
+ var $real = $('#' + real_id);
+
+ $dummy.val($(this).children('input.part_picker_description').val());
+ $real.val($(this).children('input.part_picker_id').val());
+
+ $('#part_selection').jqmClose();
+
+ return true;
+ });
+ });
+</script>
--- /dev/null
+[%- USE HTML %]
+[%- USE L %]
+[%- USE LxERP %]
+[%- USE T8 %]
+
+<h1>[% 'Part picker' | $T8 %]</h1>
+<div style='overflow:hidden'>
+
+[% L.input_tag('part_picker_filter', SELF.filter.all_substr__ilike, class='part_picker_filter') %]
+[% L.hidden_tag('part_picker_real_id', FORM.real_id) %]
+
+<div style='clear:both'></div>
+<div id='part_picker_result'></div>
+</div>
+
+<script type='text/javascript'>
+ var timer;
+ var update_results = function(){
+ var $type = $('#[% FORM.real_id %]_type');
+ var $column = $('#[% FORM.real_id %]_column');
+ $.ajax({
+ url: 'controller.pl?action=Part/part_picker_result',
+ data: {
+ 'filter.all:substr::ilike': function(){ var val = $('#part_picker_filter').val(); return val === undefined ? '' : val },
+ 'filter.type': function(){ return $type.val() },
+ 'column': function(){ return $column.val() },
+ 'real_id': [% FORM.real_id.json %],
+ },
+ success: function(data){ $('#part_picker_result').html(data) }
+ });
+ };
+ $(function(){
+ $('#part_picker_filter').focus();
+ update_results();
+ });
+ $('#part_picker_filter').keypress(function (event){
+ window.clearTimeout(timer);
+ timer = window.setTimeout(update_results, 100);
+ });
+</script>
--- /dev/null
+[% USE L %]
+
+<h1>Waren Picker Testpage</h1>
+
+<br>
+Alle: <br>
+[% L.part_picker('part_id') %] <br>
+Nur Waren: <br>
+[% L.part_picker('part_id2', undef, type='part') %]<br>
+Nur Dienstleistungen: <br>
+[% L.part_picker('part_id3', undef, type='service') %]<br>