use SL::JSON;
use Rose::Object::MakeMethods::Generic (
- 'scalar --get_set_init' => [ qw(charts models chart) ],
+ 'scalar --get_set_init' => [ qw(charts models chart filter) ],
);
sub action_ajax_autocomplete {
);
}
+sub init_filter { $_[0]->models->filtered->laundered }
+
1;
use Rose::Object::MakeMethods::Generic (
scalar => [ qw(controller model query with_objects filtered sorted paginated finalized final_params) ],
- 'scalar --get_set_init' => [ qw(handlers source additional_url_params) ],
+ 'scalar --get_set_init' => [ qw(handlers source list_action additional_url_params) ],
array => [ qw(plugins) ],
);
my @plugins;
for my $plugin (qw(filtered sorted paginated)) {
- next unless my $spec = delete $params{$plugin} // {};
+ next if exists($params{$plugin}) && !$params{$plugin};
+
+ my $spec = delete $params{$plugin} // {};
my $plugin_class = "SL::Controller::Helper::GetModels::" . ucfirst $plugin;
push @plugins, $self->$plugin($plugin_class->new(%$spec, get_models => $self));
}
sub get_callback_params {
my ($self, %override_params) = @_;
- my %default_params = $self->_run_handlers('callback', action => $self->controller->action_name);
+ my %default_params = $self->_run_handlers('callback', action => $self->list_action);
}
sub get_callback {
$::form
}
+sub init_list_action {
+ $_[0]->controller->action_name
+}
+
sub init_additional_url_params { +{} }
1;
The name of the model for this GetModels instance. If none is given, the model
is inferred from the name of the controller class.
+=item list_action ACTION
+
+If callbacks are generated, use this action instead of the current action.
+Usually you can omit this. In case the reporting is done without redirecting
+from a mutating action, this is necessary to have callbacks for paginating and
+sorting point to the correct action.
+
=item sorted PARAMS
=item paginated PARAMS
use List::MoreUtils qw(uniq);
use Rose::Object::MakeMethods::Generic (
- scalar => [ qw(filter_args filter_params orig_filter filter) ],
- 'scalar --get_set_init' => [ qw(form_params launder_to) ],
+ scalar => [ qw(filter_args filter_params orig_filter filter no_launder) ],
+ 'scalar --get_set_init' => [ qw(form_params laundered) ],
);
sub init {
class => $self->get_models->manager,
with_objects => $params{with_objects},
);
- my $laundered;
- if ($self->launder_to eq '__INPLACE__') {
- # nothing to do
- } elsif ($self->launder_to) {
- $laundered = {};
- $parse_filter_args{launder_to} = $laundered;
+
+ # Store laundered result in $self->laundered.
+
+ if (!$self->no_launder) {
+ $self->laundered({});
+ $parse_filter_args{launder_to} = $self->laundered;
} else {
+ $self->laundered(undef);
$parse_filter_args{no_launder} = 1;
}
my %calculated_params = SL::Controller::Helper::ParseFilter::parse_filter($filter, %parse_filter_args);
%calculated_params = $self->merge_args(\%calculated_params, \%filter_args, \%params);
- if ($laundered) {
- if ($self->get_models->controller->can($self->launder_to)) {
- $self->get_models->controller->${\ $self->launder_to }($laundered);
- } else {
- $self->get_models->controller->{$self->launder_to} = $laundered;
- }
- }
-
# $::lxdebug->dump(0, "get_current_filter_params: ", \%calculated_params);
$self->filter_params(\%calculated_params);
'filter'
}
-sub init_launder_to {
- 'filter'
-}
+sub init_laundered {
+ my ($self) = @_;
+ $self->get_models->finalize;
+ return $self->{laundered};
+}
1;
...
filtered => {
filter => HASHREF,
- launder_to => HASHREF | SUBNAME | '__INPLACE__',
+ no_launder => 0 | 1,
}
OR
Defaults to the value C<filter> if missing.
-=item * C<launder_to>
-
-Optional. Indicates a target for laundered filter arguments in the controller.
-Can be set to C<undef> to disable laundering, and can be set to method named or
-hash keys of the controller. In the latter case the laundered structure will be
-put there.
+=item * C<no_launder>
-Defaults to the controller. Laundered values will end up in C<SELF.filter> for
-template purposes.
-
-Setting this to the special value C<__INPLACE__> will cause inplace laundering.
+Optional. If given and trueish then laundering is disabled.
=back
C<Filtered> will honor custom filters defined in RDBO managers. See
L<SL::DB::Helper::Filtered> for an explanation fo those.
+=head1 FUNCTIONS
+
+=over 4
+
+=item C<laundered>
+
+Finalizes the object (which causes laundering of the filter structure)
+and returns a hashref of the laundered filter. If the plugin is
+configured not to launder then C<undef> will be returned.
+
+=back
+
=head1 BUGS
=over 4
use Data::Dumper;
use Text::ParseWords;
+sub _lazy_bool_eq {
+ my ($key, $value) = @_;
+
+ return () if ($value // '') eq '';
+ return (or => [ $key => undef, $key => 0 ]) if !$value;
+ return ($key => 1);
+}
+
my %filters = (
date => sub { DateTime->from_lxoffice($_[0]) },
number => sub { $::form->parse_amount(\%::myconfig, $_[0]) },
} qw(similar match imatch regex regexp like ilike rlike is is_not ne eq lt gt le ge),
);
+my %complex_methods = (
+ lazy_bool_eq => \&_lazy_bool_eq,
+);
+
sub parse_filter {
my ($filter, %params) = @_;
$flattened = _collapse_indirect_filters($flattened);
+ my $all_filters = { %filters, %{ $params{filters} || {} } };
+ my $all_methods = { %methods, %{ $params{methods} || {} } };
+ my $all_complex = { %complex_methods, %{ $params{complex_methods} || {} } };
+
my @result;
for (my $i = 0; $i < scalar @$flattened; $i += 2) {
+ my (@args, @filters, $method);
+
my ($key, $value) = ($flattened->[$i], $flattened->[$i+1]);
my ($type, $op) = $key =~ m{:(.+)::(.+)};
- if ($key =~ s/:multi//) {
- my @multi;
- my $orig_key = $key;
- for my $value (parse_line('\s+', 0, $value)) {
- ($key, $value) = _apply_all($key, $value, qr/\b:(\w+)/, { %filters, %{ $params{filters} || {} } });
- ($key, $value) = _apply_all($key, $value, qr/\b::(\w+)/, { %methods, %{ $params{methods} || {} } });
- ($key, $value) = _dispatch_custom_filters($params{class}, $with_objects, $key, $value) if $params{class};
- ($key, $value) = _apply_value_filters($key, $value, $type, $op);
- push @multi, $key, $value;
- $key = $orig_key;
- }
- ($key, $value) = (and => \@multi);
- } else {
- ($key, $value) = _apply_all($key, $value, qr/\b:(\w+)/, { %filters, %{ $params{filters} || {} } });
- ($key, $value) = _apply_all($key, $value, qr/\b::(\w+)/, { %methods, %{ $params{methods} || {} } });
- ($key, $value) = _dispatch_custom_filters($params{class}, $with_objects, $key, $value) if $params{class};
- ($key, $value) = _apply_value_filters($key, $value, $type, $op);
+ my $is_multi = $key =~ s/:multi//;
+ my @value_tokens = $is_multi ? parse_line('\s+', 0, $value) : ($value);
+
+ ($key, $method) = split m{::}, $key, 2;
+ ($key, @filters) = split m{:}, $key;
+
+ my $orig_key = $key;
+
+ for my $value_token (@value_tokens) {
+ $key = $orig_key;
+
+ $value_token = _apply($value_token, $_, $all_filters) for @filters;
+ $value_token = _apply($value_token, $method, $all_methods) if $method && exists $all_methods->{$method};
+ ($key, $value_token) = _apply_complex($key, $value_token, $method, $all_complex) if $method && exists $all_complex->{$method};
+ ($key, $value_token) = _dispatch_custom_filters($params{class}, $with_objects, $key, $value_token) if $params{class};
+ ($key, $value_token) = _apply_value_filters($key, $value_token, $type, $op);
+
+ push @args, $key, $value_token;
}
- push @result, $key, $value if defined $key;
+ next unless defined $key;
+
+ push @result, $is_multi ? (and => [ @args ]) : @args;
}
return \@result;
}
return $filters->{$name}->($value);
}
-sub _apply_all {
- my ($key, $value, $re, $subs) = @_;
-
- while ($key =~ s/$re//) {
- $value = _apply($value, $1, $subs);
- }
-
- return $key, $value;
+sub _apply_complex {
+ my ($key, $value, $name, $filters) = @_;
+ return $key, $value unless $name && $filters->{$name};
+ return $filters->{$name}->($key, $value);
}
1;
All these are recognized like the L<Rose::DB::Object> methods.
+=item lazu_bool_eq
+
+If the value is undefined or an empty string then this parameter will
+be completely removed from the query. Otherwise a falsish filter value
+will match for C<NULL> and C<FALSE>; trueish values will only match
+C<TRUE>.
+
=back
=head1 BUGS AND CAVEATS
use strict;
use parent qw(Exporter);
-our @EXPORT = qw(attr_duration);
+our @EXPORT = qw(attr_duration attr_duration_minutes);
use Carp;
_make($package, $_) for @attributes;
}
+sub attr_duration_minutes {
+ my ($package, @attributes) = @_;
+
+ _make_minutes($package, $_) for @attributes;
+}
+
sub _make {
my ($package, $attribute) = @_;
};
}
+sub _make_minutes {
+ my ($package, $attribute) = @_;
+
+ no strict 'refs';
+
+ *{ $package . '::' . $attribute . '_as_hours' } = sub {
+ my ($self, $value) = @_;
+
+ $self->$attribute($value * 60 + ($self->$attribute % 60)) if @_ > 1;
+ return int(($self->$attribute // 0) / 60);
+ };
+
+ *{ $package . '::' . $attribute . '_as_minutes' } = sub {
+ my ($self, $value) = @_;
+
+ $self->$attribute(int($self->$attribute) - (int($self->$attribute) % 60) + ($value // 0)) if @_ > 1;
+ return ($self->$attribute // 0) % 60;
+ };
+
+ *{ $package . '::' . $attribute . '_as_duration_string' } = sub {
+ my ($self, $value) = @_;
+
+ if (@_ > 1) {
+ if (!defined($value) || ($value eq '')) {
+ $self->$attribute(undef);
+ } else {
+ croak $::locale->text("Invalid duration format") if $value !~ m{^(?:(\d*):)?(\d+)$};
+ $self->$attribute(($1 // 0) * 60 + ($2 // 0));
+ }
+ }
+
+ my $as_hours = "${attribute}_as_hours";
+ my $as_minutes = "${attribute}_as_minutes";
+ return defined($self->$attribute) ? sprintf('%d:%02d', $self->$as_hours, $self->$as_minutes) : undef;
+ };
+}
+
1;
__END__
# In a Rose model:
use SL::DB::Helper::AttrDuration;
__PACKAGE__->attr_duration('time_estimation');
+ __PACKAGE__->attr_duration_minutes('hours');
# Read access:
print "Minutes: " . $obj->time_estimation_as_minutes . " hours: " . $obj->time_estimation_as_hours . "\n";
=head1 OVERVIEW
-This is a helper for columns that store a duration as a numeric or
-floating point number representing a number of hours. So the value
-1.75 would stand for "1 hour, 45 minutes".
+This is a helper for columns that store a duration in one of two formats:
-The helper methods created are:
+=over 2
+
+=item 1. as a numeric or floating point number representing a number
+of hours
+
+=item 2. as an integer presenting a number of minutes
+
+=back
+
+In the first case the value 1.75 would stand for "1 hour, 45
+minutes". In the second case the value 105 represents the same
+duration.
+
+The helper methods created depend on the mode. Calling
+C<attr_duration> makes the following methods available:
=over 4
=back
+With C<attr_duration_minutes> the following methods are available:
+
+=over 4
+
+=item C<attribute_as_minutes [$new_value]>
+
+Access only the minutes. Return values are in the range [0 - 59].
+
+=item C<attribute_as_hours [$new_value]>
+
+Access only the hours. Returns an integer value.
+
+=item C<attribute_as_duration_string [$new_value]>
+
+Access the full value as a formatted string in the form C<h:mm>,
+e.g. C<1:30> for the value 90 minutes. Parsing such a string is
+supported, too.
+
+=back
+
=head1 FUNCTIONS
=over 4
Package method. Call with the names of attributes for which the helper
methods should be created.
+=item C<attr_duration_minutes @attributes>
+
+Package method. Call with the names of attributes for which the helper
+methods should be created.
+
=back
=head1 BUGS
map { $_ => $options->{$_} }
sort keys %$options; # deterministic order
- join ';', apply { s/([;\\])/\\$1/g } $dbconnect, $dbuser, $dbpasswd, $options_str, $initial_sql;
+ join ';', apply { $_ //= ''; s/([;\\])/\\$1/g } $dbconnect, $dbuser, $dbpasswd, $options_str, $initial_sql;
}
1;
]);
$TMPL_VAR{ALL_PROJECTS} = SL::DB::Manager::Project->get_all_sorted(query => \@conditions);
+ $form->{ALL_PROJECTS} = $TMPL_VAR{ALL_PROJECTS}; # make projects available for second row drop-down in io.pl
$TMPL_VAR{ALL_EMPLOYEES} = SL::DB::Manager::Employee->get_all_sorted(query => [ or => [ id => $::form->{employee_id}, deleted => 0 ] ]);
$TMPL_VAR{ALL_SALESMEN} = SL::DB::Manager::Employee->get_all_sorted(query => [ or => [ id => $::form->{salesman_id}, deleted => 0 ] ]);
$TMPL_VAR{ALL_SHIPTO} = SL::DB::Manager::Shipto->get_all_sorted(query => [
]);
$TMPL_VAR{ALL_PROJECTS} = SL::DB::Manager::Project->get_all_sorted(query => \@conditions);
+ $form->{ALL_PROJECTS} = $TMPL_VAR{ALL_PROJECTS}; # make projects available for second row drop-down in io.pl
# label subs
my $employee_list_query_gen = sub { $::form->{$_[0]} ? [ or => [ id => $::form->{$_[0]}, deleted => 0 ] ] : [ deleted => 0 ] };
if (focussable(this)) window.focused_element = this;
});
+ // Lowest priority: first focussable element in form.
+ set_cursor_to_first_element();
+
+ // Medium priority: class set in template
var initial_focus = $(".initial_focus").filter(':visible')[0];
if (initial_focus)
$(initial_focus).focus();
// legacy. sone forms install these
if (typeof fokus == 'function') { fokus(); return; }
if (focus_by_name('cursor_fokus')) return;
- set_cursor_to_first_element();
});
$('form').submit(function(){
use lib 't';
-use Test::More tests => 37;
+use Test::More tests => 38;
use Test::Deep;
use Data::Dumper;
'orderitems.part.test' => { 'what', { ilike => '%2%' } },
]
}, 'relationship + additional tokens + filters + methods', class => 'SL::DB::Manager::Order';
+
+test {
+ part => {
+ 'obsolete::lazy_bool_eq' => '0',
+ },
+}, {
+ query => [
+ or => [
+ 'part.obsolete' => undef,
+ 'part.obsolete' => 0
+ ],
+ ],
+ with_objects => [ 'part' ],
+}, 'complex methods modifying the key';
+
__PACKAGE__->meta->setup(
table => 'dummy',
- columns => [ dummy => { type => 'numeric', precision => 2, scale => 12 }, ]
+ columns => [
+ dummy => { type => 'numeric', precision => 2, scale => 12 },
+ inty => { type => 'integer' },
+ ]
);
use SL::DB::Helper::AttrDuration;
__PACKAGE__->attr_duration('dummy');
+__PACKAGE__->attr_duration_minutes('inty');
package main;
-use Test::More tests => 91;
+use Test::More tests => 120;
use Test::Exception;
use strict;
Support::TestSetup::login();
my $item;
+### attr_duration
+
# Wenn das Attribut undef ist:
is(new_item->dummy, undef, 'uninitialized: raw');
is(new_item->dummy_as_hours, 0, 'uninitialized: as_hours');
lives_ok { new_item()->dummy_as_man_days_unit('hour') } 'known unit hour';
lives_ok { new_item()->dummy_as_man_days_unit('man_day') } 'known unit man_day';
+### attr_duration_minutes
+
+# Wenn das Attribut undef ist:
+is(new_item->inty, undef, 'uninitialized: raw');
+is(new_item->inty_as_hours, 0, 'uninitialized: as_hours');
+is(new_item->inty_as_minutes, 0, 'uninitialized: as_minutes');
+is(new_item->inty_as_duration_string, undef, 'uninitialized: as_duration_string');
+
+# Auslesen kleiner 60 Minuten:
+is(new_item(inty => 37)->inty, 37, 'initialized < 60: raw');
+is(new_item(inty => 37)->inty_as_hours, 0, 'initialized < 60: as_hours');
+is(new_item(inty => 37)->inty_as_minutes, 37, 'initialized < 60: as_minutes');
+is(new_item(inty => 37)->inty_as_duration_string, '0:37', 'initialized < 60: as_duration_string');
+
+# Auslesen größer 60 Minuten:
+is(new_item(inty => 145)->inty, 145, 'initialized > 60: raw');
+is(new_item(inty => 145)->inty_as_hours, 2, 'initialized > 60: as_hours');
+is(new_item(inty => 145)->inty_as_minutes, 25, 'initialized > 60: as_minutes');
+is(new_item(inty => 145)->inty_as_duration_string, '2:25', 'initialized > 60: as_duration_string');
+
+$item = new_item(inty => 145); $item->inty_as_duration_string(undef);
+is($item->inty, undef, 'write as_duration_string undef read raw');
+is($item->inty_as_minutes, 0, 'write as_duration_string undef read as_minutes');
+is($item->inty_as_hours, 0, 'write as_duration_string undef read as_hours');
+is($item->inty_as_duration_string, undef, 'write as_duration_string undef read as_duration_string');
+
+$item = new_item(inty => 145); $item->inty_as_duration_string('');
+is($item->inty, undef, 'write as_duration_string "" read raw');
+is($item->inty_as_minutes, 0, 'write as_duration_string "" read as_minutes');
+is($item->inty_as_hours, 0, 'write as_duration_string "" read as_hours');
+is($item->inty_as_duration_string, undef, 'write as_duration_string "" read as_duration_string');
+
+$item = new_item(inty => 145); $item->inty_as_duration_string("3:21");
+is($item->inty, 201, 'write as_duration_string 3:21 read raw');
+is($item->inty_as_minutes, 21, 'write as_duration_string 3:21 read as_minutes');
+is($item->inty_as_hours, 3, 'write as_duration_string 3:21 read as_hours');
+is($item->inty_as_duration_string, "3:21", 'write as_duration_string 3:21 read as_duration_string');
+
+$item = new_item(inty => 145); $item->inty_as_duration_string("03:1");
+is($item->inty, 181, 'write as_duration_string 03:1 read raw');
+is($item->inty_as_minutes, 1, 'write as_duration_string 03:1 read as_minutes');
+is($item->inty_as_hours, 3, 'write as_duration_string 03:1 read as_hours');
+is($item->inty_as_duration_string, "3:01", 'write as_duration_string 03:1 read as_duration_string');
+
+# Parametervalidierung
+throws_ok { new_item()->inty_as_duration_string('invalid') } qr/invalid.*format/i, 'invalid duration format';
+
done_testing();
% einkaufslieferschein (purchase_delivery_order)
\newcommand{\einkaufslieferschein} {Eingangslieferschein}
+
+% Brief/letter
+\newcommand{\ihrzeichen}{Ihr Zeichen}
+\newcommand{\betreff}{Betreff}
% einkaufslieferschein (purchase_delivery_order)
\newcommand{\einkaufslieferschein} {Purchase delivery order}
+
+% Brief/letter
+\newcommand{\ihrzeichen}{Your reference}
+\newcommand{\betreff}{Subject}
<%contact_formal%>
- <%countrycode%> <%zipcode%> <%city%>
+ <%street%>
+
+ <%zipcode%> <%city%>
<%country%>
\vspace{2.5cm}
\hfill<%date%>
-\textbf{<%reference%>}
-\vspace{1cm}
+<%if reference%>
+\textbf{\ihrzeichen : <%reference%>}
+<%end if%>
+\vspace{1cm}
-\textbf{<%subject%>}
+<%if subject%>
+\textbf{\betreff : <%subject%>}
+<%end if%>
\vspace{1cm}
[%- INCLUDE 'common/flash.html' %]
-[%- PROCESS 'background_job_history/_filter.html' filter=SELF.filter %]
+[%- PROCESS 'background_job_history/_filter.html' filter=SELF.models.filtered.laundered %]
[% IF !ENTRIES.size %]
<p>
[%- USE L %]
-[%- PROCESS 'bank_transactions/_filter.html' filter=SELF.filter %]
+[%- PROCESS 'bank_transactions/_filter.html' filter=SELF.models.filtered.laundered %]
<hr>
[%- USE L %]
-[%- PROCESS 'delivery_plan/_filter.html' filter=SELF.filter %]
+[%- PROCESS 'delivery_plan/_filter.html' filter=SELF.models.filtered.laundered %]
<hr>
[%- SET vctypelabel = vc == 'customer' ? LxERP.t8('Customer type') : LxERP.t8('Vendor type') %]
- <script type="text/javascript">
- $(function(){ document.Form.donumber.focus(); });
- </script>
-
<style type="text/css">
.fixed_width {
width: 250px;
<th align="right">[% IF is_customer %][% 'Customer' | $T8 %][% ELSE %][% 'Vendor' | $T8 %][% END %]</th>
<td colspan="3">
[%- UNLESS SHOW_VC_DROP_DOWN %]
- <input type="text" name="[% HTML.escape(vc) %]" class="fixed_width">
+ <input type="text" name="[% HTML.escape(vc) %]" class="fixed_width initial_focus">
[%- ELSE %]
- <select name="[% vc %]" class="fixed_width">
+ <select name="[% vc %]" class="fixed_width initial_focus">
<option></option>
[%- FOREACH row = ALL_VC %]
<option>[% HTML.escape(row.name) %]--[% HTML.escape(row.id) %]</option>
[%- USE L %]
-[%- PROCESS 'financial_controlling_report/_filter.html' filter=SELF.filter %]
+[%- PROCESS 'financial_controlling_report/_filter.html' filter=SELF.models.filtered.laundered %]
<hr>
<div style='overflow:hidden'>
-[% LxERP.t8("Filter") %]: [% L.input_tag('part_picker_filter', SELF.filter.all_substr_multi__ilike, class='part_picker_filter') %]
+[% LxERP.t8("Filter") %]: [% L.input_tag('part_picker_filter', SELF.models.filtered.laundered.all_substr_multi__ilike, class='part_picker_filter') %]
[% L.hidden_tag('part_picker_real_id', FORM.real_id) %]
<div class='float-right'>
[%- USE L %]
[%- PROCESS 'common/flash.html' %]
-[%- PROCESS 'price_rule/_filter.html' filter=SELF.filter UNLESS FORM.inline %]
+[%- PROCESS 'price_rule/_filter.html' filter=SELF.models.filtered.laundered UNLESS FORM.inline %]
<hr>
</div>
<div class='filter_toggle' style='display:none'>
<a href='#' onClick='javascript:$(".filter_toggle").toggle()'>[% 'Hide Filter' | $T8 %]</a>
-[%- PROCESS 'project/_filter.html' filter=SELF.filter %]
+[%- PROCESS 'project/_filter.html' filter=SELF.models.filtered.laundered %]
[% L.hidden_tag('action', 'Project/dispatch') %]
[% L.hidden_tag('sort_by', FORM.sort_by) %]
<td class="top_border"></td>
<td class="top_border"></td>
<td class="top_border"></td>
- <td class="top_border"></td>
<td class="bt_balance top_border" align="right">[% LxERP.format_amount(SELF.bt_balance, 2) %]</td>
<td class="bb_balance top_border" align="right">[% LxERP.format_amount(-1 * SELF.bb_balance, 2) %]</td>
<td class="top_border"></td>
<td class="top_border"></td>
<td class="top_border"></td>
<td class="top_border"></td>
+ <td class="top_border"></td>
</tr>
</tfoot>
</table>
[%- USE L %]
-[%- PROCESS "requirement_spec/_filter.html" filter=SELF.filter %]
+[%- PROCESS "requirement_spec/_filter.html" filter=SELF.models.filtered.laundered %]
<hr>