]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Acq/Search.pm
Acq: middle-layer support for unified lineitem/purchase order/picklist search
[working/Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Acq / Search.pm
1 package OpenILS::Application::Acq::Search;
2 use base "OpenILS::Application";
3
4 use strict;
5 use warnings;
6
7 use OpenILS::Event;
8 use OpenILS::Utils::CStoreEditor q/:funcs/;
9 use OpenILS::Utils::Fieldmapper;
10 use OpenILS::Application::Acq::Lineitem;
11 use OpenILS::Application::Acq::Financials;
12 use OpenILS::Application::Acq::Picklist;
13
14 my %RETRIEVERS = (
15     "lineitem" =>
16         \&{"OpenILS::Application::Acq::Lineitem::retrieve_lineitem_impl"},
17     "picklist" =>
18         \&{"OpenILS::Application::Acq::Picklist::retrieve_picklist_impl"},
19     "purchase_order" => \&{
20         "OpenILS::Application::Acq::Financials::retrieve_purchase_order_impl"
21     }
22 );
23
24 sub F { $Fieldmapper::fieldmap->{"Fieldmapper::" . $_[0]}; }
25
26 # This subroutine returns 1 if the argument is a) a scalar OR
27 # b) an array of ONLY scalars. Otherwise it returns 0.
28 sub check_1d_max {
29     my ($o) = @_;
30     return 1 unless ref $o;
31     if (ref($o) eq "ARRAY") {
32         foreach (@$o) { return 0 if ref $_; }
33         return 1;
34     }
35     0;
36 }
37
38 # Returns 1 if and only if argument is an array of exactly two scalars.
39 sub could_be_range {
40     my ($o) = @_;
41     if (ref $o eq "ARRAY") {
42         return 1 if (scalar(@$o) == 2 && (!ref $o->[0] && !ref $o->[1]));
43     }
44     0;
45 }
46
47 sub prepare_acqlia_search_and {
48     my ($acqlia) = @_;
49
50     my @phrases = ();
51     foreach my $unit (@{$acqlia}) {
52         my $something = 0;
53         my $subquery = {
54             "select" => {"acqlia" => ["id"]},
55             "from" => "acqlia",
56             "where" => {"-and" => [{"lineitem" => {"=" => {"+jub" => "id"}}}]}
57         };
58
59         while (my ($k, $v) = each %$unit) {
60             my $point = $subquery->{"where"}->{"-and"};
61             if ($k !~ /^__/) {
62                 push @$point, {"definition" => $k};
63                 $something++;
64
65                 if ($unit->{"__fuzzy"} and not ref $v) {
66                     push @$point, {"attr_value" => {"ilike" => "%" . $v . "%"}};
67                 } elsif ($unit->{"__between"} and could_be_range($v)) {
68                     push @$point, {"attr_value" => {"between" => $v}};
69                 } elsif (check_1d_max($v)) {
70                     push @$point, {"attr_value" => $v};
71                 } else {
72                     $something--;
73                 }
74             }
75         }
76         push @phrases, {"-exists" => $subquery} if $something;
77     }
78     @phrases;
79 }
80
81 sub prepare_acqlia_search_or {
82     my ($acqlia) = @_;
83
84     my $point = [];
85     my $result = {"+acqlia" => {"-or" => $point}};
86
87     foreach my $unit (@$acqlia) {
88         while (my ($k, $v) = each %$unit) {
89             if ($k !~ /^__/) {
90                 if ($unit->{"__fuzzy"} and not ref $v) {
91                     push @$point, {
92                         "-and" => {
93                             "definition" => $k,
94                             "attr_value" => {"ilike" => "%" . $v . "%"}
95                         }
96                     };
97                 } elsif ($unit->{"__between"} and could_be_range($v)) {
98                     push @$point, {
99                         "-and" => {
100                             "definition" => $k,
101                             "attr_value" => {"between" => $v}
102                         }
103                     };
104                 } elsif (check_1d_max($v)) {
105                     push @$point, {
106                         "-and" => {"definition" => $k, "attr_value" => $v}
107                     };
108                 } else {
109                     next;
110                 }
111                 last;
112             }
113         }
114     }
115     $result;
116 }
117
118 sub prepare_terms {
119     my ($terms, $is_and) = @_;
120
121     my $conj = $is_and ? "-and" : "-or";
122     my $outer_clause = {};
123
124     foreach my $class (qw/acqpo acqpl jub/) {
125         next if not exists $terms->{$class};
126
127         my $clause = [];
128         $outer_clause->{$conj} = [] unless $outer_clause->{$conj};
129         foreach my $unit (@{$terms->{$class}}) {
130             while (my ($k, $v) = each %$unit) {
131                 if ($k !~ /^__/) {
132                     if ($unit->{"__fuzzy"} and not ref $v) {
133                         push @$clause, {$k => {"ilike" => "%" . $v . "%"}};
134                     } elsif ($unit->{"__between"} and could_be_range($v)) {
135                         push @$clause, {$k => {"between" => $v}};
136                     } elsif (check_1d_max($v)) {
137                         push @$clause, {$k => $v};
138                     }
139                 }
140             }
141         }
142         push @{$outer_clause->{$conj}}, {"+" . $class => $clause};
143     }
144
145     if ($terms->{"acqlia"}) {
146         push @{$outer_clause->{$conj}},
147             $is_and ? prepare_acqlia_search_and($terms->{"acqlia"}) :
148                 prepare_acqlia_search_or($terms->{"acqlia"});
149     }
150
151     return undef unless scalar keys %$outer_clause;
152     $outer_clause;
153 }
154
155 __PACKAGE__->register_method(
156     method    => "grand_search",
157     api_name  => "open-ils.acq.lineitem.grand_search",
158     signature => {
159         desc   => q/Returns lineitems based on flexible search terms./,
160         params => [
161             {desc => "Authentication token", type => "string"},
162             {desc => "Field/value pairs for AND'ing", type => "object"},
163             {desc => "Field/value pairs for OR'ing", type => "object"},
164             {desc => "Conjunction between AND pairs and OR pairs " .
165                 "(can be 'and' or 'or')", type => "string"},
166             {desc => "Retrieval options (clear_marc, flesh_notes, etc) " .
167                 "- XXX detail all the options",
168                 type => "object"}
169         ],
170         return => {desc => "A stream of LIs on success, Event on failure"}
171     }
172 );
173
174 __PACKAGE__->register_method(
175     method    => "grand_search",
176     api_name  => "open-ils.acq.purchase_order.grand_search",
177     signature => {
178         desc   => q/Returns purchase orders based on flexible search terms.
179             See open-ils.acq.lineitem.grand_search/,
180         return => {desc => "A stream of POs on success, Event on failure"}
181     }
182 );
183
184 __PACKAGE__->register_method(
185     method    => "grand_search",
186     api_name  => "open-ils.acq.picklist.grand_search",
187     signature => {
188         desc   => q/Returns pick lists based on flexible search terms.
189             See open-ils.acq.lineitem.grand_search/,
190         return => {desc => "A stream of PLs on success, Event on failure"}
191     }
192 );
193
194 sub grand_search {
195     my ($self, $conn, $auth, $and_terms, $or_terms, $conj, $options) = @_;
196     my $e = new_editor("authtoken" => $auth);
197     return $e->die_event unless $e->checkauth;
198
199     # What kind of object are we returning? Important: (\w+) had better be
200     # a legit acq classname particle, so don't register any crazy api_names.
201     my $ret_type = ($self->api_name =~ /cq.(\w+).gr/)[0];
202     my $retriever = $RETRIEVERS{$ret_type};
203
204     my $query = {
205         "select" => {
206             F("acq::$ret_type")->{"hint"} =>
207                 [{"column" => "id", "transform" => "distinct"}]
208         },
209         "from" => {
210             "jub" => {
211                 "acqpo" => {
212                     "type" => "full",
213                     "field" => "id",
214                     "fkey" => "purchase_order"
215                 },
216                 "acqpl" => {
217                     "type" => "full",
218                     "field" => "id",
219                     "fkey" => "picklist"
220                 }
221             }
222         }
223     };
224
225     $and_terms = prepare_terms($and_terms, 1);
226     $or_terms = prepare_terms($or_terms, 0) and do {
227         $query->{"from"}->{"jub"}->{"acqlia"} = {
228             "type" => "left", "field" => "lineitem", "fkey" => "id",
229         };
230     };
231
232     if ($and_terms and $or_terms) {
233         $query->{"where"} = {
234             "-" . (lc $conj eq "or" ? "or" : "and") => [$and_terms, $or_terms]
235         };
236     } elsif ($and_terms) {
237         $query->{"where"} = $and_terms;
238     } elsif ($or_terms) {
239         $query->{"where"} = $or_terms;
240     } else {
241         $e->disconnect;
242         return new OpenILS::Event("BAD_PARAMS", "desc" => "No usable terms");
243     }
244
245     my $results = $e->json_query($query) or return $e->die_event;
246     $conn->respond($retriever->($e, $_->{"id"}, $options)) foreach (@$results);
247     $e->disconnect;
248     undef;
249 }
250
251 1;