From: Mike Rylander Date: Thu, 31 Oct 2019 19:23:22 +0000 (-0400) Subject: Angular Acq Search: Perl API changes X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=758be409929aa1d43c4ac34765084ab3351fe289;p=working%2FEvergreen.git Angular Acq Search: Perl API changes * Add __age (interval), __starts, and __ends operators. Update __between to support __castdate modifier, for a more natural comparison of dates entered by humans to timestamps stored in the database. * teach open-ils.acq.lineitem.retrieve more fleshing In particular, teach it how to flesh the LI provider, Vandelay queue, and claim policy so that Angular LI search can display them without having to make additional server requests. * Add additonal fleshing for purchase order, selection lists, and invoice searches. * Add au_by_id option This specifies performing queries on au-linked fields by ID rather than adding joins to query the fields by user barcode or username, etc. * Implement "contains" operator for provider searches. This is similar to how user searches are handled. This patch contains work by Mike Rylander, Galen Charlton, and Jason Etheridge. Signed-off-by: Mike Rylander Signed-off-by: Jason Etheridge Signed-off-by: Galen Charlton --- diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Financials.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Financials.pm index e66e648563..062a9d1e69 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Financials.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Financials.pm @@ -1015,6 +1015,15 @@ sub retrieve_purchase_order_impl { if ($options->{"flesh_provider"}) { push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "provider"; } + if ($options->{"flesh_owner"}) { + push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "owner"; + } + if ($options->{"flesh_creator"}) { + push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "creator"; + } + if ($options->{"flesh_editor"}) { + push @{$flesh->{"flesh_fields"}->{"acqpo"}}, "editor"; + } push (@{$flesh->{flesh_fields}->{acqpo}}, 'po_items') if $options->{flesh_po_items}; diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Invoice.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Invoice.pm index 78765c62c4..653a66411b 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Invoice.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Invoice.pm @@ -717,6 +717,13 @@ sub fetch_invoice_impl { } } ]; + if ($options->{"flesh_provider"}) { + if ($options->{"no_flesh_misc"}) { + $args = [ $invoice_id, { "flesh" => 1, "flesh_fields" => { "acqinv" => [] } } ]; + } + push @{ $args->[1]->{flesh_fields}->{acqinv} }, "provider"; + push @{ $args->[1]->{flesh_fields}->{acqinv} }, "shipper"; + } return $e->retrieve_acq_invoice($args); } diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Lineitem.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Lineitem.pm index af491b6c68..2ed6dae2c4 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Lineitem.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Lineitem.pm @@ -129,6 +129,9 @@ flesh_attrs : for attributes, flesh_notes : for notes, flesh_cancel_reason : for cancel reason, flesh_li_details : for order details objects, +flesh_provider : for provider, +flesh_claim_policy : for claim policy, +flesh_queued_record : for queued bib record from Vandelay, clear_marc : to clear marcxml from lineitem/, type => 'hash'}, ], return => {desc => 'lineitem object on success, Event on error'} @@ -163,6 +166,9 @@ sub retrieve_lineitem_impl { push(@{$fields->{acqlin}}, 'alert_text') if $$options{flesh_notes}; push(@{$fields->{jub} }, 'order_summary') if $$options{flesh_order_summary}; push(@{$fields->{jub} }, 'cancel_reason') if $$options{flesh_cancel_reason}; + push(@{$fields->{jub} }, 'provider') if $$options{flesh_provider}; + push(@{$fields->{jub} }, 'claim_policy') if $$options{flesh_claim_policy}; + push(@{$fields->{jub} }, 'queued_record') if $$options{flesh_queued_record}; if($$options{flesh_li_details}) { push(@{$fields->{jub} }, 'lineitem_details'); diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Picklist.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Picklist.pm index 2e9381f1d4..4030b58eca 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Picklist.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Picklist.pm @@ -127,6 +127,10 @@ sub retrieve_picklist_impl { if($$options{flesh_owner}); $picklist->owner($e->retrieve_actor_user($picklist->owner)->usrname) if($$options{flesh_username}); + $picklist->creator($e->retrieve_actor_user($picklist->creator)) + if($$options{flesh_creator}); + $picklist->editor($e->retrieve_actor_user($picklist->editor)) + if($$options{flesh_editor}); return $picklist; } diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Search.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Search.pm index c09fd35543..83d23a8bed 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Search.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Search.pm @@ -52,11 +52,12 @@ sub could_be_range { } sub castdate { - my ($value, $gte, $lte) = @_; + my ($value, $gte, $lte, $between) = @_; my $op = "="; $op = ">=" if $gte; $op = "<=" if $lte; + $op = "between" if $between; # avoid transforming a date if the match value is NULL. return {'=' => undef} if $op eq '=' and not $value; @@ -76,7 +77,7 @@ sub prepare_acqlia_search_and { }; # castdate not supported for acqlia fields: they're all type text - my ($k, $v, $fuzzy, $between, $not) = breakdown_term($unit); + my ($k, $v, $fuzzy, $between, $not, $starts, $ends) = breakdown_term($unit); my $point = $subquery->{"where"}->{"-and"}; my $term_clause; @@ -84,6 +85,10 @@ sub prepare_acqlia_search_and { if ($fuzzy and not ref $v) { push @$point, {"attr_value" => {"ilike" => "%" . $v . "%"}}; + } elsif ($starts and not ref $v) { + push @$point, {"attr_value" => {"ilike" => $v . "%"}}; + } elsif ($ends and not ref $v) { + push @$point, {"attr_value" => {"ilike" => "%" . $v}}; } elsif ($between and could_be_range($v)) { push @$point, {"attr_value" => {"between" => $v}}; } elsif (check_1d_max($v)) { @@ -106,7 +111,7 @@ sub prepare_acqlia_search_or { foreach my $unit (@$acqlia) { # castdate not supported for acqlia fields: they're all type text - my ($k, $v, $fuzzy, $between, $not) = breakdown_term($unit); + my ($k, $v, $fuzzy, $between, $not, $starts, $ends) = breakdown_term($unit); my $term_clause; if ($fuzzy and not ref $v) { $term_clause = { @@ -115,6 +120,20 @@ sub prepare_acqlia_search_or { "attr_value" => {"ilike" => "%" . $v . "%"} } }; + } elsif ($starts and not ref $v) { + $term_clause = { + "-and" => { + "definition" => $k, + "attr_value" => {"ilike" => $v . "%"} + } + }; + } elsif ($ends and not ref $v) { + $term_clause = { + "-and" => { + "definition" => $k, + "attr_value" => {"ilike" => "%" . $v} + } + }; } elsif ($between and could_be_range($v)) { $term_clause = { "-and" => { @@ -143,9 +162,12 @@ sub breakdown_term { $term->{"__fuzzy"} ? 1 : 0, $term->{"__between"} ? 1 : 0, $term->{"__not"} ? 1 : 0, + $term->{"__starts"} ? 1 : 0, + $term->{"__ends"} ? 1 : 0, $term->{"__castdate"} ? 1 : 0, $term->{"__gte"} ? 1 : 0, - $term->{"__lte"} ? 1 : 0 + $term->{"__lte"} ? 1 : 0, + $term->{"__age"} ? 1 : 0, ); } @@ -226,6 +248,74 @@ sub prepare_au_terms { @joins; } +sub gen_acqpro_term { + my ($value, $n) = @_; + my $lc_value = { + "=" => { transform => "lowercase", value => lc($value) } + }; + + +{ + "-or" => [ + {"+acqpro$n" => {"name" => $value}}, + {"+acqpro$n" => {"code" => $lc_value}}, + ] + }; +} + +# go through the terms hash, find keys that correspond to fields links +# to actor.usr, and rewrite the search as one that searches not by +# actor.usr.id but by any of these user properties: card barcode, username, +# given names and family name. +sub prepare_acqpro_terms { + my ($terms, $join_num) = @_; + + my @joins = (); + my $nots = 0; + $join_num ||= 0; + + foreach my $conj (qw/-and -or/) { + next unless exists $terms->{$conj}; + + my @new_outer_terms = (); + HINT_UNIT: foreach my $hint_unit (@{$terms->{$conj}}) { + my $hint = (keys %$hint_unit)[0]; + (my $plain_hint = $hint) =~ y/+//d; + if ($hint eq "-not") { + $hint_unit = $hint_unit->{$hint}; + $nots++; + redo HINT_UNIT; + } + + if (my $links = get_fm_links_by_hint($plain_hint) and + $plain_hint ne "acqlia") { + my @new_terms = (); + my ($attr, $value) = breakdown_term($hint_unit->{$hint}); + my $is_fuzzy = ref($value) eq 'HASH' && exists($value->{'ilike'}); + if ($links->{$attr} and + $is_fuzzy and + $links->{$attr}->{"class"} eq "acqpro") { + push @joins, [$plain_hint, $attr, $join_num]; + my $acqpro_term = gen_acqpro_term($value, $join_num); + if ($nots > 0) { + $acqpro_term = {"-not" => $acqpro_term}; + $nots--; + } + push @new_outer_terms, $acqpro_term; + $join_num++; + delete $hint_unit->{$hint}; + } + } + if ($nots > 0) { + $hint_unit = {"-not" => $hint_unit}; + $nots--; + } + push @new_outer_terms, $hint_unit if scalar keys %$hint_unit; + } + $terms->{$conj} = [ @new_outer_terms ]; + } + @joins; +} + sub prepare_terms { my ($terms, $is_and) = @_; @@ -238,17 +328,39 @@ sub prepare_terms { $outer_clause->{$conj} = [] unless $outer_clause->{$conj}; foreach my $unit (@{$terms->{$class}}) { my $special_clause; - my ($k, $v, $fuzzy, $between, $not, $castdate, $gte, $lte) = + my ($k, $v, $fuzzy, $between, $not, $starts, $ends, $castdate, $gte, $lte, $age) = breakdown_term($unit); my $term_clause; - if ($fuzzy and not ref $v) { + if ($age and not ref $v) { # $v is expected to be parsed as an interval + $v =~ s/^\s*//; + + my $op = $gte ? '>=' : '<='; + $term_clause = {$k => {$op => {transform => 'age', params => ['now'], value => '-' . $v}}}; + + # !!! NOTE: we invert $not because we have to compare to a /negative/ + # interval, due to json_query restiction on function parameter order for + # transformed fields, so we flip the comparison. Alternatively we could + # swap the GTE and LTE operators, but that would make the query harder + # to read and make diagnosing issues much more difficult. + $not = $not ? 0 : 1; + + } elsif ($starts and not ref $v) { + $term_clause = {$k => {"ilike" => $v . "%"}}; + } elsif ($ends and not ref $v) { + $term_clause = {$k => {"ilike" => "%" . $v}}; + } elsif ($fuzzy and not ref $v) { $term_clause = {$k => {"ilike" => "%" . $v . "%"}}; } elsif ($between and could_be_range($v)) { - $term_clause = {$k => {"between" => $v}}; + if ($castdate) { + $v = castdate($v, 0, 0, $between); + $term_clause = {$k => $v}; + } else { + $term_clause = {$k => {between => $v}}; + } } elsif (check_1d_max($v)) { if ($castdate) { - $v = castdate($v, $gte, $lte) if $castdate; + $v = castdate($v, $gte, $lte); } elsif ($gte or $lte) { my $op = $gte ? '>=' : '<='; $v = {$op => $v}; @@ -323,6 +435,33 @@ sub add_au_joins { $n; } +sub add_acqpro_joins { + my $graft_map = shift; + my $core_hint = shift; + + my $n = 0; + foreach my $join (@_) { + my ($hint, $attr, $num) = @$join; + my $start = $graft_map->{$hint}; + my $clause = { + "class" => "acqpro", + "type" => "left", + "field" => "id", + "fkey" => $attr, + }; + + if ($hint eq $core_hint) { + $start->{"acqpro$num"} = $clause; + } else { + $start->{"join"} ||= {}; + $start->{"join"}->{"acqpro$num"} = $clause; + } + + $n++; + } + $n; +} + sub build_from_clause_and_joins { my ($query, $core, $and_terms, $or_terms) = @_; @@ -503,8 +642,13 @@ q/order_by clause must be of the long form, like: $and_terms = prepare_terms($and_terms, 1); $or_terms = prepare_terms($or_terms, 0); - my $offset = add_au_joins($graft_map, $hint, prepare_au_terms($and_terms)); - add_au_joins($graft_map, $hint, prepare_au_terms($or_terms, $offset)); + unless ($options->{au_by_id}) { + my $offset = add_au_joins($graft_map, $hint, prepare_au_terms($and_terms)); + add_au_joins($graft_map, $hint, prepare_au_terms($or_terms, $offset)); + } + + my $offset = add_acqpro_joins($graft_map, $hint, prepare_acqpro_terms($and_terms)); + add_acqpro_joins($graft_map, $hint, prepare_acqpro_terms($or_terms, $offset)); # The join to acqmapinv needs to be a left join when present. if ($query->{from}{$hint}{acqmapinv}) {