1 package OpenILS::Application::Acq::Search;
2 use base "OpenILS::Application";
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;
16 \&{"OpenILS::Application::Acq::Lineitem::retrieve_lineitem_impl"},
18 \&{"OpenILS::Application::Acq::Picklist::retrieve_picklist_impl"},
19 "purchase_order" => \&{
20 "OpenILS::Application::Acq::Financials::retrieve_purchase_order_impl"
24 sub F { $Fieldmapper::fieldmap->{"Fieldmapper::" . $_[0]}; }
26 # This subroutine returns 1 if the argument is a) a scalar OR
27 # b) an array of ONLY scalars. Otherwise it returns 0.
30 return 1 unless ref $o;
31 if (ref($o) eq "ARRAY") {
32 foreach (@$o) { return 0 if ref $_; }
38 # Returns 1 if and only if argument is an array of exactly two scalars.
41 if (ref $o eq "ARRAY") {
42 return 1 if (scalar(@$o) == 2 && (!ref $o->[0] && !ref $o->[1]));
47 sub prepare_acqlia_search_and {
51 foreach my $unit (@{$acqlia}) {
53 "select" => {"acqlia" => ["id"]},
55 "where" => {"-and" => [{"lineitem" => {"=" => {"+jub" => "id"}}}]}
58 my ($k, $v, $fuzzy, $between, $not) = breakdown_term($unit);
59 my $point = $subquery->{"where"}->{"-and"};
62 push @$point, {"definition" => $k};
64 if ($fuzzy and not ref $v) {
65 push @$point, {"attr_value" => {"ilike" => "%" . $v . "%"}};
66 } elsif ($between and could_be_range($v)) {
67 push @$point, {"attr_value" => {"between" => $v}};
68 } elsif (check_1d_max($v)) {
69 push @$point, {"attr_value" => $v};
74 my $operator = $not ? "-not-exists" : "-exists";
75 push @phrases, {$operator => $subquery};
80 sub prepare_acqlia_search_or {
84 my $result = {"+acqlia" => {"-or" => $point}};
86 foreach my $unit (@$acqlia) {
87 my ($k, $v, $fuzzy, $between, $not) = breakdown_term($unit);
89 if ($fuzzy and not ref $v) {
93 "attr_value" => {"ilike" => "%" . $v . "%"}
96 } elsif ($between and could_be_range($v)) {
99 "definition" => $k, "attr_value" => {"between" => $v}
102 } elsif (check_1d_max($v)) {
104 "-and" => {"definition" => $k, "attr_value" => $v}
110 push @$point, $not ? {"-not" => $term_clause} : $term_clause;
118 my $key = (grep { !/^__/ } keys %$term)[0];
121 $term->{"__fuzzy"} ? 1 : 0,
122 $term->{"__between"} ? 1 : 0,
123 $term->{"__not"} ? 1 : 0
127 sub get_fm_links_by_hint {
129 foreach my $field (values %{$Fieldmapper::fieldmap}) {
130 return $field->{"links"} if $field->{"hint"} eq $hint;
136 my ($value, $n) = @_;
139 {"+au$n" => {"usrname" => $value}},
140 {"+au$n" => {"first_given_name" => $value}},
141 {"+au$n" => {"second_given_name" => $value}},
142 {"+au$n" => {"family_name" => $value}},
143 {"+ac$n" => {"barcode" => $value}}
148 # go through the terms hash, find keys that correspond to fields links
149 # to actor.usr, and rewrite the search as one that searches not by
150 # actor.usr.id but by any of these user properties: card barcode, username,
151 # given names and family name.
152 sub prepare_au_terms {
153 my ($terms, $join_num) = @_;
159 foreach my $conj (qw/-and -or/) {
160 next unless exists $terms->{$conj};
162 my @new_outer_terms = ();
163 HINT_UNIT: foreach my $hint_unit (@{$terms->{$conj}}) {
164 my $hint = (keys %$hint_unit)[0];
165 (my $plain_hint = $hint) =~ y/+//d;
166 if ($hint eq "-not") {
167 $hint_unit = $hint_unit->{$hint};
172 if (my $links = get_fm_links_by_hint($plain_hint) and
173 $plain_hint ne "acqlia") {
175 my ($attr, $value) = breakdown_term($hint_unit->{$hint});
176 if ($links->{$attr} and
177 $links->{$attr}->{"class"} eq "au") {
178 push @joins, [$plain_hint, $attr, $join_num];
179 my $au_term = gen_au_term($value, $join_num);
181 $au_term = {"-not" => $au_term};
184 push @new_outer_terms, $au_term;
186 delete $hint_unit->{$hint};
190 $hint_unit = {"-not" => $hint_unit};
193 push @new_outer_terms, $hint_unit if scalar keys %$hint_unit;
195 $terms->{$conj} = [ @new_outer_terms ];
201 my ($terms, $is_and) = @_;
203 my $conj = $is_and ? "-and" : "-or";
204 my $outer_clause = {};
206 foreach my $class (qw/acqpo acqpl jub/) {
207 next if not exists $terms->{$class};
209 $outer_clause->{$conj} = [] unless $outer_clause->{$conj};
210 foreach my $unit (@{$terms->{$class}}) {
211 my ($k, $v, $fuzzy, $between, $not) = breakdown_term($unit);
213 if ($fuzzy and not ref $v) {
214 $term_clause = {$k => {"ilike" => "%" . $v . "%"}};
215 } elsif ($between and could_be_range($v)) {
216 $term_clause = {$k => {"between" => $v}};
217 } elsif (check_1d_max($v)) {
218 $term_clause = {$k => $v};
223 my $clause = {"+" . $class => $term_clause};
224 $clause = {"-not" => $clause} if $not;
225 push @{$outer_clause->{$conj}}, $clause;
229 if ($terms->{"acqlia"}) {
230 push @{$outer_clause->{$conj}},
231 $is_and ? prepare_acqlia_search_and($terms->{"acqlia"}) :
232 prepare_acqlia_search_or($terms->{"acqlia"});
235 return undef unless scalar keys %$outer_clause;
243 foreach my $join (@_) {
244 my ($hint, $attr, $num) = @$join;
245 my $start = $hint eq "jub" ? $from->{$hint} : $from->{"jub"}->{$hint};
260 if ($hint eq "jub") {
261 $start->{"au$num"} = $clause;
263 $start->{"join"} ||= {};
264 $start->{"join"}->{"au$num"} = $clause;
271 __PACKAGE__->register_method(
272 method => "unified_search",
273 api_name => "open-ils.acq.lineitem.unified_search",
276 desc => q/Returns lineitems based on flexible search terms./,
278 {desc => "Authentication token", type => "string"},
279 {desc => "Field/value pairs for AND'ing", type => "object"},
280 {desc => "Field/value pairs for OR'ing", type => "object"},
281 {desc => "Conjunction between AND pairs and OR pairs " .
282 "(can be 'and' or 'or')", type => "string"},
283 {desc => "Retrieval options (clear_marc, flesh_notes, etc) " .
284 "- XXX detail all the options",
287 return => {desc => "A stream of LIs on success, Event on failure"}
291 __PACKAGE__->register_method(
292 method => "unified_search",
293 api_name => "open-ils.acq.purchase_order.unified_search",
296 desc => q/Returns purchase orders based on flexible search terms.
297 See open-ils.acq.lineitem.unified_search/,
298 return => {desc => "A stream of POs on success, Event on failure"}
302 __PACKAGE__->register_method(
303 method => "unified_search",
304 api_name => "open-ils.acq.picklist.unified_search",
307 desc => q/Returns pick lists based on flexible search terms.
308 See open-ils.acq.lineitem.unified_search/,
309 return => {desc => "A stream of PLs on success, Event on failure"}
314 my ($self, $conn, $auth, $and_terms, $or_terms, $conj, $options) = @_;
317 my $e = new_editor("authtoken" => $auth);
318 return $e->die_event unless $e->checkauth;
320 # What kind of object are we returning? Important: (\w+) had better be
321 # a legit acq classname particle, so don't register any crazy api_names.
322 my $ret_type = ($self->api_name =~ /cq.(\w+).un/)[0];
323 my $retriever = $RETRIEVERS{$ret_type};
324 my $hint = F("acq::$ret_type")->{"hint"};
329 [{"column" => "id", "transform" => "distinct"}]
336 "fkey" => "purchase_order"
345 "order_by" => { $hint => {"id" => {}}},
346 "offset" => ($options->{"offset"} || 0)
349 $query->{"limit"} = $options->{"limit"} if $options->{"limit"};
351 $and_terms = prepare_terms($and_terms, 1);
352 $or_terms = prepare_terms($or_terms, 0) and do {
353 $query->{"from"}->{"jub"}->{"acqlia"} = {
354 "type" => "left", "field" => "lineitem", "fkey" => "id",
358 # TODO find instances of fields of type "timestamp" and massage the
359 # comparison to match search input (which is only at date precision,
361 my $offset = add_au_joins($query->{"from"}, prepare_au_terms($and_terms));
362 add_au_joins($query->{"from"}, prepare_au_terms($or_terms, $offset));
364 if ($and_terms and $or_terms) {
365 $query->{"where"} = {
366 "-" . (lc $conj eq "or" ? "or" : "and") => [$and_terms, $or_terms]
368 } elsif ($and_terms) {
369 $query->{"where"} = $and_terms;
370 } elsif ($or_terms) {
371 $query->{"where"} = $or_terms;
374 return new OpenILS::Event("BAD_PARAMS", "desc" => "No usable terms");
377 my $results = $e->json_query($query) or return $e->die_event;
378 if ($options->{"id_list"}) {
379 foreach (@$results) {
380 $conn->respond($_->{"id"}) if $_->{"id"};
383 foreach (@$results) {
384 $conn->respond($retriever->($e, $_->{"id"}, $options))