1 package OpenILS::Application::Acq::Search;
2 use base "OpenILS::Application";
7 use OpenSRF::AppSession;
9 use OpenILS::Utils::CStoreEditor q/:funcs/;
10 use OpenILS::Utils::Fieldmapper;
11 use OpenILS::Application::Acq::Lineitem;
12 use OpenILS::Application::Acq::Financials;
13 use OpenILS::Application::Acq::Picklist;
14 use OpenILS::Application::Acq::Invoice;
15 use OpenILS::Application::Acq::Order;
19 \&{"OpenILS::Application::Acq::Lineitem::retrieve_lineitem_impl"},
21 \&{"OpenILS::Application::Acq::Picklist::retrieve_picklist_impl"},
22 "purchase_order" => \&{
23 "OpenILS::Application::Acq::Financials::retrieve_purchase_order_impl"
26 "OpenILS::Application::Acq::Invoice::fetch_invoice_impl"
30 sub F { $Fieldmapper::fieldmap->{"Fieldmapper::" . $_[0]}; }
32 # This subroutine returns 1 if the argument is a) a scalar OR
33 # b) an array of ONLY scalars. Otherwise it returns 0.
36 return 1 unless ref $o;
37 if (ref($o) eq "ARRAY") {
38 foreach (@$o) { return 0 if ref $_; }
44 # Returns 1 if and only if argument is an array of exactly two scalars.
47 if (ref $o eq "ARRAY") {
48 return 1 if (scalar(@$o) == 2 && (!ref $o->[0] && !ref $o->[1]));
54 my ($value, $gte, $lte) = @_;
60 +{$op => {"transform" => "date", "value" => $value}};
63 sub prepare_acqlia_search_and {
67 foreach my $unit (@{$acqlia}) {
69 "select" => {"acqlia" => ["id"]},
71 "where" => {"-and" => [{"lineitem" => {"=" => {"+jub" => "id"}}}]}
74 # castdate not supported for acqlia fields: they're all type text
75 my ($k, $v, $fuzzy, $between, $not) = breakdown_term($unit);
76 my $point = $subquery->{"where"}->{"-and"};
79 push @$point, {"definition" => $k};
81 if ($fuzzy and not ref $v) {
82 push @$point, {"attr_value" => {"ilike" => "%" . $v . "%"}};
83 } elsif ($between and could_be_range($v)) {
84 push @$point, {"attr_value" => {"between" => $v}};
85 } elsif (check_1d_max($v)) {
86 push @$point, {"attr_value" => $v};
91 my $operator = $not ? "-not-exists" : "-exists";
92 push @phrases, {$operator => $subquery};
97 sub prepare_acqlia_search_or {
101 my $result = {"+acqlia" => {"-or" => $point}};
103 foreach my $unit (@$acqlia) {
104 # castdate not supported for acqlia fields: they're all type text
105 my ($k, $v, $fuzzy, $between, $not) = breakdown_term($unit);
107 if ($fuzzy and not ref $v) {
111 "attr_value" => {"ilike" => "%" . $v . "%"}
114 } elsif ($between and could_be_range($v)) {
117 "definition" => $k, "attr_value" => {"between" => $v}
120 } elsif (check_1d_max($v)) {
122 "-and" => {"definition" => $k, "attr_value" => $v}
128 push @$point, $not ? {"-not" => $term_clause} : $term_clause;
136 my $key = (grep { !/^__/ } keys %$term)[0];
139 $term->{"__fuzzy"} ? 1 : 0,
140 $term->{"__between"} ? 1 : 0,
141 $term->{"__not"} ? 1 : 0,
142 $term->{"__castdate"} ? 1 : 0,
143 $term->{"__gte"} ? 1 : 0,
144 $term->{"__lte"} ? 1 : 0
148 sub get_fm_links_by_hint {
150 foreach my $field (values %{$Fieldmapper::fieldmap}) {
151 return $field->{"links"} if $field->{"hint"} eq $hint;
157 my ($value, $n) = @_;
160 {"+au$n" => {"usrname" => $value}},
161 {"+au$n" => {"first_given_name" => $value}},
162 {"+au$n" => {"second_given_name" => $value}},
163 {"+au$n" => {"family_name" => $value}},
164 {"+ac$n" => {"barcode" => $value}}
169 # go through the terms hash, find keys that correspond to fields links
170 # to actor.usr, and rewrite the search as one that searches not by
171 # actor.usr.id but by any of these user properties: card barcode, username,
172 # given names and family name.
173 sub prepare_au_terms {
174 my ($terms, $join_num) = @_;
180 foreach my $conj (qw/-and -or/) {
181 next unless exists $terms->{$conj};
183 my @new_outer_terms = ();
184 HINT_UNIT: foreach my $hint_unit (@{$terms->{$conj}}) {
185 my $hint = (keys %$hint_unit)[0];
186 (my $plain_hint = $hint) =~ y/+//d;
187 if ($hint eq "-not") {
188 $hint_unit = $hint_unit->{$hint};
193 if (my $links = get_fm_links_by_hint($plain_hint) and
194 $plain_hint ne "acqlia") {
196 my ($attr, $value) = breakdown_term($hint_unit->{$hint});
197 if ($links->{$attr} and
198 $links->{$attr}->{"class"} eq "au") {
199 push @joins, [$plain_hint, $attr, $join_num];
200 my $au_term = gen_au_term($value, $join_num);
202 $au_term = {"-not" => $au_term};
205 push @new_outer_terms, $au_term;
207 delete $hint_unit->{$hint};
211 $hint_unit = {"-not" => $hint_unit};
214 push @new_outer_terms, $hint_unit if scalar keys %$hint_unit;
216 $terms->{$conj} = [ @new_outer_terms ];
222 my ($terms, $is_and) = @_;
224 my $conj = $is_and ? "-and" : "-or";
225 my $outer_clause = {};
227 foreach my $class (qw/acqpo acqpl acqinv jub/) {
228 next if not exists $terms->{$class};
230 $outer_clause->{$conj} = [] unless $outer_clause->{$conj};
231 foreach my $unit (@{$terms->{$class}}) {
232 my ($k, $v, $fuzzy, $between, $not, $castdate, $gte, $lte) =
233 breakdown_term($unit);
236 if ($fuzzy and not ref $v) {
237 $term_clause = {$k => {"ilike" => "%" . $v . "%"}};
238 } elsif ($between and could_be_range($v)) {
239 $term_clause = {$k => {"between" => $v}};
240 } elsif (check_1d_max($v)) {
241 $v = castdate($v, $gte, $lte) if $castdate;
242 $term_clause = {$k => $v};
247 my $clause = {"+" . $class => $term_clause};
248 $clause = {"-not" => $clause} if $not;
249 push @{$outer_clause->{$conj}}, $clause;
253 if ($terms->{"acqlia"}) {
254 push @{$outer_clause->{$conj}},
255 $is_and ? prepare_acqlia_search_and($terms->{"acqlia"}) :
256 prepare_acqlia_search_or($terms->{"acqlia"});
259 return undef unless scalar keys %$outer_clause;
267 foreach my $join (@_) {
268 my ($hint, $attr, $num) = @$join;
270 if ($hint eq "jub") {
271 $start = $from->{$hint};
272 } elsif ($hint eq "acqinv") {
273 $start = $from->{"jub"}->{"acqie"}->{"join"}->{$hint};
275 $start = $from->{"jub"}->{$hint};
291 if ($hint eq "jub") {
292 $start->{"au$num"} = $clause;
294 $start->{"join"} ||= {};
295 $start->{"join"}->{"au$num"} = $clause;
302 __PACKAGE__->register_method(
303 method => "unified_search",
304 api_name => "open-ils.acq.lineitem.unified_search",
307 desc => q/Returns lineitems based on flexible search terms./,
309 {desc => "Authentication token", type => "string"},
310 {desc => "Field/value pairs for AND'ing", type => "object"},
311 {desc => "Field/value pairs for OR'ing", type => "object"},
312 {desc => "Conjunction between AND pairs and OR pairs " .
313 "(can be 'and' or 'or')", type => "string"},
314 {desc => "Retrieval options (clear_marc, flesh_notes, etc) " .
315 "- XXX detail all the options",
318 return => {desc => "A stream of LIs on success, Event on failure"}
322 __PACKAGE__->register_method(
323 method => "unified_search",
324 api_name => "open-ils.acq.purchase_order.unified_search",
327 desc => q/Returns purchase orders based on flexible search terms.
328 See open-ils.acq.lineitem.unified_search/,
329 return => {desc => "A stream of POs on success, Event on failure"}
333 __PACKAGE__->register_method(
334 method => "unified_search",
335 api_name => "open-ils.acq.picklist.unified_search",
338 desc => q/Returns pick lists based on flexible search terms.
339 See open-ils.acq.lineitem.unified_search/,
340 return => {desc => "A stream of PLs on success, Event on failure"}
344 __PACKAGE__->register_method(
345 method => "unified_search",
346 api_name => "open-ils.acq.invoice.unified_search",
349 desc => q/Returns invoices lists based on flexible search terms.
350 See open-ils.acq.lineitem.unified_search/,
351 return => {desc => "A stream of invoices on success, Event on failure"}
356 my ($self, $conn, $auth, $and_terms, $or_terms, $conj, $options) = @_;
359 my $e = new_editor("authtoken" => $auth);
360 return $e->die_event unless $e->checkauth;
362 # What kind of object are we returning? Important: (\w+) had better be
363 # a legit acq classname particle, so don't register any crazy api_names.
364 my $ret_type = ($self->api_name =~ /cq.(\w+).un/)[0];
365 my $retriever = $RETRIEVERS{$ret_type};
366 my $hint = F("acq::$ret_type")->{"hint"};
369 "select" => {$hint => [{"column" => "id", "transform" => "distinct"}]},
375 "fkey" => "purchase_order"
384 "field" => "lineitem",
396 "order_by" => {$hint => {"id" => {}}},
397 "offset" => ($options->{"offset"} || 0)
400 $query->{"limit"} = $options->{"limit"} if $options->{"limit"};
402 # XXX for the future? but it doesn't quite work as is.
403 # # Remove anything in temporary picklists from search results.
405 # $and_terms->{"acqpl"} ||= [];
406 # push @{$and_terms->{"acqpl"}}, {"name" => "", "__not" => 1};
408 $and_terms = prepare_terms($and_terms, 1);
409 $or_terms = prepare_terms($or_terms, 0) and do {
410 $query->{"from"}->{"jub"}->{"acqlia"} = {
411 "type" => "left", "field" => "lineitem", "fkey" => "id",
415 my $offset = add_au_joins($query->{"from"}, prepare_au_terms($and_terms));
416 add_au_joins($query->{"from"}, prepare_au_terms($or_terms, $offset));
418 if ($and_terms and $or_terms) {
419 $query->{"where"} = {
420 "-" . (lc $conj eq "or" ? "or" : "and") => [$and_terms, $or_terms]
422 } elsif ($and_terms) {
423 $query->{"where"} = $and_terms;
424 } elsif ($or_terms) {
425 $query->{"where"} = $or_terms;
428 return new OpenILS::Event("BAD_PARAMS", "desc" => "No usable terms");
431 my $results = $e->json_query($query) or return $e->die_event;
432 if ($options->{"id_list"}) {
433 $conn->respond($_->{"id"}) foreach (grep { $_->{"id"} } @$results);
435 $conn->respond($retriever->($e, $_->{"id"}, $options))
436 foreach (grep { $_->{"id"} } @$results);
443 __PACKAGE__->register_method(
444 method => "bib_search",
445 api_name => "open-ils.acq.biblio.wrapped_search",
448 desc => q/Returns new lineitems for each matching bib record/,
450 {desc => "Authentication token", type => "string"},
451 {desc => "search string", type => "string"},
452 {desc => "search options", type => "object"}
454 return => {desc => "A stream of LIs on success, Event on failure"}
458 # This is very similar to zsearch() in Order.pm
460 my ($self, $conn, $auth, $search, $options) = @_;
462 my $e = new_editor("authtoken" => $auth, "xact" => 1);
463 return $e->die_event unless $e->checkauth;
464 return $e->die_event unless $e->allowed("CREATE_PICKLIST");
466 my $mgr = new OpenILS::Application::Acq::BatchManager(
467 "editor" => $e, "conn" => $conn
471 $options->{"limit"} ||= 10;
473 my $ses = create OpenSRF::AppSession("open-ils.search");
474 my $req = $ses->request(
475 "open-ils.search.biblio.multiclass.query.staff", $options, $search
481 while (my $resp = $req->recv("timeout" => 60)) {
482 $picklist = OpenILS::Application::Acq::Order::zsearch_build_pl(
483 $mgr, undef # XXX could have per-user name for temp picklist here?
486 my $result = $resp->content;
487 next if not ref $result;
489 # The result object contains a whole heck of a lot more information
490 # than just bib IDs, so maybe we could tell the client something
491 # useful (progress meter at least) in the future...
494 OpenILS::Application::Acq::Order::create_lineitem(
496 "picklist" => $picklist->id,
497 "source_label" => "native-evergreen-catalog",
498 "marc" => $e->retrieve_biblio_record_entry($bib)->marc,
501 } (@{$result->{"ids"}});
507 # new editor, no transaction needed this time
508 $e = new_editor("authtoken" => $auth) or return $e->die_event;
509 return $e->die_event unless $e->checkauth;
510 $conn->respond($RETRIEVERS{"lineitem"}->($e, $_, $options)) foreach @li_ids;