1 use strict; use warnings;
2 package OpenILS::Application::Cat;
3 use OpenILS::Application::AppUtils;
4 use OpenILS::Application;
5 use OpenILS::Application::Cat::Merge;
6 use OpenILS::Application::Cat::Authority;
7 use OpenILS::Application::Cat::BibCommon;
8 use OpenILS::Application::Cat::AssetCommon;
9 use base qw/OpenILS::Application/;
10 use Time::HiRes qw(time);
11 use OpenSRF::EX qw(:try);
12 use OpenSRF::Utils::JSON;
13 use OpenILS::Utils::Fieldmapper;
15 use OpenILS::Const qw/:const/;
18 use Unicode::Normalize;
20 use OpenILS::Utils::FlatXML;
21 use OpenILS::Utils::CStoreEditor q/:funcs/;
23 use OpenSRF::Utils::SettingsClient;
24 use OpenSRF::Utils::Logger qw($logger);
25 use OpenSRF::AppSession;
27 my $U = "OpenILS::Application::AppUtils";
31 __PACKAGE__->register_method(
32 method => "retrieve_marc_template",
33 api_name => "open-ils.cat.biblio.marc_template.retrieve",
35 Returns a MARC 'record tree' based on a set of pre-defined templates.
36 Templates include : book
39 sub retrieve_marc_template {
40 my( $self, $client, $type ) = @_;
41 return $marctemplates{$type} if defined($marctemplates{$type});
42 $marctemplates{$type} = _load_marc_template($type);
43 return $marctemplates{$type};
46 __PACKAGE__->register_method(
47 method => 'fetch_marc_template_types',
48 api_name => 'open-ils.cat.marc_template.types.retrieve'
51 my $marc_template_files;
53 sub fetch_marc_template_types {
54 my( $self, $conn ) = @_;
55 __load_marc_templates();
56 return [ keys %$marc_template_files ];
59 sub __load_marc_templates {
60 return if $marc_template_files;
61 if(!$conf) { $conf = OpenSRF::Utils::SettingsClient->new; }
63 $marc_template_files = $conf->config_value(
64 "apps", "open-ils.cat","app_settings", "marctemplates" );
66 $logger->info("Loaded marc templates: " . Dumper($marc_template_files));
69 sub _load_marc_template {
72 __load_marc_templates();
74 my $template = $$marc_template_files{$type};
75 open( F, $template ) or
76 throw OpenSRF::EX::ERROR ("Unable to open MARC template file: $template : $@");
80 my $xml = join('', @xml);
82 return XML::LibXML->new->parse_string($xml)->documentElement->toString;
87 __PACKAGE__->register_method(
88 method => 'fetch_bib_sources',
89 api_name => 'open-ils.cat.bib_sources.retrieve.all');
91 sub fetch_bib_sources {
92 return OpenILS::Application::Cat::BibCommon->fetch_bib_sources();
95 __PACKAGE__->register_method(
96 method => "create_record_xml",
97 api_name => "open-ils.cat.biblio.record.xml.create.override",
98 signature => q/@see open-ils.cat.biblio.record.xml.create/);
100 __PACKAGE__->register_method(
101 method => "create_record_xml",
102 api_name => "open-ils.cat.biblio.record.xml.create",
104 Inserts a new biblio with the given XML
108 sub create_record_xml {
109 my( $self, $client, $login, $xml, $source ) = @_;
111 my $override = 1 if $self->api_name =~ /override/;
113 my( $user_obj, $evt ) = $U->checksesperm($login, 'CREATE_MARC');
116 $logger->activity("user ".$user_obj->id." creating new MARC record");
118 my $meth = $self->method_lookup("open-ils.cat.biblio.record.xml.import");
120 $meth = $self->method_lookup(
121 "open-ils.cat.biblio.record.xml.import.override") if $override;
123 my ($s) = $meth->run($login, $xml, $source);
129 __PACKAGE__->register_method(
130 method => "biblio_record_replace_marc",
131 api_name => "open-ils.cat.biblio.record.xml.update",
134 Updates the XML for a given biblio record.
135 This does not change any other aspect of the record entry
136 exception the XML, the editor, and the edit date.
137 @return The update record object
141 __PACKAGE__->register_method(
142 method => 'biblio_record_replace_marc',
143 api_name => 'open-ils.cat.biblio.record.marc.replace',
145 @param auth The authtoken
146 @param recid The record whose MARC we're replacing
147 @param newxml The new xml to use
151 __PACKAGE__->register_method(
152 method => 'biblio_record_replace_marc',
153 api_name => 'open-ils.cat.biblio.record.marc.replace.override',
154 signature => q/@see open-ils.cat.biblio.record.marc.replace/
157 sub biblio_record_replace_marc {
158 my( $self, $conn, $auth, $recid, $newxml, $source ) = @_;
159 my $e = new_editor(authtoken=>$auth, xact=>1);
160 return $e->die_event unless $e->checkauth;
161 return $e->die_event unless $e->allowed('CREATE_MARC', $e->requestor->ws_ou);
163 my $res = OpenILS::Application::Cat::BibCommon->biblio_record_replace_marc(
164 $e, $recid, $newxml, $source,
165 $self->api_name =~ /replace/o,
166 $self->api_name =~ /override/o);
168 $e->commit unless $U->event_code($res);
172 __PACKAGE__->register_method(
173 method => "update_biblio_record_entry",
174 api_name => "open-ils.cat.biblio.record_entry.update",
176 Updates a biblio.record_entry
177 @param auth The authtoken
178 @param record The record with updated values
179 @return 1 on success, Event on error.
183 sub update_biblio_record_entry {
184 my($self, $conn, $auth, $record) = @_;
185 my $e = new_editor(authtoken=>$auth, xact=>1);
186 return $e->die_event unless $e->checkauth;
187 return $e->die_event unless $e->allowed('UPDATE_RECORD');
188 $e->update_biblio_record_entry($record) or return $e->die_event;
193 __PACKAGE__->register_method(
194 method => "undelete_biblio_record_entry",
195 api_name => "open-ils.cat.biblio.record_entry.undelete",
197 Un-deletes a record and sets active=true
198 @param auth The authtoken
199 @param record The record_id to ressurect
200 @return 1 on success, Event on error.
203 sub undelete_biblio_record_entry {
204 my($self, $conn, $auth, $record_id) = @_;
205 my $e = new_editor(authtoken=>$auth, xact=>1);
206 return $e->die_event unless $e->checkauth;
207 return $e->die_event unless $e->allowed('UPDATE_RECORD');
209 my $record = $e->retrieve_biblio_record_entry($record_id)
210 or return $e->die_event;
211 $record->deleted('f');
212 $record->active('t');
214 # no 2 non-deleted records can have the same tcn_value
215 my $existing = $e->search_biblio_record_entry(
217 tcn_value => $record->tcn_value,
218 id => {'!=' => $record_id}
220 return OpenILS::Event->new('TCN_EXISTS') if @$existing;
222 $e->update_biblio_record_entry($record) or return $e->die_event;
228 __PACKAGE__->register_method(
229 method => "biblio_record_xml_import",
230 api_name => "open-ils.cat.biblio.record.xml.import.override",
231 signature => q/@see open-ils.cat.biblio.record.xml.import/);
233 __PACKAGE__->register_method(
234 method => "biblio_record_xml_import",
235 api_name => "open-ils.cat.biblio.record.xml.import",
236 notes => <<" NOTES");
237 Takes a marcxml record and imports the record into the database. In this
238 case, the marcxml record is assumed to be a complete record (i.e. valid
239 MARC). The title control number is taken from (whichever comes first)
240 tags 001, 039[ab], 020a, 022a, 010, 035a and whichever does not already exist
242 user_session must have IMPORT_MARC permissions
246 sub biblio_record_xml_import {
247 my( $self, $client, $authtoken, $xml, $source, $auto_tcn) = @_;
248 my $e = new_editor(xact=>1, authtoken=>$authtoken);
249 return $e->die_event unless $e->checkauth;
250 return $e->die_event unless $e->allowed('IMPORT_MARC', $e->requestor->ws_ou);
252 my $res = OpenILS::Application::Cat::BibCommon->biblio_record_xml_import(
253 $e, $xml, $source, $auto_tcn, $self->api_name =~ /override/);
255 $e->commit unless $U->event_code($res);
259 __PACKAGE__->register_method(
260 method => "biblio_record_record_metadata",
261 api_name => "open-ils.cat.biblio.record.metadata.retrieve",
263 argc => 1, #(session_id, biblio_tree )
264 notes => "Walks the tree and commits any changed nodes " .
265 "adds any new nodes, and deletes any deleted nodes",
268 sub biblio_record_record_metadata {
269 my( $self, $client, $authtoken, $ids ) = @_;
271 return [] unless $ids and @$ids;
273 my $editor = new_editor(authtoken => $authtoken);
274 return $editor->event unless $editor->checkauth;
275 return $editor->event unless $editor->allowed('VIEW_USER');
280 return $editor->event unless
281 my $rec = $editor->retrieve_biblio_record_entry($_);
282 $rec->creator($editor->retrieve_actor_user($rec->creator));
283 $rec->editor($editor->retrieve_actor_user($rec->editor));
284 $rec->clear_marc; # slim the record down
285 push( @results, $rec );
293 __PACKAGE__->register_method(
294 method => "biblio_record_marc_cn",
295 api_name => "open-ils.cat.biblio.record.marc_cn.retrieve",
296 argc => 1, #(bib id )
299 sub biblio_record_marc_cn {
300 my( $self, $client, $id ) = @_;
302 my $session = OpenSRF::AppSession->create("open-ils.cstore");
304 ->request("open-ils.cstore.direct.biblio.record_entry.retrieve", $id )
308 my $doc = XML::LibXML->new->parse_string($marc);
309 $doc->documentElement->setNamespace( "http://www.loc.gov/MARC21/slim", "marc", 1 );
312 for my $tag ( qw/050 055 060 070 080 082 086 088 090 092 096 098 099/ ) {
313 my @node = $doc->findnodes("//marc:datafield[\@tag='$tag']");
315 my $cn = $x->findvalue("marc:subfield[\@code='a' or \@code='b']");
316 push @res, {$tag => $cn} if ($cn);
324 __PACKAGE__->register_method(
325 method => "orgs_for_title",
327 api_name => "open-ils.cat.actor.org_unit.retrieve_by_title"
331 my( $self, $client, $record_id ) = @_;
333 my $vols = $U->simple_scalar_request(
335 "open-ils.cstore.direct.asset.call_number.search.atomic",
336 { record => $record_id, deleted => 'f' });
338 my $orgs = { map {$_->owning_lib => 1 } @$vols };
339 return [ keys %$orgs ];
343 __PACKAGE__->register_method(
344 method => "retrieve_copies",
346 api_name => "open-ils.cat.asset.copy_tree.retrieve");
348 __PACKAGE__->register_method(
349 method => "retrieve_copies",
350 api_name => "open-ils.cat.asset.copy_tree.global.retrieve");
352 # user_session may be null/undef
353 sub retrieve_copies {
355 my( $self, $client, $user_session, $docid, @org_ids ) = @_;
357 if(ref($org_ids[0])) { @org_ids = @{$org_ids[0]}; }
361 # grabbing copy trees should be available for everyone..
362 if(!@org_ids and $user_session) {
364 OpenILS::Application::AppUtils->check_user_session( $user_session ); #throws EX on error
365 @org_ids = ($user_obj->home_ou);
368 if( $self->api_name =~ /global/ ) {
369 return _build_volume_list( { record => $docid, deleted => 'f' } );
374 for my $orgid (@org_ids) {
375 my $vols = _build_volume_list(
376 { record => $docid, owning_lib => $orgid, deleted => 'f' } );
377 push( @all_vols, @$vols );
387 sub _build_volume_list {
388 my $search_hash = shift;
390 $search_hash->{deleted} = 'f';
391 my $e = new_editor();
393 my $vols = $e->search_asset_call_number($search_hash);
397 for my $volume (@$vols) {
399 my $copies = $e->search_asset_copy(
400 { call_number => $volume->id , deleted => 'f' });
402 $copies = [ sort { $a->barcode cmp $b->barcode } @$copies ];
404 for my $c (@$copies) {
405 if( $c->status == OILS_COPY_STATUS_CHECKED_OUT ) {
407 $e->search_action_circulation(
409 { target_copy => $c->id },
411 order_by => { circ => 'xact_start desc' },
420 $volume->copies($copies);
421 push( @volumes, $volume );
424 #$session->disconnect();
430 __PACKAGE__->register_method(
431 method => "fleshed_copy_update",
432 api_name => "open-ils.cat.asset.copy.fleshed.batch.update",);
434 __PACKAGE__->register_method(
435 method => "fleshed_copy_update",
436 api_name => "open-ils.cat.asset.copy.fleshed.batch.update.override",);
439 sub fleshed_copy_update {
440 my( $self, $conn, $auth, $copies, $delete_stats ) = @_;
441 return 1 unless ref $copies;
442 my( $reqr, $evt ) = $U->checkses($auth);
444 my $editor = new_editor(requestor => $reqr, xact => 1);
445 my $override = $self->api_name =~ /override/;
446 $evt = OpenILS::Application::Cat::AssetCommon->update_fleshed_copies(
447 $editor, $override, undef, $copies, $delete_stats);
449 $logger->info("fleshed copy update failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
454 $logger->info("fleshed copy update successfully updated ".scalar(@$copies)." copies");
459 __PACKAGE__->register_method(
461 api_name => 'open-ils.cat.biblio.records.merge',
463 Merges a group of records
464 @param auth The login session key
465 @param master The id of the record all other records should be merged into
466 @param records Array of records to be merged into the master record
467 @return 1 on success, Event on error.
472 my( $self, $conn, $auth, $master, $records ) = @_;
473 my( $reqr, $evt ) = $U->checkses($auth);
475 my $editor = new_editor( requestor => $reqr, xact => 1 );
476 my $v = OpenILS::Application::Cat::Merge::merge_records($editor, $master, $records);
479 # tell the client the merge is complete, then merge the holds
480 $conn->respond_complete(1);
481 merge_holds($master, $records);
486 my($master, $records) = @_;
487 return unless $master and @$records;
488 return if @$records == 1 and $master == $$records[0];
490 my $e = new_editor(xact=>1);
491 my $holds = $e->search_action_hold_request(
492 { cancel_time => undef,
493 fulfillment_time => undef,
500 for my $hold_id (@$holds) {
502 my $hold = $e->retrieve_action_hold_request($hold_id);
504 $logger->info("Changing hold ".$hold->id.
505 " target from ".$hold->target." to $master in record merge");
507 $hold->target($master);
508 unless($e->update_action_hold_request($hold)) {
510 $logger->error("Error updating hold ". $evt->textcode .":". $evt->desc .":". $evt->stacktrace);
519 __PACKAGE__->register_method(
520 method => "fleshed_volume_update",
521 api_name => "open-ils.cat.asset.volume.fleshed.batch.update",);
523 __PACKAGE__->register_method(
524 method => "fleshed_volume_update",
525 api_name => "open-ils.cat.asset.volume.fleshed.batch.update.override",);
527 sub fleshed_volume_update {
528 my( $self, $conn, $auth, $volumes, $delete_stats ) = @_;
529 my( $reqr, $evt ) = $U->checkses($auth);
532 my $override = ($self->api_name =~ /override/);
533 my $editor = new_editor( requestor => $reqr, xact => 1 );
535 for my $vol (@$volumes) {
536 $logger->info("vol-update: investigating volume ".$vol->id);
538 $vol->editor($reqr->id);
539 $vol->edit_date('now');
541 my $copies = $vol->copies;
544 $vol->editor($editor->requestor->id);
545 $vol->edit_date('now');
547 if( $vol->isdeleted ) {
549 $logger->info("vol-update: deleting volume");
550 my $cs = $editor->search_asset_copy(
551 { call_number => $vol->id, deleted => 'f' } );
552 return OpenILS::Event->new(
553 'VOLUME_NOT_EMPTY', payload => $vol->id ) if @$cs;
556 return $editor->event unless
557 $editor->update_asset_call_number($vol);
560 } elsif( $vol->isnew ) {
561 $logger->info("vol-update: creating volume");
562 $evt = OpenILS::Application::Cat::AssetCommon->create_volume( $override, $editor, $vol );
565 } elsif( $vol->ischanged ) {
566 $logger->info("vol-update: update volume");
567 $evt = update_volume($vol, $editor);
571 # now update any attached copies
572 if( $copies and @$copies and !$vol->isdeleted ) {
573 $_->call_number($vol->id) for @$copies;
574 $evt = OpenILS::Application::Cat::AssetCommon->update_fleshed_copies(
575 $editor, $override, $vol, $copies, $delete_stats);
581 return scalar(@$volumes);
590 return $evt if ( $evt = OpenILS::Application::Cat::AssetCommon->org_cannot_have_vols($editor, $vol->owning_lib) );
592 my $vols = $editor->search_asset_call_number( {
593 owning_lib => $vol->owning_lib,
594 record => $vol->record,
595 label => $vol->label,
600 # There exists a different volume in the DB with the same properties
601 return OpenILS::Event->new('VOLUME_LABEL_EXISTS', payload => $vol->id)
602 if grep { $_->id ne $vol->id } @$vols;
604 return $editor->event unless $editor->update_asset_call_number($vol);
610 __PACKAGE__->register_method (
611 method => 'delete_bib_record',
612 api_name => 'open-ils.cat.biblio.record_entry.delete');
614 sub delete_bib_record {
615 my($self, $conn, $auth, $rec_id) = @_;
616 my $e = new_editor(xact=>1, authtoken=>$auth);
617 return $e->die_event unless $e->checkauth;
618 return $e->die_event unless $e->allowed('DELETE_RECORD', $e->requestor->ws_ou);
619 my $vols = $e->search_asset_call_number({record=>$rec_id, deleted=>'f'});
620 return OpenILS::Event->new('RECORD_NOT_EMPTY', payload=>$rec_id) if @$vols;
621 my $evt = OpenILS::Application::Cat::BibCommon->delete_rec($e, $rec_id);
622 if($evt) { $e->rollback; return $evt; }
629 __PACKAGE__->register_method (
630 method => 'batch_volume_transfer',
631 api_name => 'open-ils.cat.asset.volume.batch.transfer',
634 __PACKAGE__->register_method (
635 method => 'batch_volume_transfer',
636 api_name => 'open-ils.cat.asset.volume.batch.transfer.override',
640 sub batch_volume_transfer {
641 my( $self, $conn, $auth, $args ) = @_;
644 my $rec = $$args{docid};
645 my $o_lib = $$args{lib};
646 my $vol_ids = $$args{volumes};
648 my $override = 1 if $self->api_name =~ /override/;
650 $logger->info("merge: transferring volumes to lib=$o_lib and record=$rec");
652 my $e = new_editor(authtoken => $auth, xact =>1);
653 return $e->event unless $e->checkauth;
654 return $e->event unless $e->allowed('UPDATE_VOLUME', $o_lib);
656 my $dorg = $e->retrieve_actor_org_unit($o_lib)
659 my $ou_type = $e->retrieve_actor_org_unit_type($dorg->ou_type)
662 return $evt if ( $evt = OpenILS::Application::Cat::AssetCommon->org_cannot_have_vols($e, $o_lib) );
664 my $vols = $e->batch_retrieve_asset_call_number($vol_ids);
669 for my $vol (@$vols) {
671 # if we've already looked at this volume, go to the next
672 next if !$vol or grep { $vol->id == $_ } @seen;
674 # grab all of the volumes in the list that have
675 # the same label so they can be merged
676 my @all = grep { $_->label eq $vol->label } @$vols;
678 # take note of the fact that we've looked at this set of volumes
679 push( @seen, $_->id ) for @all;
680 push( @rec_ids, $_->record ) for @all;
682 # for each volume, see if there are any copies that have a
683 # remote circ_lib (circ_lib != vol->owning_lib and != $o_lib ).
685 unless( $override ) {
688 $logger->debug("merge: searching for copies with remote circ_lib for volume ".$v->id);
690 call_number => $v->id,
691 circ_lib => { "not in" => [ $o_lib, $v->owning_lib ] },
695 my $copies = $e->search_asset_copy($args, {idlist=>1});
697 # if the copy's circ_lib matches the destination lib,
699 return OpenILS::Event->new('COPY_REMOTE_CIRC_LIB') if @$copies;
703 # see if there is a volume at the destination lib that
704 # already has the requested label
705 my $existing_vol = $e->search_asset_call_number(
707 label => $vol->label,
714 if( $existing_vol ) {
716 if( grep { $_->id == $existing_vol->id } @all ) {
717 # this volume is already accounted for in our list of volumes to merge
718 $existing_vol = undef;
721 # this volume exists on the destination record/owning_lib and must
722 # be used as the destination for merging
723 $logger->debug("merge: volume already exists at destination record: ".
724 $existing_vol->id.' : '.$existing_vol->label) if $existing_vol;
728 if( @all > 1 || $existing_vol ) {
729 $logger->info("merge: found collisions in volume transfer");
730 my @args = ($e, \@all);
731 @args = ($e, \@all, $existing_vol) if $existing_vol;
732 ($vol, $evt) = OpenILS::Application::Cat::Merge::merge_volumes(@args);
736 if( !$existing_vol ) {
738 $vol->owning_lib($o_lib);
740 $vol->editor($e->requestor->id);
741 $vol->edit_date('now');
743 $logger->info("merge: updating volume ".$vol->id);
744 $e->update_asset_call_number($vol) or return $e->event;
747 $logger->info("merge: bypassing volume update because existing volume used as target");
750 # regardless of what volume was used as the destination,
751 # update any copies that have moved over to the new lib
752 my $copies = $e->search_asset_copy({call_number=>$vol->id, deleted => 'f'});
754 # update circ lib on the copies - make this a method flag?
755 for my $copy (@$copies) {
756 next if $copy->circ_lib == $o_lib;
757 $logger->info("merge: transfer moving circ lib on copy ".$copy->id);
758 $copy->circ_lib($o_lib);
759 $copy->editor($e->requestor->id);
760 $copy->edit_date('now');
761 $e->update_asset_copy($copy) or return $e->event;
764 # Now see if any empty records need to be deleted after all of this
767 $logger->debug("merge: seeing if we should delete record $_...");
768 $evt = OpenILS::Application::Cat::BibCommon->delete_rec($e, $_)
769 if OpenILS::Application::Cat::BibCommon->title_is_empty($e, $_);
774 $logger->info("merge: transfer succeeded");
782 __PACKAGE__->register_method(
783 api_name => 'open-ils.cat.call_number.find_or_create',
784 method => 'find_or_create_volume',
787 sub find_or_create_volume {
788 my( $self, $conn, $auth, $label, $record_id, $org_id ) = @_;
789 my $e = new_editor(authtoken=>$auth, xact=>1);
790 return $e->die_event unless $e->checkauth;
791 my ($vol, $evt, $exists) =
792 OpenILS::Application::Cat::AssetCommon->find_or_create_volume($e, $label, $record_id, $org_id);
794 $e->rollback if $exists;