Angular Acq Search: Perl API changes
authorMike Rylander <mrylander@gmail.com>
Thu, 31 Oct 2019 19:23:22 +0000 (15:23 -0400)
committerGalen Charlton <gmc@equinoxinitiative.org>
Mon, 3 Feb 2020 16:52:43 +0000 (11:52 -0500)
* 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 <mrylander@gmail.com>
Signed-off-by: Jason Etheridge <jason@EquinoxInitiative.org>
Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>
Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Financials.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Invoice.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Lineitem.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Picklist.pm
Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Search.pm

index e66e648..062a9d1 100644 (file)
@@ -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};
 
index 78765c6..653a664 100644 (file)
@@ -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);
 }
index af491b6..2ed6dae 100644 (file)
@@ -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');
index 2e9381f..4030b58 100644 (file)
@@ -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;
 }
index c09fd35..83d23a8 100644 (file)
@@ -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}) {