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