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}) {
54 "select" => {"acqlia" => ["id"]},
56 "where" => {"-and" => [{"lineitem" => {"=" => {"+jub" => "id"}}}]}
59 while (my ($k, $v) = each %$unit) {
60 my $point = $subquery->{"where"}->{"-and"};
62 push @$point, {"definition" => $k};
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};
76 push @phrases, {"-exists" => $subquery} if $something;
81 sub prepare_acqlia_search_or {
85 my $result = {"+acqlia" => {"-or" => $point}};
87 foreach my $unit (@$acqlia) {
88 my ($k, $v, $fuzzy, $between) = 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}
114 my $key = (grep { !/^__/ } keys %$term)[0];
117 $term->{"__fuzzy"} ? 1 : 0,
118 $term->{"__between"} ? 1 : 0
122 sub get_fm_links_by_hint {
124 foreach my $field (values %{$Fieldmapper::fieldmap}) {
125 return $field->{"links"} if $field->{"hint"} eq $hint;
131 my ($value, $n) = @_;
138 "first_given_name" => $value,
139 "second_given_name" => $value,
140 "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 # alias, given names and family name.
152 sub prepare_au_terms {
153 my ($terms, $join_num) = @_;
157 foreach my $conj (qw/-and -or/) {
158 next unless exists $terms->{$conj};
160 my @new_outer_terms = ();
161 foreach my $hint_unit (@{$terms->{$conj}}) {
162 my $hint = (keys %$hint_unit)[0];
163 (my $plain_hint = $hint) =~ y/+//d;
165 if (my $links = get_fm_links_by_hint($plain_hint) and
166 $plain_hint ne "acqlia") {
168 foreach my $pair (@{$hint_unit->{$hint}}) {
169 my ($attr, $value) = breakdown_term($pair);
170 if ($links->{$attr} and
171 $links->{$attr}->{"class"} eq "au") {
172 push @joins, [$plain_hint, $attr, $join_num];
173 push @new_outer_terms, gen_au_term($value, $join_num);
176 push @new_terms, $pair;
180 $hint_unit->{$hint} = [ @new_terms ];
182 delete $hint_unit->{$hint};
185 push @new_outer_terms, $hint_unit if scalar keys %$hint_unit;
187 $terms->{$conj} = [ @new_outer_terms ];
193 my ($terms, $is_and) = @_;
195 my $conj = $is_and ? "-and" : "-or";
196 my $outer_clause = {};
198 foreach my $class (qw/acqpo acqpl jub/) {
199 next if not exists $terms->{$class};
202 $outer_clause->{$conj} = [] unless $outer_clause->{$conj};
203 foreach my $unit (@{$terms->{$class}}) {
204 my ($k, $v, $fuzzy, $between) = breakdown_term($unit);
205 if ($fuzzy and not ref $v) {
206 push @$clause, {$k => {"ilike" => "%" . $v . "%"}};
207 } elsif ($between and could_be_range($v)) {
208 push @$clause, {$k => {"between" => $v}};
209 } elsif (check_1d_max($v)) {
210 push @$clause, {$k => $v};
213 push @{$outer_clause->{$conj}}, {"+" . $class => $clause};
216 if ($terms->{"acqlia"}) {
217 push @{$outer_clause->{$conj}},
218 $is_and ? prepare_acqlia_search_and($terms->{"acqlia"}) :
219 prepare_acqlia_search_or($terms->{"acqlia"});
222 return undef unless scalar keys %$outer_clause;
230 foreach my $join (@_) {
231 my ($hint, $attr, $num) = @$join;
232 my $start = $hint eq "jub" ? $from->{$hint} : $from->{"jub"}->{$hint};
247 if ($hint eq "jub") {
248 $start->{"au$num"} = $clause;
250 $start->{"join"} ||= {};
251 $start->{"join"}->{"au$num"} = $clause;
258 __PACKAGE__->register_method(
259 method => "unified_search",
260 api_name => "open-ils.acq.lineitem.unified_search",
263 desc => q/Returns lineitems based on flexible search terms./,
265 {desc => "Authentication token", type => "string"},
266 {desc => "Field/value pairs for AND'ing", type => "object"},
267 {desc => "Field/value pairs for OR'ing", type => "object"},
268 {desc => "Conjunction between AND pairs and OR pairs " .
269 "(can be 'and' or 'or')", type => "string"},
270 {desc => "Retrieval options (clear_marc, flesh_notes, etc) " .
271 "- XXX detail all the options",
274 return => {desc => "A stream of LIs on success, Event on failure"}
278 __PACKAGE__->register_method(
279 method => "unified_search",
280 api_name => "open-ils.acq.purchase_order.unified_search",
283 desc => q/Returns purchase orders based on flexible search terms.
284 See open-ils.acq.lineitem.unified_search/,
285 return => {desc => "A stream of POs on success, Event on failure"}
289 __PACKAGE__->register_method(
290 method => "unified_search",
291 api_name => "open-ils.acq.picklist.unified_search",
294 desc => q/Returns pick lists based on flexible search terms.
295 See open-ils.acq.lineitem.unified_search/,
296 return => {desc => "A stream of PLs on success, Event on failure"}
301 my ($self, $conn, $auth, $and_terms, $or_terms, $conj, $options) = @_;
304 my $e = new_editor("authtoken" => $auth);
305 return $e->die_event unless $e->checkauth;
307 # What kind of object are we returning? Important: (\w+) had better be
308 # a legit acq classname particle, so don't register any crazy api_names.
309 my $ret_type = ($self->api_name =~ /cq.(\w+).un/)[0];
310 my $retriever = $RETRIEVERS{$ret_type};
311 my $hint = F("acq::$ret_type")->{"hint"};
316 [{"column" => "id", "transform" => "distinct"}]
323 "fkey" => "purchase_order"
332 "order_by" => { $hint => {"id" => {}}},
333 "offset" => ($options->{"offset"} || 0)
336 $query->{"limit"} = $options->{"limit"} if $options->{"limit"};
338 $and_terms = prepare_terms($and_terms, 1);
339 $or_terms = prepare_terms($or_terms, 0) and do {
340 $query->{"from"}->{"jub"}->{"acqlia"} = {
341 "type" => "left", "field" => "lineitem", "fkey" => "id",
345 my $offset = add_au_joins($query->{"from"}, prepare_au_terms($and_terms));
346 add_au_joins($query->{"from"}, prepare_au_terms($or_terms, $offset));
348 if ($and_terms and $or_terms) {
349 $query->{"where"} = {
350 "-" . (lc $conj eq "or" ? "or" : "and") => [$and_terms, $or_terms]
352 } elsif ($and_terms) {
353 $query->{"where"} = $and_terms;
354 } elsif ($or_terms) {
355 $query->{"where"} = $or_terms;
358 return new OpenILS::Event("BAD_PARAMS", "desc" => "No usable terms");
361 my $results = $e->json_query($query) or return $e->die_event;
362 $conn->respond($retriever->($e, $_->{"id"}, $options)) foreach (@$results);