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) = @_;
160 "=" => { transform => "lowercase", value => lc($value) }
165 {"+au$n" => {"usrname" => $value}},
166 {"+au$n" => {"first_given_name" => $lc_value}},
167 {"+au$n" => {"second_given_name" => $lc_value}},
168 {"+au$n" => {"family_name" => $lc_value}},
169 {"+ac$n" => {"barcode" => $value}}
174 # go through the terms hash, find keys that correspond to fields links
175 # to actor.usr, and rewrite the search as one that searches not by
176 # actor.usr.id but by any of these user properties: card barcode, username,
177 # given names and family name.
178 sub prepare_au_terms {
179 my ($terms, $join_num) = @_;
185 foreach my $conj (qw/-and -or/) {
186 next unless exists $terms->{$conj};
188 my @new_outer_terms = ();
189 HINT_UNIT: foreach my $hint_unit (@{$terms->{$conj}}) {
190 my $hint = (keys %$hint_unit)[0];
191 (my $plain_hint = $hint) =~ y/+//d;
192 if ($hint eq "-not") {
193 $hint_unit = $hint_unit->{$hint};
198 if (my $links = get_fm_links_by_hint($plain_hint) and
199 $plain_hint ne "acqlia") {
201 my ($attr, $value) = breakdown_term($hint_unit->{$hint});
202 if ($links->{$attr} and
203 $links->{$attr}->{"class"} eq "au") {
204 push @joins, [$plain_hint, $attr, $join_num];
205 my $au_term = gen_au_term($value, $join_num);
207 $au_term = {"-not" => $au_term};
210 push @new_outer_terms, $au_term;
212 delete $hint_unit->{$hint};
216 $hint_unit = {"-not" => $hint_unit};
219 push @new_outer_terms, $hint_unit if scalar keys %$hint_unit;
221 $terms->{$conj} = [ @new_outer_terms ];
227 my ($terms, $is_and) = @_;
229 my $conj = $is_and ? "-and" : "-or";
230 my $outer_clause = {};
232 foreach my $class (qw/acqpo acqpl acqinv jub/) {
233 next if not exists $terms->{$class};
235 $outer_clause->{$conj} = [] unless $outer_clause->{$conj};
236 foreach my $unit (@{$terms->{$class}}) {
237 my ($k, $v, $fuzzy, $between, $not, $castdate, $gte, $lte) =
238 breakdown_term($unit);
241 if ($fuzzy and not ref $v) {
242 $term_clause = {$k => {"ilike" => "%" . $v . "%"}};
243 } elsif ($between and could_be_range($v)) {
244 $term_clause = {$k => {"between" => $v}};
245 } elsif (check_1d_max($v)) {
246 $v = castdate($v, $gte, $lte) if $castdate;
247 $term_clause = {$k => $v};
252 my $clause = {"+" . $class => $term_clause};
253 $clause = {"-not" => $clause} if $not;
254 push @{$outer_clause->{$conj}}, $clause;
258 if ($terms->{"acqlia"}) {
259 push @{$outer_clause->{$conj}},
260 $is_and ? prepare_acqlia_search_and($terms->{"acqlia"}) :
261 prepare_acqlia_search_or($terms->{"acqlia"});
264 return undef unless scalar keys %$outer_clause;
269 my $graft_map = shift;
270 my $core_hint = shift;
273 foreach my $join (@_) {
274 my ($hint, $attr, $num) = @$join;
275 my $start = $graft_map->{$hint};
291 if ($hint eq $core_hint) {
292 $start->{"au$num"} = $clause;
294 $start->{"join"} ||= {};
295 $start->{"join"}->{"au$num"} = $clause;
303 sub build_from_clause_and_joins {
304 my ($query, $core, $and_terms, $or_terms) = @_;
308 $graft_map{$core} = $query->{from}{$core} = {};
310 my $join_type = keys(%$or_terms) ? "left" : "inner";
312 my @classes = grep { $core ne $_ } (keys(%$and_terms), keys(%$or_terms));
313 my %classes_uniq = map { $_ => 1 } @classes;
314 @classes = keys(%classes_uniq);
316 my $acqlia_join = sub {
317 return {"type" => "left", "field" => "lineitem", "fkey" => "id"};
320 foreach my $class (@classes) {
321 if ($class eq 'acqlia') {
322 if ($core eq 'acqinv') {
324 $query->{from}{$core}{acqmapinv}{join}{jub}{join}{acqlia} =
326 } elsif ($core eq 'jub') {
328 $query->{from}{$core}{acqlia} =
332 $query->{from}{$core}{jub}{join}{acqlia} =
335 } elsif ($class eq 'acqinv' or $core eq 'acqinv') {
337 $query->{from}{$core}{acqmapinv}{join}{$class} ||= {};
338 $graft_map{$class}{type} = $join_type;
340 $graft_map{$class} = $query->{from}{$core}{$class} ||= {};
341 $graft_map{$class}{type} = $join_type;
348 __PACKAGE__->register_method(
349 method => "unified_search",
350 api_name => "open-ils.acq.lineitem.unified_search",
353 desc => q/Returns lineitems based on flexible search terms./,
355 {desc => "Authentication token", type => "string"},
356 {desc => "Field/value pairs for AND'ing", type => "object"},
357 {desc => "Field/value pairs for OR'ing", type => "object"},
358 {desc => "Conjunction between AND pairs and OR pairs " .
359 "(can be 'and' or 'or')", type => "string"},
360 {desc => "Retrieval options (clear_marc, flesh_notes, etc) " .
361 "- XXX detail all the options",
364 return => {desc => "A stream of LIs on success, Event on failure"}
368 __PACKAGE__->register_method(
369 method => "unified_search",
370 api_name => "open-ils.acq.purchase_order.unified_search",
373 desc => q/Returns purchase orders based on flexible search terms.
374 See open-ils.acq.lineitem.unified_search/,
375 return => {desc => "A stream of POs on success, Event on failure"}
379 __PACKAGE__->register_method(
380 method => "unified_search",
381 api_name => "open-ils.acq.picklist.unified_search",
384 desc => q/Returns pick lists based on flexible search terms.
385 See open-ils.acq.lineitem.unified_search/,
386 return => {desc => "A stream of PLs on success, Event on failure"}
390 __PACKAGE__->register_method(
391 method => "unified_search",
392 api_name => "open-ils.acq.invoice.unified_search",
395 desc => q/Returns invoices lists based on flexible search terms.
396 See open-ils.acq.lineitem.unified_search/,
397 return => {desc => "A stream of invoices on success, Event on failure"}
402 my ($self, $conn, $auth, $and_terms, $or_terms, $conj, $options) = @_;
405 my $e = new_editor("authtoken" => $auth);
406 return $e->die_event unless $e->checkauth;
408 # What kind of object are we returning? Important: (\w+) had better be
409 # a legit acq classname particle, so don't register any crazy api_names.
410 my $ret_type = ($self->api_name =~ /cq.(\w+).un/)[0];
411 my $retriever = $RETRIEVERS{$ret_type};
412 my $hint = F("acq::$ret_type")->{"hint"};
414 my $select_clause = {
415 $hint => [{"column" => "id", "transform" => "distinct"}]
418 if ($options->{"order_by"}) {
419 # What's the point of this block? When using ORDER BY in conjuction
420 # with SELECT DISTINCT, the fields present in ORDER BY have to also
421 # be in the SELECT clause. This will take _one_ such field and add
422 # it to the SELECT clause as needed.
423 my ($order_by, $class, $field);
425 ($order_by = $options->{"order_by"}->[0]) &&
426 ($class = $order_by->{"class"}) =~ /^[\da-z_]+$/ &&
427 ($field = $order_by->{"field"}) =~ /^[\da-z_]+$/
430 return new OpenILS::Event(
431 "BAD_PARAMS", "note" =>
432 q/order_by clause must be of the long form, like:
433 "order_by": [{"class": "foo", "field": "bar", "direction": "asc"}]/
436 $select_clause->{$class} ||= [];
437 push @{$select_clause->{$class}}, $field;
442 select => $select_clause,
443 order_by => ($options->{order_by} || {$hint => {id => {}}}),
444 offset => ($options->{offset} || 0)
447 $query->{"limit"} = $options->{"limit"} if $options->{"limit"};
449 my $graft_map = build_from_clause_and_joins(
450 $query, $hint, $and_terms, $or_terms
453 $and_terms = prepare_terms($and_terms, 1);
454 $or_terms = prepare_terms($or_terms, 0);
456 my $offset = add_au_joins($graft_map, $hint, prepare_au_terms($and_terms));
457 add_au_joins($graft_map, $hint, prepare_au_terms($or_terms, $offset));
459 if ($and_terms and $or_terms) {
460 $query->{"where"} = {
461 "-" . (lc $conj eq "or" ? "or" : "and") => [$and_terms, $or_terms]
463 } elsif ($and_terms) {
464 $query->{"where"} = $and_terms;
465 } elsif ($or_terms) {
466 $query->{"where"} = $or_terms;
469 return new OpenILS::Event("BAD_PARAMS", "desc" => "No usable terms");
472 my $results = $e->json_query($query) or return $e->die_event;
473 my @id_list = map { $_->{"id"} } (grep { $_->{"id"} } @$results);
475 if ($options->{"id_list"}) {
476 $conn->respond($_) foreach @id_list;
479 my $resp = $retriever->($e, $_, $options);
480 next if(ref($resp) ne "Fieldmapper::acq::$ret_type");
481 $conn->respond($resp);
489 __PACKAGE__->register_method(
490 method => "bib_search",
491 api_name => "open-ils.acq.biblio.wrapped_search",
494 desc => q/Returns new lineitems for each matching bib record/,
496 {desc => "Authentication token", type => "string"},
497 {desc => "search string", type => "string"},
498 {desc => "search options", type => "object"}
500 return => {desc => "A stream of LIs on success, Event on failure"}
504 __PACKAGE__->register_method(
505 method => "bib_search",
506 api_name => "open-ils.acq.biblio.create_by_id",
509 desc => q/Returns new lineitems for each matching bib record/,
511 {desc => "Authentication token", type => "string"},
512 {desc => "list of bib IDs", type => "array"},
513 {desc => "options (for lineitem fleshing)", type => "object"}
515 return => {desc => "A stream of LIs on success, Event on failure"}
519 # This is very similar to zsearch() in Order.pm
521 my ($self, $conn, $auth, $search, $opts) = @_;
523 my $e = new_editor("authtoken" => $auth, "xact" => 1);
524 return $e->die_event unless $e->checkauth;
525 return $e->die_event unless $e->allowed("CREATE_PICKLIST");
527 my $mgr = new OpenILS::Application::Acq::BatchManager(
528 "editor" => $e, "conn" => $conn
535 if ($self->api_name =~ /create_by_id/) {
536 $search = [ sort @$search ]; # for consitency
537 my $bibs = $e->search_biblio_record_entry(
538 {"id" => $search}, {"order_by" => {"bre" => ["id"]}}
539 ) or return $e->die_event;
541 if ($opts->{"reuse_picklist"}) {
542 $picklist = $e->retrieve_acq_picklist($opts->{"reuse_picklist"}) or
543 return $e->die_event;
544 return $e->die_event unless
545 $e->allowed("UPDATE_PICKLIST", $picklist->org_unit);
547 # If we're reusing an existing picklist, we don't need to
548 # create new lineitems for any bib records for which we already
550 my $already_have = $e->search_acq_lineitem({
551 "picklist" => $picklist->id,
552 "eg_bib_id" => [ map { $_->id } @$bibs ]
553 }) or return $e->die_event;
555 # So in that case we a) save the lineitem id's of the relevant
556 # items that already exist so that we can return those items later,
557 # and b) remove the bib id's in question from our list of bib
558 # id's to lineitemize.
559 if (@$already_have) {
560 push @li_ids, $_->id foreach (@$already_have);
562 foreach my $bib (@$bibs) {
563 push @new_bibs, $bib unless
564 grep { $_->eg_bib_id == $bib->id } @$already_have;
566 $bibs = [ @new_bibs ];
569 $picklist = OpenILS::Application::Acq::Order::zsearch_build_pl($mgr, undef)
570 or return $e->die_event;
573 $conn->respond($picklist->id);
576 OpenILS::Application::Acq::Order::create_lineitem(
578 "picklist" => $picklist->id,
579 "source_label" => "native-evergreen-catalog",
581 "eg_bib_id" => $_->id
585 $opts->{"limit"} ||= 10;
587 my $ses = create OpenSRF::AppSession("open-ils.search");
588 my $req = $ses->request(
589 "open-ils.search.biblio.multiclass.query.staff", $opts, $search
593 while (my $resp = $req->recv("timeout" => 60)) {
594 $picklist = OpenILS::Application::Acq::Order::zsearch_build_pl(
598 my $result = $resp->content;
599 next if not ref $result;
601 # The result object contains a whole heck of a lot more information
602 # than just bib IDs, so maybe we could tell the client something
603 # useful (progress meter at least) in the future...
606 OpenILS::Application::Acq::Order::create_lineitem(
608 "picklist" => $picklist->id,
609 "source_label" => "native-evergreen-catalog",
610 "marc" => $e->retrieve_biblio_record_entry($bib)->marc,
613 } (@{$result->{"ids"}});
620 $logger->info("created @li_ids new lineitems for picklist $picklist");
622 # new editor, but still using transaction to ensure correct retrieval
623 # in a replicated setup
624 $e = new_editor("authtoken" => $auth, xact => 1) or return $e->die_event;
625 return $e->die_event unless $e->checkauth;
626 $conn->respond($RETRIEVERS{"lineitem"}->($e, $_, $opts)) foreach @li_ids;