]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Acq/Search.pm
Acq: import file of bib IDs, get paginated LI table from which make new orders
[working/Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Acq / Search.pm
1 package OpenILS::Application::Acq::Search;
2 use base "OpenILS::Application";
3
4 use strict;
5 use warnings;
6
7 use OpenSRF::AppSession;
8 use OpenSRF::Utils::Logger qw/:logger/;
9 use OpenILS::Event;
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;
17
18 my %RETRIEVERS = (
19     "lineitem" =>
20         \&{"OpenILS::Application::Acq::Lineitem::retrieve_lineitem_impl"},
21     "picklist" =>
22         \&{"OpenILS::Application::Acq::Picklist::retrieve_picklist_impl"},
23     "purchase_order" => \&{
24         "OpenILS::Application::Acq::Financials::retrieve_purchase_order_impl"
25     },
26     "invoice" => \&{
27         "OpenILS::Application::Acq::Invoice::fetch_invoice_impl"
28     },
29 );
30
31 sub F { $Fieldmapper::fieldmap->{"Fieldmapper::" . $_[0]}; }
32
33 # This subroutine returns 1 if the argument is a) a scalar OR
34 # b) an array of ONLY scalars. Otherwise it returns 0.
35 sub check_1d_max {
36     my ($o) = @_;
37     return 1 unless ref $o;
38     if (ref($o) eq "ARRAY") {
39         foreach (@$o) { return 0 if ref $_; }
40         return 1;
41     }
42     0;
43 }
44
45 # Returns 1 if and only if argument is an array of exactly two scalars.
46 sub could_be_range {
47     my ($o) = @_;
48     if (ref $o eq "ARRAY") {
49         return 1 if (scalar(@$o) == 2 && (!ref $o->[0] && !ref $o->[1]));
50     }
51     0;
52 }
53
54 sub castdate {
55     my ($value, $gte, $lte) = @_;
56
57     my $op = "=";
58     $op = ">=" if $gte;
59     $op = "<=" if $lte;
60
61     +{$op => {"transform" => "date", "value" => $value}};
62 }
63
64 sub prepare_acqlia_search_and {
65     my ($acqlia) = @_;
66
67     my @phrases = ();
68     foreach my $unit (@{$acqlia}) {
69         my $subquery = {
70             "select" => {"acqlia" => ["id"]},
71             "from" => "acqlia",
72             "where" => {"-and" => [{"lineitem" => {"=" => {"+jub" => "id"}}}]}
73         };
74
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"};
78         my $term_clause;
79
80         push @$point, {"definition" => $k};
81
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};
88         } else {
89             next;
90         }
91
92         my $operator = $not ? "-not-exists" : "-exists";
93         push @phrases, {$operator => $subquery};
94     }
95     @phrases;
96 }
97
98 sub prepare_acqlia_search_or {
99     my ($acqlia) = @_;
100
101     my $point = [];
102     my $result = {"+acqlia" => {"-or" => $point}};
103
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);
107         my $term_clause;
108         if ($fuzzy and not ref $v) {
109             $term_clause = {
110                 "-and" => {
111                     "definition" => $k,
112                     "attr_value" => {"ilike" => "%" . $v . "%"}
113                 }
114             };
115         } elsif ($between and could_be_range($v)) {
116             $term_clause = {
117                 "-and" => {
118                     "definition" => $k, "attr_value" => {"between" => $v}
119                 }
120             };
121         } elsif (check_1d_max($v)) {
122             $term_clause = {
123                 "-and" => {"definition" => $k, "attr_value" => $v}
124             };
125         } else {
126             next;
127         }
128
129         push @$point, $not ? {"-not" => $term_clause} : $term_clause;
130     }
131     $result;
132 }
133
134 sub breakdown_term {
135     my ($term) = @_;
136
137     my $key = (grep { !/^__/ } keys %$term)[0];
138     (
139         $key, $term->{$key},
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
146     );
147 }
148
149 sub get_fm_links_by_hint {
150     my ($hint) = @_;
151     foreach my $field (values %{$Fieldmapper::fieldmap}) {
152         return $field->{"links"} if $field->{"hint"} eq $hint;
153     }
154     undef;
155 }
156
157 sub gen_au_term {
158     my ($value, $n) = @_;
159     +{
160         "-or" => [
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}}
166         ]
167     };
168 }
169
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) = @_;
176
177     my @joins = ();
178     my $nots = 0;
179     $join_num ||= 0;
180
181     foreach my $conj (qw/-and -or/) {
182         next unless exists $terms->{$conj};
183
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};
190                 $nots++;
191                 redo HINT_UNIT;
192             }
193
194             if (my $links = get_fm_links_by_hint($plain_hint) and
195                 $plain_hint ne "acqlia") {
196                 my @new_terms = ();
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);
202                     if ($nots > 0) {
203                         $au_term = {"-not" => $au_term};
204                         $nots--;
205                     }
206                     push @new_outer_terms, $au_term;
207                     $join_num++;
208                     delete $hint_unit->{$hint};
209                 }
210             }
211             if ($nots > 0) {
212                 $hint_unit = {"-not" => $hint_unit};
213                 $nots--;
214             }
215             push @new_outer_terms, $hint_unit if scalar keys %$hint_unit;
216         }
217         $terms->{$conj} = [ @new_outer_terms ];
218     }
219     @joins;
220 }
221
222 sub prepare_terms {
223     my ($terms, $is_and) = @_;
224
225     my $conj = $is_and ? "-and" : "-or";
226     my $outer_clause = {};
227
228     foreach my $class (qw/acqpo acqpl acqinv jub/) {
229         next if not exists $terms->{$class};
230
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);
235
236             my $term_clause;
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};
244             } else {
245                 next;
246             }
247
248             my $clause = {"+" . $class => $term_clause};
249             $clause = {"-not" => $clause} if $not;
250             push @{$outer_clause->{$conj}}, $clause;
251         }
252     }
253
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"});
258     }
259
260     return undef unless scalar keys %$outer_clause;
261     $outer_clause;
262 }
263
264 sub add_au_joins {
265     my ($from) = shift;
266
267     my $n = 0;
268     foreach my $join (@_) {
269         my ($hint, $attr, $num) = @$join;
270         my $start;
271         if ($hint eq "jub") {
272             $start = $from->{$hint};
273         } elsif ($hint eq "acqinv") {
274             $start = $from->{"jub"}->{"acqie"}->{"join"}->{$hint};
275         } else {
276             $start = $from->{"jub"}->{$hint};
277         }
278         my $clause = {
279             "class" => "au",
280             "type" => "left",
281             "field" => "id",
282             "fkey" => $attr,
283             "join" => {
284                 "ac$num" => {
285                     "class" => "ac",
286                     "type" => "left",
287                     "field" => "id",
288                     "fkey" => "card"
289                 }
290             }
291         };
292         if ($hint eq "jub") {
293             $start->{"au$num"} = $clause;
294         } else {
295             $start->{"join"} ||= {};
296             $start->{"join"}->{"au$num"} = $clause;
297         }
298         $n++;
299     }
300     $n;
301 }
302
303 __PACKAGE__->register_method(
304     method    => "unified_search",
305     api_name  => "open-ils.acq.lineitem.unified_search",
306     stream    => 1,
307     signature => {
308         desc   => q/Returns lineitems based on flexible search terms./,
309         params => [
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",
317                 type => "object"}
318         ],
319         return => {desc => "A stream of LIs on success, Event on failure"}
320     }
321 );
322
323 __PACKAGE__->register_method(
324     method    => "unified_search",
325     api_name  => "open-ils.acq.purchase_order.unified_search",
326     stream    => 1,
327     signature => {
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"}
331     }
332 );
333
334 __PACKAGE__->register_method(
335     method    => "unified_search",
336     api_name  => "open-ils.acq.picklist.unified_search",
337     stream    => 1,
338     signature => {
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"}
342     }
343 );
344
345 __PACKAGE__->register_method(
346     method    => "unified_search",
347     api_name  => "open-ils.acq.invoice.unified_search",
348     stream    => 1,
349     signature => {
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"}
353     }
354 );
355
356 sub unified_search {
357     my ($self, $conn, $auth, $and_terms, $or_terms, $conj, $options) = @_;
358     $options ||= {};
359
360     my $e = new_editor("authtoken" => $auth);
361     return $e->die_event unless $e->checkauth;
362
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"};
368
369     my $query = {
370         "select" => {$hint => [{"column" => "id", "transform" => "distinct"}]},
371         "from" => {
372             "jub" => {
373                 "acqpo" => {
374                     "type" => "full",
375                     "field" => "id",
376                     "fkey" => "purchase_order"
377                 },
378                 "acqpl" => {
379                     "type" => "full",
380                     "field" => "id",
381                     "fkey" => "picklist"
382                 },
383                 "acqie" => {
384                     "type" => "full",
385                     "field" => "lineitem",
386                     "fkey" => "id",
387                     "join" => {
388                         "acqinv" => {
389                             "type" => "full",
390                             "fkey" => "invoice",
391                             "field" => "id"
392                         }
393                     }
394                 }
395             }
396         },
397         "order_by" => {$hint => {"id" => {}}},
398         "offset" => ($options->{"offset"} || 0)
399     };
400
401     $query->{"limit"} = $options->{"limit"} if $options->{"limit"};
402
403     # XXX for the future? but it doesn't quite work as is.
404 #    # Remove anything in temporary picklists from search results.
405 #    $and_terms ||= {};
406 #    $and_terms->{"acqpl"} ||= [];
407 #    push @{$and_terms->{"acqpl"}}, {"name" => "", "__not" => 1};
408
409     $and_terms = prepare_terms($and_terms, 1);
410     $or_terms = prepare_terms($or_terms, 0) and do {
411         $query->{"from"}->{"jub"}->{"acqlia"} = {
412             "type" => "left", "field" => "lineitem", "fkey" => "id",
413         };
414     };
415
416     my $offset = add_au_joins($query->{"from"}, prepare_au_terms($and_terms));
417     add_au_joins($query->{"from"}, prepare_au_terms($or_terms, $offset));
418
419     if ($and_terms and $or_terms) {
420         $query->{"where"} = {
421             "-" . (lc $conj eq "or" ? "or" : "and") => [$and_terms, $or_terms]
422         };
423     } elsif ($and_terms) {
424         $query->{"where"} = $and_terms;
425     } elsif ($or_terms) {
426         $query->{"where"} = $or_terms;
427     } else {
428         $e->disconnect;
429         return new OpenILS::Event("BAD_PARAMS", "desc" => "No usable terms");
430     }
431
432     my $results = $e->json_query($query) or return $e->die_event;
433     if ($options->{"id_list"}) {
434         $conn->respond($_->{"id"}) foreach (grep { $_->{"id"} } @$results);
435     } else {
436         $conn->respond($retriever->($e, $_->{"id"}, $options))
437             foreach (grep { $_->{"id"} } @$results);
438     }
439
440     $e->disconnect;
441     undef;
442 }
443
444 __PACKAGE__->register_method(
445     method    => "bib_search",
446     api_name  => "open-ils.acq.biblio.wrapped_search",
447     stream    => 1,
448     signature => {
449         desc   => q/Returns new lineitems for each matching bib record/,
450         params => [
451             {desc => "Authentication token", type => "string"},
452             {desc => "search string", type => "string"},
453             {desc => "search options", type => "object"}
454         ],
455         return => {desc => "A stream of LIs on success, Event on failure"}
456     }
457 );
458
459 __PACKAGE__->register_method(
460     method    => "bib_search",
461     api_name  => "open-ils.acq.biblio.create_by_id",
462     stream    => 1,
463     signature => {
464         desc   => q/Returns new lineitems for each matching bib record/,
465         params => [
466             {desc => "Authentication token", type => "string"},
467             {desc => "list of bib IDs", type => "array"},
468             {desc => "options (for lineitem fleshing)", type => "object"}
469         ],
470         return => {desc => "A stream of LIs on success, Event on failure"}
471     }
472 );
473
474 # This is very similar to zsearch() in Order.pm
475 sub bib_search {
476     my ($self, $conn, $auth, $search, $opts) = @_;
477
478     my $e = new_editor("authtoken" => $auth, "xact" => 1);
479     return $e->die_event unless $e->checkauth;
480     return $e->die_event unless $e->allowed("CREATE_PICKLIST");
481
482     my $mgr = new OpenILS::Application::Acq::BatchManager(
483         "editor" => $e, "conn" => $conn
484     );
485
486     $opts ||= {};
487
488     my $picklist;
489     my @li_ids = ();
490     if ($self->api_name =~ /create_by_id/) {
491         $search = [ sort @$search ]; # for consitency
492         my $bibs = $e->search_biblio_record_entry(
493             {"id" => $search}, {"order_by" => {"bre" => ["id"]}}
494         ) or return $e->die_event;
495
496         if ($opts->{"reuse_picklist"}) {
497             $picklist = $e->retrieve_acq_picklist($opts->{"reuse_picklist"}) or
498                 return $e->die_event;
499             return $e->die_event unless
500                 $e->allowed("UPDATE_PICKLIST", $picklist->org_unit);
501
502             # If we're reusing an existing picklist, we don't need to
503             # create new lineitems for any bib records for which we already
504
505             my $already_have = $e->search_acq_lineitem({
506                 "picklist" => $picklist->id,
507                 "eg_bib_id" => [ map { $_->id } @$bibs ]
508             }) or return $e->die_event;
509          
510             # So in that case we a) save the lineitem id's of the relevant
511             # items that already exist so that we can return those items later,
512             # and b) remove the bib id's in question from our list of bib
513             # id's to lineitemize.
514             if (@$already_have) {
515                 push @li_ids, $_->id foreach (@$already_have);
516                 my @new_bibs = ();
517                 foreach my $bib (@$bibs) {
518                     push @new_bibs, $bib unless
519                         grep { $_->eg_bib_id == $bib->id } @$already_have;
520                 }
521                 $bibs = [ @new_bibs ];
522             }
523         } else {
524             $picklist =
525                 OpenILS::Application::Acq::Order::zsearch_build_pl($mgr, undef);
526         }
527
528         $conn->respond($picklist->id);
529
530         push @li_ids, map {
531             OpenILS::Application::Acq::Order::create_lineitem(
532                 $mgr,
533                 "picklist" => $picklist->id,
534                 "source_label" => "native-evergreen-catalog",
535                 "marc" => $_->marc,
536                 "eg_bib_id" => $_->id
537             )->id;
538         } (@$bibs);
539     } else {
540         $opts->{"limit"} ||= 10;
541
542         my $ses = create OpenSRF::AppSession("open-ils.search");
543         my $req = $ses->request(
544             "open-ils.search.biblio.multiclass.query.staff", $opts, $search
545         );
546
547         my $count = 0;
548         while (my $resp = $req->recv("timeout" => 60)) {
549             $picklist = OpenILS::Application::Acq::Order::zsearch_build_pl(
550                 $mgr, undef
551             ) unless $count++;
552
553             my $result = $resp->content;
554             next if not ref $result;
555
556             # The result object contains a whole heck of a lot more information
557             # than just bib IDs, so maybe we could tell the client something
558             # useful (progress meter at least) in the future...
559             push @li_ids, map {
560                 my $bib = $_->[0];
561                 OpenILS::Application::Acq::Order::create_lineitem(
562                     $mgr,
563                     "picklist" => $picklist->id,
564                     "source_label" => "native-evergreen-catalog",
565                     "marc" => $e->retrieve_biblio_record_entry($bib)->marc,
566                     "eg_bib_id" => $bib
567                 )->id;
568             } (@{$result->{"ids"}});
569         }
570         $ses->disconnect;
571     }
572
573     $e->commit;
574
575     $logger->info("created @li_ids new lineitems for picklist $picklist");
576
577     # new editor, no transaction needed this time
578     $e = new_editor("authtoken" => $auth) or return $e->die_event;
579     return $e->die_event unless $e->checkauth;
580     $conn->respond($RETRIEVERS{"lineitem"}->($e, $_, $opts)) foreach @li_ids;
581     $e->disconnect;
582
583     undef;
584 }
585
586 1;