1 package OpenILS::Application::Acq::Search;
2 use base "OpenILS::Application";
7 use OpenSRF::AppSession;
8 use OpenSRF::Utils::Logger qw/:logger/;
10 use OpenILS::Utils::CStoreEditor q/:funcs/;
11 use OpenILS::Utils::Fieldmapper;
12 use OpenILS::Application::Acq::Lineitem;
13 use OpenILS::Application::Acq::Financials;
14 use OpenILS::Application::Acq::Picklist;
15 use OpenILS::Application::Acq::Invoice;
16 use OpenILS::Application::Acq::Order;
20 \&{"OpenILS::Application::Acq::Lineitem::retrieve_lineitem_impl"},
22 \&{"OpenILS::Application::Acq::Picklist::retrieve_picklist_impl"},
23 "purchase_order" => \&{
24 "OpenILS::Application::Acq::Financials::retrieve_purchase_order_impl"
27 "OpenILS::Application::Acq::Invoice::fetch_invoice_impl"
31 sub F { $Fieldmapper::fieldmap->{"Fieldmapper::" . $_[0]}; }
33 # This subroutine returns 1 if the argument is a) a scalar OR
34 # b) an array of ONLY scalars. Otherwise it returns 0.
37 return 1 unless ref $o;
38 if (ref($o) eq "ARRAY") {
39 foreach (@$o) { return 0 if ref $_; }
45 # Returns 1 if and only if argument is an array of exactly two scalars.
48 if (ref $o eq "ARRAY") {
49 return 1 if (scalar(@$o) == 2 && (!ref $o->[0] && !ref $o->[1]));
55 my ($value, $gte, $lte) = @_;
61 +{$op => {"transform" => "date", "value" => $value}};
64 sub prepare_acqlia_search_and {
68 foreach my $unit (@{$acqlia}) {
70 "select" => {"acqlia" => ["id"]},
72 "where" => {"-and" => [{"lineitem" => {"=" => {"+jub" => "id"}}}]}
75 # castdate not supported for acqlia fields: they're all type text
76 my ($k, $v, $fuzzy, $between, $not) = breakdown_term($unit);
77 my $point = $subquery->{"where"}->{"-and"};
80 push @$point, {"definition" => $k};
82 if ($fuzzy and not ref $v) {
83 push @$point, {"attr_value" => {"ilike" => "%" . $v . "%"}};
84 } elsif ($between and could_be_range($v)) {
85 push @$point, {"attr_value" => {"between" => $v}};
86 } elsif (check_1d_max($v)) {
87 push @$point, {"attr_value" => $v};
92 my $operator = $not ? "-not-exists" : "-exists";
93 push @phrases, {$operator => $subquery};
98 sub prepare_acqlia_search_or {
102 my $result = {"+acqlia" => {"-or" => $point}};
104 foreach my $unit (@$acqlia) {
105 # castdate not supported for acqlia fields: they're all type text
106 my ($k, $v, $fuzzy, $between, $not) = breakdown_term($unit);
108 if ($fuzzy and not ref $v) {
112 "attr_value" => {"ilike" => "%" . $v . "%"}
115 } elsif ($between and could_be_range($v)) {
118 "definition" => $k, "attr_value" => {"between" => $v}
121 } elsif (check_1d_max($v)) {
123 "-and" => {"definition" => $k, "attr_value" => $v}
129 push @$point, $not ? {"-not" => $term_clause} : $term_clause;
137 my $key = (grep { !/^__/ } keys %$term)[0];
140 $term->{"__fuzzy"} ? 1 : 0,
141 $term->{"__between"} ? 1 : 0,
142 $term->{"__not"} ? 1 : 0,
143 $term->{"__castdate"} ? 1 : 0,
144 $term->{"__gte"} ? 1 : 0,
145 $term->{"__lte"} ? 1 : 0
149 sub get_fm_links_by_hint {
151 foreach my $field (values %{$Fieldmapper::fieldmap}) {
152 return $field->{"links"} if $field->{"hint"} eq $hint;
158 my ($value, $n) = @_;
161 {"+au$n" => {"usrname" => $value}},
162 {"+au$n" => {"first_given_name" => $value}},
163 {"+au$n" => {"second_given_name" => $value}},
164 {"+au$n" => {"family_name" => $value}},
165 {"+ac$n" => {"barcode" => $value}}
170 # go through the terms hash, find keys that correspond to fields links
171 # to actor.usr, and rewrite the search as one that searches not by
172 # actor.usr.id but by any of these user properties: card barcode, username,
173 # given names and family name.
174 sub prepare_au_terms {
175 my ($terms, $join_num) = @_;
181 foreach my $conj (qw/-and -or/) {
182 next unless exists $terms->{$conj};
184 my @new_outer_terms = ();
185 HINT_UNIT: foreach my $hint_unit (@{$terms->{$conj}}) {
186 my $hint = (keys %$hint_unit)[0];
187 (my $plain_hint = $hint) =~ y/+//d;
188 if ($hint eq "-not") {
189 $hint_unit = $hint_unit->{$hint};
194 if (my $links = get_fm_links_by_hint($plain_hint) and
195 $plain_hint ne "acqlia") {
197 my ($attr, $value) = breakdown_term($hint_unit->{$hint});
198 if ($links->{$attr} and
199 $links->{$attr}->{"class"} eq "au") {
200 push @joins, [$plain_hint, $attr, $join_num];
201 my $au_term = gen_au_term($value, $join_num);
203 $au_term = {"-not" => $au_term};
206 push @new_outer_terms, $au_term;
208 delete $hint_unit->{$hint};
212 $hint_unit = {"-not" => $hint_unit};
215 push @new_outer_terms, $hint_unit if scalar keys %$hint_unit;
217 $terms->{$conj} = [ @new_outer_terms ];
223 my ($terms, $is_and) = @_;
225 my $conj = $is_and ? "-and" : "-or";
226 my $outer_clause = {};
228 foreach my $class (qw/acqpo acqpl acqinv jub/) {
229 next if not exists $terms->{$class};
231 $outer_clause->{$conj} = [] unless $outer_clause->{$conj};
232 foreach my $unit (@{$terms->{$class}}) {
233 my ($k, $v, $fuzzy, $between, $not, $castdate, $gte, $lte) =
234 breakdown_term($unit);
237 if ($fuzzy and not ref $v) {
238 $term_clause = {$k => {"ilike" => "%" . $v . "%"}};
239 } elsif ($between and could_be_range($v)) {
240 $term_clause = {$k => {"between" => $v}};
241 } elsif (check_1d_max($v)) {
242 $v = castdate($v, $gte, $lte) if $castdate;
243 $term_clause = {$k => $v};
248 my $clause = {"+" . $class => $term_clause};
249 $clause = {"-not" => $clause} if $not;
250 push @{$outer_clause->{$conj}}, $clause;
254 if ($terms->{"acqlia"}) {
255 push @{$outer_clause->{$conj}},
256 $is_and ? prepare_acqlia_search_and($terms->{"acqlia"}) :
257 prepare_acqlia_search_or($terms->{"acqlia"});
260 return undef unless scalar keys %$outer_clause;
268 foreach my $join (@_) {
269 my ($hint, $attr, $num) = @$join;
271 if ($hint eq "jub") {
272 $start = $from->{$hint};
273 } elsif ($hint eq "acqinv") {
274 $start = $from->{"jub"}->{"acqie"}->{"join"}->{$hint};
276 $start = $from->{"jub"}->{$hint};
292 if ($hint eq "jub") {
293 $start->{"au$num"} = $clause;
295 $start->{"join"} ||= {};
296 $start->{"join"}->{"au$num"} = $clause;
303 __PACKAGE__->register_method(
304 method => "unified_search",
305 api_name => "open-ils.acq.lineitem.unified_search",
308 desc => q/Returns lineitems based on flexible search terms./,
310 {desc => "Authentication token", type => "string"},
311 {desc => "Field/value pairs for AND'ing", type => "object"},
312 {desc => "Field/value pairs for OR'ing", type => "object"},
313 {desc => "Conjunction between AND pairs and OR pairs " .
314 "(can be 'and' or 'or')", type => "string"},
315 {desc => "Retrieval options (clear_marc, flesh_notes, etc) " .
316 "- XXX detail all the options",
319 return => {desc => "A stream of LIs on success, Event on failure"}
323 __PACKAGE__->register_method(
324 method => "unified_search",
325 api_name => "open-ils.acq.purchase_order.unified_search",
328 desc => q/Returns purchase orders based on flexible search terms.
329 See open-ils.acq.lineitem.unified_search/,
330 return => {desc => "A stream of POs on success, Event on failure"}
334 __PACKAGE__->register_method(
335 method => "unified_search",
336 api_name => "open-ils.acq.picklist.unified_search",
339 desc => q/Returns pick lists based on flexible search terms.
340 See open-ils.acq.lineitem.unified_search/,
341 return => {desc => "A stream of PLs on success, Event on failure"}
345 __PACKAGE__->register_method(
346 method => "unified_search",
347 api_name => "open-ils.acq.invoice.unified_search",
350 desc => q/Returns invoices lists based on flexible search terms.
351 See open-ils.acq.lineitem.unified_search/,
352 return => {desc => "A stream of invoices on success, Event on failure"}
357 my ($self, $conn, $auth, $and_terms, $or_terms, $conj, $options) = @_;
360 my $e = new_editor("authtoken" => $auth);
361 return $e->die_event unless $e->checkauth;
363 # What kind of object are we returning? Important: (\w+) had better be
364 # a legit acq classname particle, so don't register any crazy api_names.
365 my $ret_type = ($self->api_name =~ /cq.(\w+).un/)[0];
366 my $retriever = $RETRIEVERS{$ret_type};
367 my $hint = F("acq::$ret_type")->{"hint"};
369 my $select_clause = {
370 $hint => [{"column" => "id", "transform" => "distinct"}]
373 if ($options->{"order_by"}) {
374 # What's the point of this block? When using ORDER BY in conjuction
375 # with SELECT DISTINCT, the fields present in ORDER BY have to also
376 # be in the SELECT clause. This will take _one_ such field and add
377 # it to the SELECT clause as needed.
378 my ($order_by, $class, $field);
380 ($order_by = $options->{"order_by"}->[0]) &&
381 ($class = $order_by->{"class"}) =~ /^[\da-z_]+$/ &&
382 ($field = $order_by->{"field"}) =~ /^[\da-z_]+$/
385 return new OpenILS::Event(
386 "BAD_PARAMS", "note" =>
387 q/order_by clause must be of the long form, like:
388 "order_by": [{"class": "foo", "field": "bar", "direction": "asc"}]/
391 $select_clause->{$class} ||= [];
392 push @{$select_clause->{$class}}, $field;
397 "select" => $select_clause,
403 "fkey" => "purchase_order"
412 "field" => "lineitem",
424 "order_by" => ($options->{"order_by"} || {$hint => {"id" => {}}}),
425 "offset" => ($options->{"offset"} || 0)
428 $query->{"limit"} = $options->{"limit"} if $options->{"limit"};
430 # XXX for the future? but it doesn't quite work as is.
431 # # Remove anything in temporary picklists from search results.
433 # $and_terms->{"acqpl"} ||= [];
434 # push @{$and_terms->{"acqpl"}}, {"name" => "", "__not" => 1};
436 $and_terms = prepare_terms($and_terms, 1);
437 $or_terms = prepare_terms($or_terms, 0) and do {
438 $query->{"from"}->{"jub"}->{"acqlia"} = {
439 "type" => "left", "field" => "lineitem", "fkey" => "id",
443 my $offset = add_au_joins($query->{"from"}, prepare_au_terms($and_terms));
444 add_au_joins($query->{"from"}, prepare_au_terms($or_terms, $offset));
446 if ($and_terms and $or_terms) {
447 $query->{"where"} = {
448 "-" . (lc $conj eq "or" ? "or" : "and") => [$and_terms, $or_terms]
450 } elsif ($and_terms) {
451 $query->{"where"} = $and_terms;
452 } elsif ($or_terms) {
453 $query->{"where"} = $or_terms;
456 return new OpenILS::Event("BAD_PARAMS", "desc" => "No usable terms");
459 my $results = $e->json_query($query) or return $e->die_event;
460 my @id_list = map { $_->{"id"} } (grep { $_->{"id"} } @$results);
462 if ($options->{"id_list"}) {
463 $conn->respond($_) foreach @id_list;
465 $conn->respond($retriever->($e, $_, $options)) foreach @id_list;
472 __PACKAGE__->register_method(
473 method => "bib_search",
474 api_name => "open-ils.acq.biblio.wrapped_search",
477 desc => q/Returns new lineitems for each matching bib record/,
479 {desc => "Authentication token", type => "string"},
480 {desc => "search string", type => "string"},
481 {desc => "search options", type => "object"}
483 return => {desc => "A stream of LIs on success, Event on failure"}
487 __PACKAGE__->register_method(
488 method => "bib_search",
489 api_name => "open-ils.acq.biblio.create_by_id",
492 desc => q/Returns new lineitems for each matching bib record/,
494 {desc => "Authentication token", type => "string"},
495 {desc => "list of bib IDs", type => "array"},
496 {desc => "options (for lineitem fleshing)", type => "object"}
498 return => {desc => "A stream of LIs on success, Event on failure"}
502 # This is very similar to zsearch() in Order.pm
504 my ($self, $conn, $auth, $search, $opts) = @_;
506 my $e = new_editor("authtoken" => $auth, "xact" => 1);
507 return $e->die_event unless $e->checkauth;
508 return $e->die_event unless $e->allowed("CREATE_PICKLIST");
510 my $mgr = new OpenILS::Application::Acq::BatchManager(
511 "editor" => $e, "conn" => $conn
518 if ($self->api_name =~ /create_by_id/) {
519 $search = [ sort @$search ]; # for consitency
520 my $bibs = $e->search_biblio_record_entry(
521 {"id" => $search}, {"order_by" => {"bre" => ["id"]}}
522 ) or return $e->die_event;
524 if ($opts->{"reuse_picklist"}) {
525 $picklist = $e->retrieve_acq_picklist($opts->{"reuse_picklist"}) or
526 return $e->die_event;
527 return $e->die_event unless
528 $e->allowed("UPDATE_PICKLIST", $picklist->org_unit);
530 # If we're reusing an existing picklist, we don't need to
531 # create new lineitems for any bib records for which we already
533 my $already_have = $e->search_acq_lineitem({
534 "picklist" => $picklist->id,
535 "eg_bib_id" => [ map { $_->id } @$bibs ]
536 }) or return $e->die_event;
538 # So in that case we a) save the lineitem id's of the relevant
539 # items that already exist so that we can return those items later,
540 # and b) remove the bib id's in question from our list of bib
541 # id's to lineitemize.
542 if (@$already_have) {
543 push @li_ids, $_->id foreach (@$already_have);
545 foreach my $bib (@$bibs) {
546 push @new_bibs, $bib unless
547 grep { $_->eg_bib_id == $bib->id } @$already_have;
549 $bibs = [ @new_bibs ];
553 OpenILS::Application::Acq::Order::zsearch_build_pl($mgr, undef);
556 $conn->respond($picklist->id);
559 OpenILS::Application::Acq::Order::create_lineitem(
561 "picklist" => $picklist->id,
562 "source_label" => "native-evergreen-catalog",
564 "eg_bib_id" => $_->id
568 $opts->{"limit"} ||= 10;
570 my $ses = create OpenSRF::AppSession("open-ils.search");
571 my $req = $ses->request(
572 "open-ils.search.biblio.multiclass.query.staff", $opts, $search
576 while (my $resp = $req->recv("timeout" => 60)) {
577 $picklist = OpenILS::Application::Acq::Order::zsearch_build_pl(
581 my $result = $resp->content;
582 next if not ref $result;
584 # The result object contains a whole heck of a lot more information
585 # than just bib IDs, so maybe we could tell the client something
586 # useful (progress meter at least) in the future...
589 OpenILS::Application::Acq::Order::create_lineitem(
591 "picklist" => $picklist->id,
592 "source_label" => "native-evergreen-catalog",
593 "marc" => $e->retrieve_biblio_record_entry($bib)->marc,
596 } (@{$result->{"ids"}});
603 $logger->info("created @li_ids new lineitems for picklist $picklist");
605 # new editor, no transaction needed this time
606 $e = new_editor("authtoken" => $auth) or return $e->die_event;
607 return $e->die_event unless $e->checkauth;
608 $conn->respond($RETRIEVERS{"lineitem"}->($e, $_, $opts)) foreach @li_ids;