]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Cat.pm
setting the bib source id on update/create/import when source is defined
[Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Cat.pm
1 use strict; use warnings;
2 package OpenILS::Application::Cat;
3 use OpenILS::Application::AppUtils;
4 use OpenSRF::Application;
5 use OpenILS::Application::Cat::Utils;
6 use OpenILS::Application::Cat::Merge;
7 use base qw/OpenSRF::Application/;
8 use Time::HiRes qw(time);
9 use OpenSRF::EX qw(:try);
10 use JSON;
11 use OpenILS::Utils::Fieldmapper;
12 use OpenILS::Event;
13
14 use XML::LibXML;
15 use Unicode::Normalize;
16 use Data::Dumper;
17 use OpenILS::Utils::FlatXML;
18 use OpenILS::Utils::Editor q/:funcs/;
19 use OpenILS::Perm;
20 use OpenSRF::Utils::SettingsClient;
21 use OpenSRF::Utils::Logger qw($logger);
22
23 my $apputils = "OpenILS::Application::AppUtils";
24
25 my $utils = "OpenILS::Application::Cat::Utils";
26 my $U = "OpenILS::Application::AppUtils";
27
28 my $conf;
29
30 my %marctemplates;
31
32 sub entityize { 
33         my $stuff = shift;
34         my $form = shift || "";
35
36         if ($form eq 'D') {
37                 $stuff = NFD($stuff);
38         } else {
39                 $stuff = NFC($stuff);
40         }
41
42         $stuff =~ s/([\x{0080}-\x{fffd}])/sprintf('&#x%X;',ord($1))/sgoe;
43         return $stuff;
44 }
45
46 __PACKAGE__->register_method(
47         method  => "retrieve_marc_template",
48         api_name        => "open-ils.cat.biblio.marc_template.retrieve",
49         notes           => <<"  NOTES");
50         Returns a MARC 'record tree' based on a set of pre-defined templates.
51         Templates include : book
52         NOTES
53
54 sub retrieve_marc_template {
55         my( $self, $client, $type ) = @_;
56
57         return $marctemplates{$type} if defined($marctemplates{$type});
58         $marctemplates{$type} = _load_marc_template($type);
59         return $marctemplates{$type};
60 }
61
62 sub _load_marc_template {
63         my $type = shift;
64
65         if(!$conf) { $conf = OpenSRF::Utils::SettingsClient->new; }
66
67         my $template = $conf->config_value(                                     
68                 "apps", "open-ils.cat","app_settings", "marctemplates", $type );
69         warn "Opening template file $template\n";
70
71         open( F, $template ) or 
72                 throw OpenSRF::EX::ERROR ("Unable to open MARC template file: $template : $@");
73
74         my @xml = <F>;
75         close(F);
76         my $xml = join('', @xml);
77
78         return XML::LibXML->new->parse_string($xml)->documentElement->toString;
79 }
80
81 my $__bib_sources;
82 sub bib_source_from_name {
83         my $name = shift;
84         $logger->debug("searching for bib source: $name");
85
86         $__bib_sources = new_editor()->retrieve_all_config_bib_source()
87                 unless $__bib_sources;
88
89         my ($s) = grep { lc($_->source) eq lc($name) } @$__bib_sources;
90
91         return $s->id if $s;
92         return undef;
93 }
94
95
96
97 __PACKAGE__->register_method(
98         method  => "create_record_xml",
99         api_name        => "open-ils.cat.biblio.record.xml.create.override",
100         signature       => q/@see open-ils.cat.biblio.record.xml.create/);
101
102 __PACKAGE__->register_method(
103         method          => "create_record_xml",
104         api_name                => "open-ils.cat.biblio.record.xml.create",
105         signature       => q/
106                 Inserts a new biblio with the given XML
107         /
108 );
109
110 sub create_record_xml {
111         my( $self, $client, $login, $xml, $source ) = @_;
112
113         my $override = 1 if $self->api_name =~ /override/;
114
115         my( $user_obj, $evt ) = $U->checksesperm($login, 'CREATE_MARC');
116         return $evt if $evt;
117
118         $logger->activity("user ".$user_obj->id." creating new MARC record");
119
120         my $meth = $self->method_lookup("open-ils.cat.biblio.record.xml.import");
121
122         $meth = $self->method_lookup(
123                 "open-ils.cat.biblio.record.xml.import.override") if $override;
124
125         my ($s) = $meth->run($login, $xml, $source);
126         return $s;
127 }
128
129
130
131 __PACKAGE__->register_method(
132         method  => "biblio_record_replace_marc",
133         api_name        => "open-ils.cat.biblio.record.xml.update",
134         argc            => 3, 
135         signature       => q/
136                 Updates the XML for a given biblio record.
137                 This does not change any other aspect of the record entry
138                 exception the XML, the editor, and the edit date.
139                 @return The update record object
140         /
141 );
142
143 __PACKAGE__->register_method(
144         method          => 'biblio_record_replace_marc',
145         api_name                => 'open-ils.cat.biblio.record.marc.replace',
146         signature       => q/
147                 @param auth The authtoken
148                 @param recid The record whose MARC we're replacing
149                 @param newxml The new xml to use
150         /
151 );
152
153 __PACKAGE__->register_method(
154         method          => 'biblio_record_replace_marc',
155         api_name                => 'open-ils.cat.biblio.record.marc.replace.override',
156         signature       => q/@see open-ils.cat.biblio.record.marc.replace/
157 );
158
159 sub biblio_record_replace_marc  {
160         my( $self, $conn, $auth, $recid, $newxml, $source ) = @_;
161
162         my $e = new_editor(authtoken => $auth, xact => 1);
163
164         return $e->event unless $e->checkauth;
165         return $e->event unless $e->allowed('CREATE_MARC');
166
167         my $rec = $e->retrieve_biblio_record_entry($recid)
168                 or return $e->event;
169
170         my $fixtcn = 1 if $self->api_name =~ /replace/o;
171
172         # See if there is a different record in the database that has our TCN value
173         # If we're not updating the TCN, all we care about it the marcdoc
174         my $override = $self->api_name =~ /override/;
175
176         my( $tcn, $tsource, $marcdoc, $evt) = 
177                 _find_tcn_info($e->session, $newxml, $override, $recid);
178
179         return $evt if $evt;
180
181         if( $fixtcn ) {
182                 $rec->tcn_value($tcn);
183                 $rec->tcn_source($tsource);
184         }
185
186         $rec->source(bib_source_from_name($source)) if $source;
187         $rec->editor($e->requestor->id);
188         $rec->edit_date('now');
189         $rec->marc( entityize( $marcdoc->documentElement->toString ) );
190
191         $logger->activity("user ".$e->requestor->id." replacing MARC for record $recid");
192
193         $e->update_biblio_record_entry($rec) or return $e->event;
194         $e->request('open-ils.worm.wormize.biblio', $recid) or return $e->event;
195         $e->commit;
196
197         return $rec;
198 }
199
200
201
202
203 __PACKAGE__->register_method(
204         method  => "biblio_record_xml_import",
205         api_name        => "open-ils.cat.biblio.record.xml.import.override",
206         signature       => q/@see open-ils.cat.biblio.record.xml.import/);
207
208 __PACKAGE__->register_method(
209         method  => "biblio_record_xml_import",
210         api_name        => "open-ils.cat.biblio.record.xml.import",
211         notes           => <<"  NOTES");
212         Takes a marcxml record and imports the record into the database.  In this
213         case, the marcxml record is assumed to be a complete record (i.e. valid
214         MARC).  The title control number is taken from (whichever comes first)
215         tags 001, 039[ab], 020a, 022a, 010, 035a and whichever does not already exist
216         in the database.
217         user_session must have IMPORT_MARC permissions
218         NOTES
219
220
221 sub biblio_record_xml_import {
222         my( $self, $client, $authtoken, $xml, $source) = @_;
223
224
225         # XXX Make the source the ID from config.bib_source
226
227         my $override = 1 if $self->api_name =~ /override/;
228
229         my( $tcn, $tcn_source, $marcdoc );
230         my( $requestor, $evt ) = $U->checksesperm($authtoken, 'IMPORT_MARC');
231         return $evt if $evt;
232
233         my $session = $apputils->start_db_session();
234
235         ( $tcn, $tcn_source, $marcdoc, $evt ) = _find_tcn_info($session, $xml, $override);
236         return $evt if $evt;
237
238         $logger->activity("user ".$requestor->id.
239                 " creating new biblio entry with tcn=$tcn and tcn_source $tcn_source");
240
241         my $record = Fieldmapper::biblio::record_entry->new;
242
243         $record->source(bib_source_from_name($source)) if $source;
244         $record->tcn_source($tcn_source);
245         $record->tcn_value($tcn);
246         $record->creator($requestor->id);
247         $record->editor($requestor->id);
248         $record->create_date('now');
249         $record->edit_date('now');
250         $record->marc( entityize( $marcdoc->documentElement->toString ) );
251
252         my $id = $session->request(
253                 "open-ils.storage.direct.biblio.record_entry.create", $record )->gather(1);
254
255         return $U->DB_UPDATE_FAILED($record) unless $id;
256         $record->id( $id );
257
258         $logger->info("marc create/import created new record $id");
259
260         $apputils->commit_db_session($session);
261
262         $logger->debug("Sending record off to be wormized");
263
264         my $stat = $U->storagereq( 'open-ils.worm.wormize.biblio', $id );
265         throw OpenSRF::EX::ERROR 
266                 ("Unable to wormize imported record") unless $stat;
267
268         return $record;
269 }
270
271
272 sub _find_tcn_info { 
273         my $session             = shift;
274         my $xml                 = shift;
275         my $override    = shift;
276         my $existing_rec        = shift || 0;
277
278         # parse the XML
279         my $marcxml = XML::LibXML->new->parse_string( $xml );
280         $marcxml->documentElement->setNamespace( 
281                 "http://www.loc.gov/MARC21/slim", "marc", 1 );
282
283         my $xpath = '//marc:controlfield[@tag="001"]';
284         my $tcn = $marcxml->documentElement->findvalue($xpath);
285         $logger->info("biblio import located 001 (tcn) value of $tcn");
286
287         $xpath = '//marc:controlfield[@tag="003"]';
288         my $tcn_source = $marcxml->documentElement->findvalue($xpath) || "System Local";
289
290         if(my $rec = _tcn_exists($session, $tcn, $tcn_source, $existing_rec) ) {
291
292                 my $origtcn = $tcn;
293                 $tcn = find_free_tcn( $marcxml, $session, $existing_rec );
294
295                 # if we're overriding, try to find a different TCN to use
296                 if( $override ) {
297
298                         $logger->activity("tcn value $tcn already exists, attempting to override");
299
300                         if(!$tcn) {
301                                 return ( 
302                                         undef, 
303                                         undef, 
304                                         undef,
305                                         OpenILS::Event->new(
306                                                 'OPEN_TCN_NOT_FOUND', 
307                                                         payload => $marcxml->toString())
308                                         );
309                         }
310
311                 } else {
312
313                         $logger->warn("tcn value $origtcn already exists in import/create");
314
315                         # otherwise, return event
316                         return ( 
317                                 undef, 
318                                 undef, 
319                                 undef,
320                                 OpenILS::Event->new( 
321                                         'TCN_EXISTS', payload => { 
322                                                 dup_record      => $rec, 
323                                                 tcn                     => $origtcn,
324                                                 new_tcn         => $tcn
325                                                 }
326                                         )
327                                 );
328                 }
329         }
330
331         return ($tcn, $tcn_source, $marcxml);
332 }
333
334 sub find_free_tcn {
335
336         my $marcxml = shift;
337         my $session = shift;
338         my $existing_rec = shift;
339
340         my $add_039 = 0;
341
342         my $xpath = '//marc:datafield[@tag="039"]/subfield[@code="a"]';
343         my ($tcn) = $marcxml->documentElement->findvalue($xpath) =~ /(\w+)\s*$/o;
344         $xpath = '//marc:datafield[@tag="039"]/subfield[@code="b"]';
345         my $tcn_source = $marcxml->documentElement->findvalue($xpath) || "System Local";
346
347         if(_tcn_exists($session, $tcn, $tcn_source, $existing_rec)) {
348                 $tcn = undef;
349         } else {
350                 $add_039++;
351         }
352
353
354         if(!$tcn) {
355                 $xpath = '//marc:datafield[@tag="020"]/subfield[@code="a"]';
356                 ($tcn) = $marcxml->documentElement->findvalue($xpath) =~ /(\w+)\s*$/o;
357                 $tcn_source = "ISBN";
358                 if(_tcn_exists($session, $tcn, $tcn_source, $existing_rec)) {$tcn = undef;}
359         }
360
361         if(!$tcn) { 
362                 $xpath = '//marc:datafield[@tag="022"]/subfield[@code="a"]';
363                 ($tcn) = $marcxml->documentElement->findvalue($xpath) =~ /(\w+)\s*$/o;
364                 $tcn_source = "ISSN";
365                 if(_tcn_exists($session, $tcn, $tcn_source, $existing_rec)) {$tcn = undef;}
366         }
367
368         if(!$tcn) {
369                 $xpath = '//marc:datafield[@tag="010"]';
370                 ($tcn) = $marcxml->documentElement->findvalue($xpath) =~ /(\w+)\s*$/o;
371                 $tcn_source = "LCCN";
372                 if(_tcn_exists($session, $tcn, $tcn_source, $existing_rec)) {$tcn = undef;}
373         }
374
375         if(!$tcn) {
376                 $xpath = '//marc:datafield[@tag="035"]/subfield[@code="a"]';
377                 ($tcn) = $marcxml->documentElement->findvalue($xpath) =~ /(\w+)\s*$/o;
378                 $tcn_source = "System Legacy";
379                 if(_tcn_exists($session, $tcn, $tcn_source, $existing_rec)) {$tcn = undef;}
380
381                 if($tcn) {
382                         $marcxml->documentElement->removeChild(
383                                 $marcxml->documentElement->findnodes( '//datafield[@tag="035"]' )
384                         );
385                 }
386         }
387
388         return undef unless $tcn;
389
390         if ($add_039) {
391                 my $df = $marcxml->createElementNS( 'http://www.loc.gov/MARC21/slim', 'datafield');
392                 $df->setAttribute( tag => '039' );
393                 $df->setAttribute( ind1 => ' ' );
394                 $df->setAttribute( ind2 => ' ' );
395                 $marcxml->documentElement->appendChild( $df );
396
397                 my $sfa = $marcxml->createElementNS( 'http://www.loc.gov/MARC21/slim', 'subfield');
398                 $sfa->setAttribute( code => 'a' );
399                 $sfa->appendChild( $marcxml->createTextNode( $tcn ) );
400                 $df->appendChild( $sfa );
401
402                 my $sfb = $marcxml->createElementNS( 'http://www.loc.gov/MARC21/slim', 'subfield');
403                 $sfb->setAttribute( code => 'b' );
404                 $sfb->appendChild( $marcxml->createTextNode( $tcn_source ) );
405                 $df->appendChild( $sfb );
406         }
407
408         return $tcn;
409 }
410
411
412
413 sub _tcn_exists {
414         my $session = shift;
415         my $tcn = shift;
416         my $source = shift;
417         my $existing_rec = shift || 0;
418
419         if(!$tcn) {return 0;}
420
421         $logger->debug("tcn_exists search for tcn $tcn and source $source and id $existing_rec");
422
423         # XXX why does the source matter?
424 #       my $req = $session->request(      
425 #               { tcn_value => $tcn, tcn_source => $source, deleted => 'f' } );
426
427         my $req = $session->request(      
428                 "open-ils.storage.id_list.biblio.record_entry.search_where.atomic",
429                 { tcn_value => $tcn, deleted => 'f', id => {'!=' => $existing_rec} } );
430
431         my $recs = $req->gather(1);
432
433         if($recs and $recs->[0]) {
434                 $logger->debug("_tcn_exists is true for tcn : $tcn ($source)");
435                 return $recs->[0];
436         }
437
438         $logger->debug("_tcn_exists is false for tcn : $tcn ($source)");
439         return 0;
440 }
441
442
443
444
445 # XXX deprecated. Remove me.
446
447 =head deprecated
448
449 __PACKAGE__->register_method(
450         method  => "biblio_record_tree_retrieve",
451         api_name        => "open-ils.cat.biblio.record.tree.retrieve",
452 );
453
454 sub biblio_record_tree_retrieve {
455
456         my( $self, $client, $recordid ) = @_;
457
458         my $name = "open-ils.storage.direct.biblio.record_entry.retrieve";
459         my $session = OpenSRF::AppSession->create( "open-ils.storage" );
460         my $request = $session->request( $name, $recordid );
461         my $marcxml = $request->gather(1);
462
463         if(!$marcxml) {
464                 throw OpenSRF::EX::ERROR 
465                         ("No record in database with id $recordid");
466         }
467
468         $session->disconnect();
469         $session->kill_me();
470
471         warn "turning into nodeset\n";
472         my $nodes = OpenILS::Utils::FlatXML->new()->xml_to_nodeset( $marcxml->marc ); 
473         warn "turning nodeset into tree\n";
474         my $tree = $utils->nodeset2tree( $nodes->nodeset );
475
476         $tree->owner_doc( $marcxml->id() );
477
478         warn "returning tree\n";
479
480         return $tree;
481 }
482 =cut
483
484
485 =head deprecate 
486 __PACKAGE__->register_method(
487         method  => "biblio_record_xml_update",
488         api_name        => "open-ils.cat.biblio.record.xml.update",
489         argc            => 3, #(session_id, biblio_tree ) 
490         notes           => <<'  NOTES');
491         Updates the XML of a biblio record entry
492         @param authtoken The session token for the staff updating the record
493         @param docID The record entry ID to update
494         @param xml The new MARCXML record
495         NOTES
496
497 sub biblio_record_xml_update {
498
499         my( $self, $client, $user_session,  $id, $xml ) = @_;
500
501         my $user_obj = $apputils->check_user_session($user_session); 
502
503         if($apputils->check_user_perms(
504                         $user_obj->id, $user_obj->home_ou, "UPDATE_MARC")) {
505                 return OpenILS::Perm->new("UPDATE_MARC"); 
506         }
507
508         $logger->activity("user ".$user_obj->id." updating biblio record $id");
509
510
511         my $session = OpenILS::Application::AppUtils->start_db_session();
512
513         warn "Retrieving biblio record from storage for update\n";
514
515         my $req1 = $session->request(
516                         "open-ils.storage.direct.biblio.record_entry.batch.retrieve", $id );
517         my $biblio = $req1->gather(1);
518
519         warn "retrieved doc $id\n";
520
521         my $doc = XML::LibXML->new->parse_string($xml);
522         throw OpenSRF::EX::ERROR ("Invalid XML in record update: $xml") unless $doc;
523
524         $biblio->marc( entityize( $doc->documentElement->toString ) );
525         $biblio->editor( $user_obj->id );
526         $biblio->edit_date( 'now' );
527
528         warn "Sending updated doc $id to db with xml ".$biblio->marc. "\n";
529
530         my $req = $session->request( 
531                 "open-ils.storage.direct.biblio.record_entry.update", $biblio );
532
533         $req->wait_complete;
534         my $status = $req->recv();
535         if( !$status || $status->isa("Error") || ! $status->content) {
536                 OpenILS::Application::AppUtils->rollback_db_session($session);
537                 if($status->isa("Error")) { throw $status ($status); }
538                 throw OpenSRF::EX::ERROR ("Error updating biblio record");
539         }
540         $req->finish();
541
542         # Send the doc to the wormer for wormizing
543         warn "Starting worm session\n";
544
545         my $success = 0;
546         my $wresp;
547
548         my $wreq = $session->request( "open-ils.worm.wormize.biblio", $id );
549
550         my $w = 0;
551         try {
552                 $w = $wreq->gather(1);
553
554         } catch Error with {
555                 my $e = shift;
556                 warn "wormizing failed, rolling back\n";
557                 OpenILS::Application::AppUtils->rollback_db_session($session);
558
559                 if($e) { throw $e ($e); }
560                 throw OpenSRF::EX::ERROR ("Wormizing Failed for $id" );
561         };
562
563         warn "Committing db session...\n";
564         OpenILS::Application::AppUtils->commit_db_session( $session );
565
566 #       $client->respond_complete($tree);
567
568         warn "Done wormizing\n";
569
570         #use Data::Dumper;
571         #warn "Returning tree:\n";
572         #warn Dumper $tree;
573
574         return $biblio;
575
576 }
577
578 =cut
579
580
581
582 __PACKAGE__->register_method(
583         method  => "biblio_record_record_metadata",
584         api_name        => "open-ils.cat.biblio.record.metadata.retrieve",
585         argc            => 1, #(session_id, biblio_tree ) 
586         notes           => "Walks the tree and commits any changed nodes " .
587                                         "adds any new nodes, and deletes any deleted nodes",
588 );
589
590 sub biblio_record_record_metadata {
591         my( $self, $client, $authtoken, $ids ) = @_;
592
593         return [] unless $ids and @$ids;
594
595         my $editor = OpenILS::Utils::Editor->new(authtoken => $authtoken);
596         return $editor->event unless $editor->checkauth;
597         return $editor->event unless $editor->allowed('VIEW_USER');
598
599         my @results;
600
601         for(@$ids) {
602                 return $editor->event unless 
603                         my $rec = $editor->retrieve_biblio_record_entry($_);
604                 $rec->creator($editor->retrieve_actor_user($rec->creator));
605                 $rec->editor($editor->retrieve_actor_user($rec->editor));
606                 $rec->clear_marc; # slim the record down
607                 push( @results, $rec );
608         }
609
610         return \@results;
611 }
612
613
614
615 __PACKAGE__->register_method(
616         method  => "biblio_record_marc_cn",
617         api_name        => "open-ils.cat.biblio.record.marc_cn.retrieve",
618         argc            => 1, #(bib id ) 
619 );
620
621 sub biblio_record_marc_cn {
622         my( $self, $client, $id ) = @_;
623
624         my $session = OpenSRF::AppSession->create("open-ils.cstore");
625         my $marc = $session
626                 ->request("open-ils.cstore.direct.biblio.record_entry.retrieve", $id )
627                 ->gather(1)
628                 ->marc;
629
630         my $doc = XML::LibXML->new->parse_string($marc);
631         $doc->documentElement->setNamespace( "http://www.loc.gov/MARC21/slim", "marc", 1 );
632         
633         my @res;
634         for my $tag ( qw/050 055 060 070 080 082 086 088 090 092 096 098 099/ ) {
635                 my @node = $doc->findnodes("//marc:datafield[\@tag='$tag']");
636                 for my $x (@node) {
637                         my $cn = $x->findvalue("marc:subfield[\@code='a' or \@code='b']");
638                         push @res, {$tag => $cn} if ($cn);
639                 }
640         }
641
642         return \@res
643 }
644
645 sub _get_id_by_userid {
646
647         my @users = @_;
648         my @ids;
649
650         my $session = OpenSRF::AppSession->create( "open-ils.cstore" );
651         my $request = $session->request( 
652                 "open-ils.cstore.direct.actor.user.search.atomic", { usrname => \@users } );
653
654         $request->wait_complete;
655         my $response = $request->recv();
656         if(!$request->complete) { 
657                 throw OpenSRF::EX::ERROR ("no response from cstore on user retrieve");
658         }
659
660         if(UNIVERSAL::isa( $response, "Error")){
661                 throw $response ($response);
662         }
663
664         for my $u (@{$response->content}) {
665                 next unless ref($u);
666                 push @ids, $u->id();
667         }
668
669         $request->finish;
670         $session->disconnect;
671         $session->kill_me();
672
673         return @ids;
674 }
675
676
677 # commits metadata objects to the db
678 sub _update_record_metadata {
679
680         my ($session, @docs ) = @_;
681
682         for my $doc (@docs) {
683
684                 my $user_obj = $doc->{user};
685                 my $docid = $doc->{docid};
686
687                 warn "Updating metata for doc $docid\n";
688
689                 my $request = $session->request( 
690                         "open-ils.storage.direct.biblio.record_entry.retrieve", $docid );
691                 my $record = $request->gather(1);
692
693                 warn "retrieved record\n";
694                 my ($id) = _get_id_by_userid($user_obj->usrname);
695
696                 warn "got $id from _get_id_by_userid\n";
697                 $record->editor($id);
698                 
699                 warn "Grabbed the record, updating and moving on\n";
700
701                 $request = $session->request( 
702                         "open-ils.storage.direct.biblio.record_entry.update", $record );
703                 $request->gather(1);
704         }
705
706         warn "committing metarecord update\n";
707
708         return 1;
709 }
710
711
712
713 __PACKAGE__->register_method(
714         method  => "orgs_for_title",
715         api_name        => "open-ils.cat.actor.org_unit.retrieve_by_title"
716 );
717
718 sub orgs_for_title {
719         my( $self, $client, $record_id ) = @_;
720
721         my $vols = $apputils->simple_scalar_request(
722                 "open-ils.cstore",
723                 "open-ils.cstore.direct.asset.call_number.search.atomic",
724                 { record => $record_id, deleted => 'f' });
725
726         my $orgs = { map {$_->owning_lib => 1 } @$vols };
727         return [ keys %$orgs ];
728 }
729
730
731 __PACKAGE__->register_method(
732         method  => "retrieve_copies",
733         api_name        => "open-ils.cat.asset.copy_tree.retrieve");
734
735 __PACKAGE__->register_method(
736         method  => "retrieve_copies",
737         api_name        => "open-ils.cat.asset.copy_tree.global.retrieve");
738
739 # user_session may be null/undef
740 sub retrieve_copies {
741
742         my( $self, $client, $user_session, $docid, @org_ids ) = @_;
743
744         if(ref($org_ids[0])) { @org_ids = @{$org_ids[0]}; }
745
746         $docid = "$docid";
747
748         warn " $$ retrieving copy tree for orgs @org_ids and doc $docid at " . time() . "\n";
749
750         # grabbing copy trees should be available for everyone..
751         if(!@org_ids and $user_session) {
752                 my $user_obj = 
753                         OpenILS::Application::AppUtils->check_user_session( $user_session ); #throws EX on error
754                         @org_ids = ($user_obj->home_ou);
755         }
756
757         if( $self->api_name =~ /global/ ) {
758                 warn "performing global copy_tree search for $docid\n";
759                 return _build_volume_list( { record => $docid } );
760
761         } else {
762
763                 my @all_vols;
764                 for my $orgid (@org_ids) {
765                         my $vols = _build_volume_list( 
766                                         { record => $docid, owning_lib => $orgid } );
767                         warn "Volumes built for org $orgid\n";
768                         push( @all_vols, @$vols );
769                 }
770                 
771                 warn " $$ Finished copy_tree at " . time() . "\n";
772                 return \@all_vols;
773         }
774
775         return undef;
776 }
777
778
779 sub _build_volume_list {
780         my $search_hash = shift;
781
782         $search_hash->{deleted} = 'f';
783
784         my      $session = OpenSRF::AppSession->create( "open-ils.cstore" );
785         
786
787         my $request = $session->request( 
788                         "open-ils.cstore.direct.asset.call_number.search.atomic", $search_hash );
789                         #"open-ils.storage.direct.asset.call_number.search.atomic", $search_hash );
790
791         my $vols = $request->gather(1);
792         my @volumes;
793
794         for my $volume (@$vols) {
795
796                 warn "Grabbing copies for volume: " . $volume->id . "\n";
797                 my $creq = $session->request(
798                         "open-ils.cstore.direct.asset.copy.search.atomic", 
799                         { call_number => $volume->id , deleted => 'f' });
800                         #"open-ils.storage.direct.asset.copy.search.call_number.atomic", $volume->id );
801
802                 my $copies = $creq->gather(1);
803
804                 $copies = [ sort { $a->barcode cmp $b->barcode } @$copies  ];
805
806                 $volume->copies($copies);
807
808                 push( @volumes, $volume );
809         }
810
811
812         $session->disconnect();
813         return \@volumes;
814
815 }
816
817
818 __PACKAGE__->register_method(
819         method  => "fleshed_copy_update",
820         api_name        => "open-ils.cat.asset.copy.fleshed.batch.update",);
821
822 __PACKAGE__->register_method(
823         method  => "fleshed_copy_update",
824         api_name        => "open-ils.cat.asset.copy.fleshed.batch.update.override",);
825
826 sub fleshed_copy_update {
827         my( $self, $conn, $auth, $copies ) = @_;
828         return 1 unless ref $copies;
829         my( $reqr, $evt ) = $U->checkses($auth);
830         return $evt if $evt;
831         my $editor = OpenILS::Utils::Editor->new(requestor => $reqr, xact => 1);
832         my $override = $self->api_name =~ /override/;
833         $evt = update_fleshed_copies( $editor, $override, undef, $copies);
834         return $evt if $evt;
835         $editor->finish;
836         $logger->info("fleshed copy update successfully updated ".scalar(@$copies)." copies");
837         return 1;
838 }
839
840
841 __PACKAGE__->register_method(
842         method => 'merge',
843         api_name        => 'open-ils.cat.biblio.records.merge',
844         signature       => q/
845                 Merges a group of records
846                 @param auth The login session key
847                 @param master The id of the record all other r
848                         ecords should be merged into
849                 @param records Array of records to be merged into the master record
850                 @return 1 on success, Event on error.
851         /
852 );
853
854 sub merge {
855         my( $self, $conn, $auth, $master, $records ) = @_;
856         my( $reqr, $evt ) = $U->checkses($auth);
857         return $evt if $evt;
858         my $editor = OpenILS::Utils::Editor->new( requestor => $reqr, xact => 1 );
859         my $v = OpenILS::Application::Cat::Merge::merge_records($editor, $master, $records);
860         return $v if $v;
861         $editor->finish;
862         return 1;
863 }
864
865
866
867
868 # ---------------------------------------------------------------------------
869 # ---------------------------------------------------------------------------
870
871 # returns true if the given title (id) has no un-deleted
872 # copies attached
873 sub title_is_empty {
874         my( $editor, $rid ) = @_;
875
876         my $cnlist = $editor->search_asset_call_number(
877                 { record => $rid, deleted => 'f' }, { idlist => 1 } );
878         return 1 unless @$cnlist;
879
880         for my $cn (@$cnlist) {
881                 my $copylist = $editor->search_asset_copy(
882                         { call_number => $cn, deleted => 'f' }, { idlist => 1 });
883                 return 0 if @$copylist; # false if we find any copies
884         }
885
886         return 1;
887 }
888
889
890 __PACKAGE__->register_method(
891         method  => "fleshed_volume_update",
892         api_name        => "open-ils.cat.asset.volume.fleshed.batch.update",);
893
894 __PACKAGE__->register_method(
895         method  => "fleshed_volume_update",
896         api_name        => "open-ils.cat.asset.volume.fleshed.batch.update.override",);
897
898 sub fleshed_volume_update {
899         my( $self, $conn, $auth, $volumes ) = @_;
900         my( $reqr, $evt ) = $U->checkses($auth);
901         return $evt if $evt;
902
903         my $override = ($self->api_name =~ /override/);
904         my $editor = OpenILS::Utils::Editor->new( requestor => $reqr, xact => 1 );
905
906         for my $vol (@$volumes) {
907                 $logger->info("vol-update: investigating volume ".$vol->id);
908
909                 $vol->editor($reqr->id);
910                 $vol->edit_date('now');
911
912                 my $copies = $vol->copies;
913                 $vol->clear_copies;
914
915                 if( $vol->isdeleted ) {
916
917                         $logger->info("vol-update: deleting volume");
918                         my $cs = $editor->search_asset_copy(
919                                 { call_number => $vol->id, deleted => 'f' } );
920                         return OpenILS::Event->new(
921                                 'VOLUME_NOT_EMPTY', payload => $vol->id ) if @$cs;
922
923                         $vol->deleted('t');
924                         return $editor->event unless
925                                 $editor->update_asset_call_number($vol);
926
927                         
928                 } elsif( $vol->isnew ) {
929                         $logger->info("vol-update: creating volume");
930                         $evt = create_volume( $override, $editor, $vol );
931                         return $evt if $evt;
932
933                 } elsif( $vol->ischanged ) {
934                         $logger->info("vol-update: update volume");
935                         return $editor->event unless
936                                 $editor->update_asset_call_number($vol);
937                         return $evt if $evt;
938                 }
939
940                 # now update any attached copies
941                 if( @$copies and !$vol->isdeleted ) {
942                         $_->call_number($vol->id) for @$copies;
943                         $evt = update_fleshed_copies( $editor, $override, $vol, $copies );
944                         return $evt if $evt;
945                 }
946         }
947
948         $editor->finish;
949         return scalar(@$volumes);
950 }
951
952
953 # this does the actual work
954 sub update_fleshed_copies {
955         my( $editor, $override, $vol, $copies ) = @_;
956
957         my $evt;
958         my $fetchvol = ($vol) ? 0 : 1;
959
960         my %cache;
961         $cache{$vol->id} = $vol if $vol;
962
963         for my $copy (@$copies) {
964
965                 my $copyid = $copy->id;
966                 $logger->info("vol-update: inspecting copy $copyid");
967
968                 if( !($vol = $cache{$copy->call_number}) ) {
969                         $vol = $cache{$copy->call_number} = 
970                                 $editor->retrieve_asset_call_number($copy->call_number);
971                         return $editor->event unless $vol;
972                 }
973
974                 $copy->editor($editor->requestor->id);
975                 $copy->edit_date('now');
976
977                 $copy->status( $copy->status->id ) if ref($copy->status);
978                 $copy->location( $copy->location->id ) if ref($copy->location);
979                 $copy->circ_lib( $copy->circ_lib->id ) if ref($copy->circ_lib);
980                 
981                 my $sc_entries = $copy->stat_cat_entries;
982                 $copy->clear_stat_cat_entries;
983
984                 if( $copy->isdeleted ) {
985                         $evt = delete_copy($editor, $override, $vol, $copy);
986                         return $evt if $evt;
987
988                 } elsif( $copy->isnew ) {
989                         $evt = create_copy( $editor, $vol, $copy );
990                         return $evt if $evt;
991
992                 } elsif( $copy->ischanged ) {
993
994                         $logger->info("vol-update: updating copy $copyid");
995                         return $editor->event unless
996                                 $editor->update_asset_copy(
997                                         $copy, {checkperm=>1, permorg=>$vol->owning_lib});
998                         return $evt if $evt;
999                 }
1000
1001                 $copy->stat_cat_entries( $sc_entries );
1002                 $evt = update_copy_stat_entries($editor, $copy);
1003                 return $evt if $evt;
1004         }
1005
1006         $logger->debug("vol-update: done updating copy batch");
1007
1008         return undef;
1009 }
1010
1011 sub delete_copy {
1012         my( $editor, $override, $vol, $copy ) = @_;
1013
1014         $logger->info("vol-update: deleting copy ".$copy->id);
1015         $copy->deleted('t');
1016
1017         $editor->update_asset_copy(
1018                 $copy, {checkperm=>1, permorg=>$vol->owning_lib})
1019                 or return $editor->event;
1020
1021         if( title_is_empty($editor, $vol->record) ) {
1022
1023                 if( $override ) {
1024
1025                         # delete this volume if it's not already marked as deleted
1026                         if(!$vol->deleted || $vol->deleted =~ /f/io || ! $vol->isdeleted) {
1027                                 $vol->deleted('t');
1028                                 $editor->update_asset_call_number($vol, {checkperm=>0})
1029                                         or return $editor->event;
1030                         }
1031
1032                         # then delete the record this volume points to
1033                         my $rec = $editor->retrieve_biblio_record_entry($vol->record)
1034                                 or return $editor->event;
1035
1036                         if( !$rec->deleted ) {
1037                                 $rec->deleted('t');
1038                                 $rec->active('f');
1039                                 $editor->update_biblio_record_entry($rec, {checkperm=>0})
1040                                         or return $editor->event;
1041                         }
1042
1043                 } else {
1044                         return OpenILS::Event->new('TITLE_LAST_COPY');
1045                 }
1046         }
1047
1048         return undef;
1049 }
1050
1051 sub create_copy {
1052         my( $editor, $vol, $copy ) = @_;
1053
1054         my $existing = $editor->search_asset_copy(
1055                 { barcode => $copy->barcode } );
1056         
1057         return OpenILS::Event->new('ITEM_BARCODE_EXISTS') if @$existing;
1058
1059         $copy->clear_id;
1060         $copy->creator($editor->requestor->id);
1061         $copy->create_date('now');
1062
1063         $editor->create_asset_copy(
1064                 $copy, {checkperm=>1, permorg=>$vol->owning_lib})
1065                 or return $editor->event;
1066
1067         return undef;
1068 }
1069
1070 sub update_copy_stat_entries {
1071         my( $editor, $copy ) = @_;
1072
1073         my $evt;
1074         my $entries = $copy->stat_cat_entries;
1075         return undef unless ($entries and @$entries);
1076
1077         my $maps = $editor->search_asset_stat_cat_entry_copy_map({owning_copy=>$copy->id});
1078
1079         if(!$copy->isnew) {
1080                 # if there is no stat cat entry on the copy who's id matches the
1081                 # current map's id, remove the map from the database
1082                 for my $map (@$maps) {
1083                         if(! grep { $_->id == $map->stat_cat_entry } @$entries ) {
1084
1085                                 $logger->info("copy update found stale ".
1086                                         "stat cat entry map ".$map->id. " on copy ".$copy->id);
1087
1088                                 $editor->delete_asset_stat_cat_entry_copy_map($map)
1089                                         or return $editor->event;
1090                         }
1091                 }
1092         }
1093         
1094         # go through the stat cat update/create process
1095         for my $entry (@$entries) { 
1096
1097                 # if this link already exists in the DB, don't attempt to re-create it
1098                 next if( grep{$_->stat_cat_entry == $entry->id} @$maps );
1099         
1100                 my $new_map = Fieldmapper::asset::stat_cat_entry_copy_map->new();
1101                 
1102                 $new_map->stat_cat( $entry->stat_cat );
1103                 $new_map->stat_cat_entry( $entry->id );
1104                 $new_map->owning_copy( $copy->id );
1105
1106                 $editor->create_asset_stat_cat_entry_copy_map($new_map)
1107                         or return $editor->event;
1108
1109                 $logger->info("copy update created new stat cat entry map ".$editor->data);
1110         }
1111
1112         return undef;
1113 }
1114
1115
1116 sub create_volume {
1117         my( $override, $editor, $vol ) = @_;
1118         my $evt;
1119
1120
1121         # first lets see if there are any collisions
1122         my $vols = $editor->search_asset_call_number( { 
1123                         owning_lib      => $vol->owning_lib,
1124                         record          => $vol->record,
1125                         label                   => $vol->label,
1126                         deleted         => 'f'
1127                 }
1128         );
1129
1130         my $label = undef;
1131         if(@$vols) {
1132                 if($override) { 
1133                         $label = $vol->label;
1134                 } else {
1135                         return OpenILS::Event->new(
1136                                 'VOLUME_LABEL_EXISTS', payload => $vol->id);
1137                 }
1138         }
1139
1140         # create a temp label so we can create the volume, then de-dup it
1141         $vol->label( '__SYSTEM_TMP_'.time) if $label;
1142
1143         $vol->creator($editor->requestor->id);
1144         $vol->create_date('now');
1145         $vol->clear_id;
1146
1147         $editor->create_asset_call_number($vol) or return $editor->event;
1148
1149         if($label) {
1150                 # now restore the label and merge into the existing record
1151                 $vol->label($label);
1152                 (undef, $evt) = 
1153                         OpenILS::Application::Cat::Merge::merge_volumes($editor, [$vol], $$vols[0]);
1154                 return $evt if $evt;
1155         }
1156
1157         return undef;
1158 }
1159
1160
1161
1162
1163
1164
1165 1;