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 base qw/OpenILS::Application/;
9 use Time::HiRes qw(time);
10 use OpenSRF::EX qw(:try);
11 use OpenSRF::Utils::JSON;
12 use OpenILS::Utils::Fieldmapper;
14 use OpenILS::Const qw/:const/;
17 use Unicode::Normalize;
19 use OpenILS::Utils::FlatXML;
20 use OpenILS::Utils::CStoreEditor q/:funcs/;
22 use OpenSRF::Utils::SettingsClient;
23 use OpenSRF::Utils::Logger qw($logger);
24 use OpenSRF::AppSession;
26 my $U = "OpenILS::Application::AppUtils";
30 __PACKAGE__->register_method(
31 method => "retrieve_marc_template",
32 api_name => "open-ils.cat.biblio.marc_template.retrieve",
34 Returns a MARC 'record tree' based on a set of pre-defined templates.
35 Templates include : book
38 sub retrieve_marc_template {
39 my( $self, $client, $type ) = @_;
40 return $marctemplates{$type} if defined($marctemplates{$type});
41 $marctemplates{$type} = _load_marc_template($type);
42 return $marctemplates{$type};
45 __PACKAGE__->register_method(
46 method => 'fetch_marc_template_types',
47 api_name => 'open-ils.cat.marc_template.types.retrieve'
50 my $marc_template_files;
52 sub fetch_marc_template_types {
53 my( $self, $conn ) = @_;
54 __load_marc_templates();
55 return [ keys %$marc_template_files ];
58 sub __load_marc_templates {
59 return if $marc_template_files;
60 if(!$conf) { $conf = OpenSRF::Utils::SettingsClient->new; }
62 $marc_template_files = $conf->config_value(
63 "apps", "open-ils.cat","app_settings", "marctemplates" );
65 $logger->info("Loaded marc templates: " . Dumper($marc_template_files));
68 sub _load_marc_template {
71 __load_marc_templates();
73 my $template = $$marc_template_files{$type};
74 open( F, $template ) or
75 throw OpenSRF::EX::ERROR ("Unable to open MARC template file: $template : $@");
79 my $xml = join('', @xml);
81 return XML::LibXML->new->parse_string($xml)->documentElement->toString;
85 sub bib_source_from_name {
87 $logger->debug("searching for bib source: $name");
91 my ($s) = grep { lc($_->source) eq lc($name) } @$__bib_sources;
98 __PACKAGE__->register_method(
99 method => 'fetch_bib_sources',
100 api_name => 'open-ils.cat.bib_sources.retrieve.all');
102 sub fetch_bib_sources {
103 $__bib_sources = new_editor()->retrieve_all_config_bib_source()
104 unless $__bib_sources;
105 return $__bib_sources;
110 __PACKAGE__->register_method(
111 method => "create_record_xml",
112 api_name => "open-ils.cat.biblio.record.xml.create.override",
113 signature => q/@see open-ils.cat.biblio.record.xml.create/);
115 __PACKAGE__->register_method(
116 method => "create_record_xml",
117 api_name => "open-ils.cat.biblio.record.xml.create",
119 Inserts a new biblio with the given XML
123 sub create_record_xml {
124 my( $self, $client, $login, $xml, $source ) = @_;
126 my $override = 1 if $self->api_name =~ /override/;
128 my( $user_obj, $evt ) = $U->checksesperm($login, 'CREATE_MARC');
131 $logger->activity("user ".$user_obj->id." creating new MARC record");
133 my $meth = $self->method_lookup("open-ils.cat.biblio.record.xml.import");
135 $meth = $self->method_lookup(
136 "open-ils.cat.biblio.record.xml.import.override") if $override;
138 my ($s) = $meth->run($login, $xml, $source);
144 __PACKAGE__->register_method(
145 method => "biblio_record_replace_marc",
146 api_name => "open-ils.cat.biblio.record.xml.update",
149 Updates the XML for a given biblio record.
150 This does not change any other aspect of the record entry
151 exception the XML, the editor, and the edit date.
152 @return The update record object
156 __PACKAGE__->register_method(
157 method => 'biblio_record_replace_marc',
158 api_name => 'open-ils.cat.biblio.record.marc.replace',
160 @param auth The authtoken
161 @param recid The record whose MARC we're replacing
162 @param newxml The new xml to use
166 __PACKAGE__->register_method(
167 method => 'biblio_record_replace_marc',
168 api_name => 'open-ils.cat.biblio.record.marc.replace.override',
169 signature => q/@see open-ils.cat.biblio.record.marc.replace/
172 sub biblio_record_replace_marc {
173 my( $self, $conn, $auth, $recid, $newxml, $source ) = @_;
174 my $e = new_editor(authtoken=>$auth, xact=>1);
175 return $e->die_event unless $e->checkauth;
176 return $e->die_event unless $e->allowed('CREATE_MARC', $e->requestor->ws_ou);
178 my $res = OpenILS::Application::Cat::BibCommon->biblio_record_replace_marc(
179 $e, $recid, $newxml, $source,
180 $self->api_name =~ /replace/o,
181 $self->api_name =~ /override/o);
183 $e->commit unless $U->event_code($res);
187 __PACKAGE__->register_method(
188 method => "update_biblio_record_entry",
189 api_name => "open-ils.cat.biblio.record_entry.update",
191 Updates a biblio.record_entry
192 @param auth The authtoken
193 @param record The record with updated values
194 @return 1 on success, Event on error.
198 sub update_biblio_record_entry {
199 my($self, $conn, $auth, $record) = @_;
200 my $e = new_editor(authtoken=>$auth, xact=>1);
201 return $e->die_event unless $e->checkauth;
202 return $e->die_event unless $e->allowed('UPDATE_RECORD');
203 $e->update_biblio_record_entry($record) or return $e->die_event;
208 __PACKAGE__->register_method(
209 method => "undelete_biblio_record_entry",
210 api_name => "open-ils.cat.biblio.record_entry.undelete",
212 Un-deletes a record and sets active=true
213 @param auth The authtoken
214 @param record The record_id to ressurect
215 @return 1 on success, Event on error.
218 sub undelete_biblio_record_entry {
219 my($self, $conn, $auth, $record_id) = @_;
220 my $e = new_editor(authtoken=>$auth, xact=>1);
221 return $e->die_event unless $e->checkauth;
222 return $e->die_event unless $e->allowed('UPDATE_RECORD');
224 my $record = $e->retrieve_biblio_record_entry($record_id)
225 or return $e->die_event;
226 $record->deleted('f');
227 $record->active('t');
229 # no 2 non-deleted records can have the same tcn_value
230 my $existing = $e->search_biblio_record_entry(
232 tcn_value => $record->tcn_value,
233 id => {'!=' => $record_id}
235 return OpenILS::Event->new('TCN_EXISTS') if @$existing;
237 $e->update_biblio_record_entry($record) or return $e->die_event;
243 __PACKAGE__->register_method(
244 method => "biblio_record_xml_import",
245 api_name => "open-ils.cat.biblio.record.xml.import.override",
246 signature => q/@see open-ils.cat.biblio.record.xml.import/);
248 __PACKAGE__->register_method(
249 method => "biblio_record_xml_import",
250 api_name => "open-ils.cat.biblio.record.xml.import",
251 notes => <<" NOTES");
252 Takes a marcxml record and imports the record into the database. In this
253 case, the marcxml record is assumed to be a complete record (i.e. valid
254 MARC). The title control number is taken from (whichever comes first)
255 tags 001, 039[ab], 020a, 022a, 010, 035a and whichever does not already exist
257 user_session must have IMPORT_MARC permissions
261 sub biblio_record_xml_import {
262 my( $self, $client, $authtoken, $xml, $source, $auto_tcn) = @_;
263 my $e = new_editor(xact=>1, authtoken=>$authtoken);
264 return $e->die_event unless $e->checkauth;
265 return $e->die_event unless $e->allowed('IMPORT_MARC', $e->requestor->ws_ou);
267 my $res = OpenILS::Application::Cat::BibCommon->biblio_record_xml_import(
268 $e, $xml, $source, $auto_tcn, $self->api_name =~ /override/);
270 $e->commit unless $U->event_code($res);
274 __PACKAGE__->register_method(
275 method => "biblio_record_record_metadata",
276 api_name => "open-ils.cat.biblio.record.metadata.retrieve",
278 argc => 1, #(session_id, biblio_tree )
279 notes => "Walks the tree and commits any changed nodes " .
280 "adds any new nodes, and deletes any deleted nodes",
283 sub biblio_record_record_metadata {
284 my( $self, $client, $authtoken, $ids ) = @_;
286 return [] unless $ids and @$ids;
288 my $editor = new_editor(authtoken => $authtoken);
289 return $editor->event unless $editor->checkauth;
290 return $editor->event unless $editor->allowed('VIEW_USER');
295 return $editor->event unless
296 my $rec = $editor->retrieve_biblio_record_entry($_);
297 $rec->creator($editor->retrieve_actor_user($rec->creator));
298 $rec->editor($editor->retrieve_actor_user($rec->editor));
299 $rec->clear_marc; # slim the record down
300 push( @results, $rec );
308 __PACKAGE__->register_method(
309 method => "biblio_record_marc_cn",
310 api_name => "open-ils.cat.biblio.record.marc_cn.retrieve",
311 argc => 1, #(bib id )
314 sub biblio_record_marc_cn {
315 my( $self, $client, $id ) = @_;
317 my $session = OpenSRF::AppSession->create("open-ils.cstore");
319 ->request("open-ils.cstore.direct.biblio.record_entry.retrieve", $id )
323 my $doc = XML::LibXML->new->parse_string($marc);
324 $doc->documentElement->setNamespace( "http://www.loc.gov/MARC21/slim", "marc", 1 );
327 for my $tag ( qw/050 055 060 070 080 082 086 088 090 092 096 098 099/ ) {
328 my @node = $doc->findnodes("//marc:datafield[\@tag='$tag']");
330 my $cn = $x->findvalue("marc:subfield[\@code='a' or \@code='b']");
331 push @res, {$tag => $cn} if ($cn);
339 __PACKAGE__->register_method(
340 method => "orgs_for_title",
342 api_name => "open-ils.cat.actor.org_unit.retrieve_by_title"
346 my( $self, $client, $record_id ) = @_;
348 my $vols = $U->simple_scalar_request(
350 "open-ils.cstore.direct.asset.call_number.search.atomic",
351 { record => $record_id, deleted => 'f' });
353 my $orgs = { map {$_->owning_lib => 1 } @$vols };
354 return [ keys %$orgs ];
358 __PACKAGE__->register_method(
359 method => "retrieve_copies",
361 api_name => "open-ils.cat.asset.copy_tree.retrieve");
363 __PACKAGE__->register_method(
364 method => "retrieve_copies",
365 api_name => "open-ils.cat.asset.copy_tree.global.retrieve");
367 # user_session may be null/undef
368 sub retrieve_copies {
370 my( $self, $client, $user_session, $docid, @org_ids ) = @_;
372 if(ref($org_ids[0])) { @org_ids = @{$org_ids[0]}; }
376 # grabbing copy trees should be available for everyone..
377 if(!@org_ids and $user_session) {
379 OpenILS::Application::AppUtils->check_user_session( $user_session ); #throws EX on error
380 @org_ids = ($user_obj->home_ou);
383 if( $self->api_name =~ /global/ ) {
384 return _build_volume_list( { record => $docid, deleted => 'f' } );
389 for my $orgid (@org_ids) {
390 my $vols = _build_volume_list(
391 { record => $docid, owning_lib => $orgid, deleted => 'f' } );
392 push( @all_vols, @$vols );
402 sub _build_volume_list {
403 my $search_hash = shift;
405 $search_hash->{deleted} = 'f';
406 my $e = new_editor();
408 my $vols = $e->search_asset_call_number($search_hash);
412 for my $volume (@$vols) {
414 my $copies = $e->search_asset_copy(
415 { call_number => $volume->id , deleted => 'f' });
417 $copies = [ sort { $a->barcode cmp $b->barcode } @$copies ];
419 for my $c (@$copies) {
420 if( $c->status == OILS_COPY_STATUS_CHECKED_OUT ) {
422 $e->search_action_circulation(
424 { target_copy => $c->id },
426 order_by => { circ => 'xact_start desc' },
435 $volume->copies($copies);
436 push( @volumes, $volume );
439 #$session->disconnect();
445 __PACKAGE__->register_method(
446 method => "fleshed_copy_update",
447 api_name => "open-ils.cat.asset.copy.fleshed.batch.update",);
449 __PACKAGE__->register_method(
450 method => "fleshed_copy_update",
451 api_name => "open-ils.cat.asset.copy.fleshed.batch.update.override",);
454 sub fleshed_copy_update {
455 my( $self, $conn, $auth, $copies, $delete_stats ) = @_;
456 return 1 unless ref $copies;
457 my( $reqr, $evt ) = $U->checkses($auth);
459 my $editor = new_editor(requestor => $reqr, xact => 1);
460 my $override = $self->api_name =~ /override/;
461 $evt = update_fleshed_copies($editor, $override, undef, $copies, $delete_stats);
463 $logger->info("fleshed copy update failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
468 $logger->info("fleshed copy update successfully updated ".scalar(@$copies)." copies");
473 __PACKAGE__->register_method(
475 api_name => 'open-ils.cat.biblio.records.merge',
477 Merges a group of records
478 @param auth The login session key
479 @param master The id of the record all other records should be merged into
480 @param records Array of records to be merged into the master record
481 @return 1 on success, Event on error.
486 my( $self, $conn, $auth, $master, $records ) = @_;
487 my( $reqr, $evt ) = $U->checkses($auth);
489 my $editor = new_editor( requestor => $reqr, xact => 1 );
490 my $v = OpenILS::Application::Cat::Merge::merge_records($editor, $master, $records);
493 # tell the client the merge is complete, then merge the holds
494 $conn->respond_complete(1);
495 merge_holds($master, $records);
500 my($master, $records) = @_;
501 return unless $master and @$records;
502 return if @$records == 1 and $master == $$records[0];
504 my $e = new_editor(xact=>1);
505 my $holds = $e->search_action_hold_request(
506 { cancel_time => undef,
507 fulfillment_time => undef,
514 for my $hold_id (@$holds) {
516 my $hold = $e->retrieve_action_hold_request($hold_id);
518 $logger->info("Changing hold ".$hold->id.
519 " target from ".$hold->target." to $master in record merge");
521 $hold->target($master);
522 unless($e->update_action_hold_request($hold)) {
524 $logger->error("Error updating hold ". $evt->textcode .":". $evt->desc .":". $evt->stacktrace);
535 # ---------------------------------------------------------------------------
536 # returns true if the given title (id) has no un-deleted volumes or
537 # copies attached. If a context volume is defined, a record
538 # is considered empty only if the context volume is the only
539 # remaining volume on the record.
540 # ---------------------------------------------------------------------------
542 my( $editor, $rid, $vol_id ) = @_;
544 return 0 if $rid == OILS_PRECAT_RECORD;
546 my $cnlist = $editor->search_asset_call_number(
547 { record => $rid, deleted => 'f' }, { idlist => 1 } );
549 return 1 unless @$cnlist; # no attached volumes
550 return 0 if @$cnlist > 1; # multiple attached volumes
551 return 0 unless $$cnlist[0] == $vol_id; # attached volume is not the context vol.
553 # see if the sole remaining context volume has any attached copies
554 for my $cn (@$cnlist) {
555 my $copylist = $editor->search_asset_copy(
557 { call_number => $cn, deleted => 'f' },
560 return 0 if @$copylist; # false if we find any copies
567 __PACKAGE__->register_method(
568 method => "fleshed_volume_update",
569 api_name => "open-ils.cat.asset.volume.fleshed.batch.update",);
571 __PACKAGE__->register_method(
572 method => "fleshed_volume_update",
573 api_name => "open-ils.cat.asset.volume.fleshed.batch.update.override",);
575 sub fleshed_volume_update {
576 my( $self, $conn, $auth, $volumes, $delete_stats ) = @_;
577 my( $reqr, $evt ) = $U->checkses($auth);
580 my $override = ($self->api_name =~ /override/);
581 my $editor = new_editor( requestor => $reqr, xact => 1 );
583 for my $vol (@$volumes) {
584 $logger->info("vol-update: investigating volume ".$vol->id);
586 $vol->editor($reqr->id);
587 $vol->edit_date('now');
589 my $copies = $vol->copies;
592 $vol->editor($editor->requestor->id);
593 $vol->edit_date('now');
595 if( $vol->isdeleted ) {
597 $logger->info("vol-update: deleting volume");
598 my $cs = $editor->search_asset_copy(
599 { call_number => $vol->id, deleted => 'f' } );
600 return OpenILS::Event->new(
601 'VOLUME_NOT_EMPTY', payload => $vol->id ) if @$cs;
604 return $editor->event unless
605 $editor->update_asset_call_number($vol);
608 } elsif( $vol->isnew ) {
609 $logger->info("vol-update: creating volume");
610 $evt = create_volume( $override, $editor, $vol );
613 } elsif( $vol->ischanged ) {
614 $logger->info("vol-update: update volume");
615 $evt = update_volume($vol, $editor);
619 # now update any attached copies
620 if( $copies and @$copies and !$vol->isdeleted ) {
621 $_->call_number($vol->id) for @$copies;
622 $evt = update_fleshed_copies( $editor, $override, $vol, $copies, $delete_stats );
628 return scalar(@$volumes);
637 return $evt if ( $evt = org_cannot_have_vols($editor, $vol->owning_lib) );
639 my $vols = $editor->search_asset_call_number( {
640 owning_lib => $vol->owning_lib,
641 record => $vol->record,
642 label => $vol->label,
647 # There exists a different volume in the DB with the same properties
648 return OpenILS::Event->new('VOLUME_LABEL_EXISTS', payload => $vol->id)
649 if grep { $_->id ne $vol->id } @$vols;
651 return $editor->event unless $editor->update_asset_call_number($vol);
658 my( $vol, $copy ) = @_;
659 my $org = $vol->owning_lib;
660 if( $vol->id == OILS_PRECAT_CALL_NUMBER ) {
661 $org = ref($copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
663 $logger->debug("using copy perm org $org");
668 # this does the actual work
669 sub update_fleshed_copies {
670 my( $editor, $override, $vol, $copies, $delete_stats ) = @_;
673 my $fetchvol = ($vol) ? 0 : 1;
676 $cache{$vol->id} = $vol if $vol;
678 for my $copy (@$copies) {
680 my $copyid = $copy->id;
681 $logger->info("vol-update: inspecting copy $copyid");
683 if( !($vol = $cache{$copy->call_number}) ) {
684 $vol = $cache{$copy->call_number} =
685 $editor->retrieve_asset_call_number($copy->call_number);
686 return $editor->event unless $vol;
689 return $editor->event unless
690 $editor->allowed('UPDATE_COPY', copy_perm_org($vol, $copy));
692 $copy->editor($editor->requestor->id);
693 $copy->edit_date('now');
695 $copy->status( $copy->status->id ) if ref($copy->status);
696 $copy->location( $copy->location->id ) if ref($copy->location);
697 $copy->circ_lib( $copy->circ_lib->id ) if ref($copy->circ_lib);
699 my $sc_entries = $copy->stat_cat_entries;
700 $copy->clear_stat_cat_entries;
702 if( $copy->isdeleted ) {
703 $evt = delete_copy($editor, $override, $vol, $copy);
706 } elsif( $copy->isnew ) {
707 $evt = create_copy( $editor, $vol, $copy );
710 } elsif( $copy->ischanged ) {
712 $evt = update_copy( $editor, $override, $vol, $copy );
716 $copy->stat_cat_entries( $sc_entries );
717 $evt = update_copy_stat_entries($editor, $copy, $delete_stats);
721 $logger->debug("vol-update: done updating copy batch");
729 if(defined $copy->price) {
730 my $p = $copy->price || 0;
735 my $d = $copy->deposit_amount || 0;
737 $copy->deposit_amount($d);
742 my( $editor, $override, $vol, $copy ) = @_;
745 my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
746 return $evt if ( $evt = org_cannot_have_vols($editor, $org) );
748 $logger->info("vol-update: updating copy ".$copy->id);
749 my $orig_copy = $editor->retrieve_asset_copy($copy->id);
750 my $orig_vol = $editor->retrieve_asset_call_number($copy->call_number);
752 $copy->editor($editor->requestor->id);
753 $copy->edit_date('now');
755 $copy->age_protect( $copy->age_protect->id )
756 if ref $copy->age_protect;
758 fix_copy_price($copy);
760 return $editor->event unless $editor->update_asset_copy($copy);
761 return remove_empty_objects($editor, $override, $orig_vol);
765 sub remove_empty_objects {
766 my( $editor, $override, $vol ) = @_;
768 my $koe = $U->ou_ancestor_setting_value(
769 $editor->requestor->ws_ou, 'cat.bib.keep_on_empty', $editor);
770 my $aoe = $U->ou_ancestor_setting_value(
771 $editor->requestor->ws_ou, 'cat.bib.alert_on_empty', $editor);
773 if( title_is_empty($editor, $vol->record, $vol->id) ) {
775 # delete this volume if it's not already marked as deleted
776 unless( $U->is_true($vol->deleted) || $vol->isdeleted ) {
778 $vol->editor($editor->requestor->id);
779 $vol->edit_date('now');
780 $editor->update_asset_call_number($vol) or return $editor->event;
784 # delete the bib record if the keep-on-empty setting is not set
785 my $evt = delete_rec($editor, $vol->record);
789 # return the empty alert if the alert-on-empty setting is set
790 return OpenILS::Event->new('TITLE_LAST_COPY', payload => $vol->record ) if $aoe;
797 __PACKAGE__->register_method (
798 method => 'delete_bib_record',
799 api_name => 'open-ils.cat.biblio.record_entry.delete');
801 sub delete_bib_record {
802 my($self, $conn, $auth, $rec_id) = @_;
803 my $e = new_editor(xact=>1, authtoken=>$auth);
804 return $e->die_event unless $e->checkauth;
805 return $e->die_event unless $e->allowed('DELETE_RECORD', $e->requestor->ws_ou);
806 my $vols = $e->search_asset_call_number({record=>$rec_id, deleted=>'f'});
807 return OpenILS::Event->new('RECORD_NOT_EMPTY', payload=>$rec_id) if @$vols;
808 my $evt = delete_rec($e, $rec_id);
809 if($evt) { $e->rollback; return $evt; }
815 # marks a record as deleted
817 my( $editor, $rec_id ) = @_;
819 my $rec = $editor->retrieve_biblio_record_entry($rec_id)
820 or return $editor->event;
822 return undef if $U->is_true($rec->deleted);
826 $rec->editor( $editor->requestor->id );
827 $rec->edit_date('now');
828 $editor->update_biblio_record_entry($rec) or return $editor->event;
835 my( $editor, $override, $vol, $copy ) = @_;
837 return $editor->event unless
838 $editor->allowed('DELETE_COPY',copy_perm_org($vol, $copy));
840 my $stat = $U->copy_status($copy->status)->id;
843 return OpenILS::Event->new('COPY_DELETE_WARNING', payload => $copy->id )
844 if $stat == OILS_COPY_STATUS_CHECKED_OUT or
845 $stat == OILS_COPY_STATUS_IN_TRANSIT or
846 $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF or
847 $stat == OILS_COPY_STATUS_ILL;
850 $logger->info("vol-update: deleting copy ".$copy->id);
853 $copy->editor($editor->requestor->id);
854 $copy->edit_date('now');
855 $editor->update_asset_copy($copy) or return $editor->event;
857 # Delete any open transits for this copy
858 my $transits = $editor->search_action_transit_copy(
859 { target_copy=>$copy->id, dest_recv_time => undef } );
861 for my $t (@$transits) {
862 $editor->delete_action_transit_copy($t)
863 or return $editor->event;
866 return remove_empty_objects($editor, $override, $vol);
871 my( $editor, $vol, $copy ) = @_;
873 my $existing = $editor->search_asset_copy(
874 { barcode => $copy->barcode, deleted => 'f' } );
876 return OpenILS::Event->new('ITEM_BARCODE_EXISTS') if @$existing;
878 # see if the volume this copy references is marked as deleted
879 my $evol = $editor->retrieve_asset_call_number($copy->call_number)
880 or return $editor->event;
881 return OpenILS::Event->new('VOLUME_DELETED', vol => $evol->id)
882 if $U->is_true($evol->deleted);
885 my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
886 return $evt if ( $evt = org_cannot_have_vols($editor, $org) );
889 $copy->creator($editor->requestor->id);
890 $copy->create_date('now');
891 fix_copy_price($copy);
893 $editor->create_asset_copy($copy) or return $editor->event;
897 # if 'delete_stats' is true, the copy->stat_cat_entries data is
898 # treated as the authoritative list for the copy. existing entries
899 # that are not in said list will be deleted from the DB
900 sub update_copy_stat_entries {
901 my( $editor, $copy, $delete_stats ) = @_;
903 return undef if $copy->isdeleted;
904 return undef unless $copy->ischanged or $copy->isnew;
907 my $entries = $copy->stat_cat_entries;
909 if( $delete_stats ) {
910 $entries = ($entries and @$entries) ? $entries : [];
912 return undef unless ($entries and @$entries);
915 my $maps = $editor->search_asset_stat_cat_entry_copy_map({owning_copy=>$copy->id});
918 # if there is no stat cat entry on the copy who's id matches the
919 # current map's id, remove the map from the database
920 for my $map (@$maps) {
921 if(! grep { $_->id == $map->stat_cat_entry } @$entries ) {
923 $logger->info("copy update found stale ".
924 "stat cat entry map ".$map->id. " on copy ".$copy->id);
926 $editor->delete_asset_stat_cat_entry_copy_map($map)
927 or return $editor->event;
932 # go through the stat cat update/create process
933 for my $entry (@$entries) {
936 # if this link already exists in the DB, don't attempt to re-create it
937 next if( grep{$_->stat_cat_entry == $entry->id} @$maps );
939 my $new_map = Fieldmapper::asset::stat_cat_entry_copy_map->new();
941 my $sc = ref($entry->stat_cat) ? $entry->stat_cat->id : $entry->stat_cat;
943 $new_map->stat_cat( $sc );
944 $new_map->stat_cat_entry( $entry->id );
945 $new_map->owning_copy( $copy->id );
947 $editor->create_asset_stat_cat_entry_copy_map($new_map)
948 or return $editor->event;
950 $logger->info("copy update created new stat cat entry map ".$editor->data);
958 my( $override, $editor, $vol ) = @_;
961 return $evt if ( $evt = org_cannot_have_vols($editor, $vol->owning_lib) );
963 # see if the record this volume references is marked as deleted
964 my $rec = $editor->retrieve_biblio_record_entry($vol->record)
965 or return $editor->event;
966 return OpenILS::Event->new('BIB_RECORD_DELETED', rec => $rec->id)
967 if $U->is_true($rec->deleted);
969 # first lets see if there are any collisions
970 my $vols = $editor->search_asset_call_number( {
971 owning_lib => $vol->owning_lib,
972 record => $vol->record,
973 label => $vol->label,
980 # we've found an exising volume
982 $label = $vol->label;
984 return OpenILS::Event->new(
985 'VOLUME_LABEL_EXISTS', payload => $vol->id);
989 # create a temp label so we can create the new volume,
990 # then de-dup it with the existing volume
991 $vol->label( "__SYSTEM_TMP_$$".time) if $label;
993 $vol->creator($editor->requestor->id);
994 $vol->create_date('now');
995 $vol->editor($editor->requestor->id);
996 $vol->edit_date('now');
999 $editor->create_asset_call_number($vol) or return $editor->event;
1002 # now restore the label and merge into the existing record
1003 $vol->label($label);
1005 OpenILS::Application::Cat::Merge::merge_volumes($editor, [$vol], $$vols[0]);
1006 return $evt if $evt;
1013 __PACKAGE__->register_method (
1014 method => 'batch_volume_transfer',
1015 api_name => 'open-ils.cat.asset.volume.batch.transfer',
1018 __PACKAGE__->register_method (
1019 method => 'batch_volume_transfer',
1020 api_name => 'open-ils.cat.asset.volume.batch.transfer.override',
1024 sub batch_volume_transfer {
1025 my( $self, $conn, $auth, $args ) = @_;
1028 my $rec = $$args{docid};
1029 my $o_lib = $$args{lib};
1030 my $vol_ids = $$args{volumes};
1032 my $override = 1 if $self->api_name =~ /override/;
1034 $logger->info("merge: transferring volumes to lib=$o_lib and record=$rec");
1036 my $e = new_editor(authtoken => $auth, xact =>1);
1037 return $e->event unless $e->checkauth;
1038 return $e->event unless $e->allowed('UPDATE_VOLUME', $o_lib);
1040 my $dorg = $e->retrieve_actor_org_unit($o_lib)
1041 or return $e->event;
1043 my $ou_type = $e->retrieve_actor_org_unit_type($dorg->ou_type)
1044 or return $e->event;
1046 return $evt if ( $evt = org_cannot_have_vols($e, $o_lib) );
1048 my $vols = $e->batch_retrieve_asset_call_number($vol_ids);
1053 for my $vol (@$vols) {
1055 # if we've already looked at this volume, go to the next
1056 next if !$vol or grep { $vol->id == $_ } @seen;
1058 # grab all of the volumes in the list that have
1059 # the same label so they can be merged
1060 my @all = grep { $_->label eq $vol->label } @$vols;
1062 # take note of the fact that we've looked at this set of volumes
1063 push( @seen, $_->id ) for @all;
1064 push( @rec_ids, $_->record ) for @all;
1066 # for each volume, see if there are any copies that have a
1067 # remote circ_lib (circ_lib != vol->owning_lib and != $o_lib ).
1069 unless( $override ) {
1072 $logger->debug("merge: searching for copies with remote circ_lib for volume ".$v->id);
1074 call_number => $v->id,
1075 circ_lib => { "not in" => [ $o_lib, $v->owning_lib ] },
1079 my $copies = $e->search_asset_copy($args, {idlist=>1});
1081 # if the copy's circ_lib matches the destination lib,
1083 return OpenILS::Event->new('COPY_REMOTE_CIRC_LIB') if @$copies;
1087 # see if there is a volume at the destination lib that
1088 # already has the requested label
1089 my $existing_vol = $e->search_asset_call_number(
1091 label => $vol->label,
1093 owning_lib =>$o_lib,
1098 if( $existing_vol ) {
1100 if( grep { $_->id == $existing_vol->id } @all ) {
1101 # this volume is already accounted for in our list of volumes to merge
1102 $existing_vol = undef;
1105 # this volume exists on the destination record/owning_lib and must
1106 # be used as the destination for merging
1107 $logger->debug("merge: volume already exists at destination record: ".
1108 $existing_vol->id.' : '.$existing_vol->label) if $existing_vol;
1112 if( @all > 1 || $existing_vol ) {
1113 $logger->info("merge: found collisions in volume transfer");
1114 my @args = ($e, \@all);
1115 @args = ($e, \@all, $existing_vol) if $existing_vol;
1116 ($vol, $evt) = OpenILS::Application::Cat::Merge::merge_volumes(@args);
1117 return $evt if $evt;
1120 if( !$existing_vol ) {
1122 $vol->owning_lib($o_lib);
1124 $vol->editor($e->requestor->id);
1125 $vol->edit_date('now');
1127 $logger->info("merge: updating volume ".$vol->id);
1128 $e->update_asset_call_number($vol) or return $e->event;
1131 $logger->info("merge: bypassing volume update because existing volume used as target");
1134 # regardless of what volume was used as the destination,
1135 # update any copies that have moved over to the new lib
1136 my $copies = $e->search_asset_copy({call_number=>$vol->id, deleted => 'f'});
1138 # update circ lib on the copies - make this a method flag?
1139 for my $copy (@$copies) {
1140 next if $copy->circ_lib == $o_lib;
1141 $logger->info("merge: transfer moving circ lib on copy ".$copy->id);
1142 $copy->circ_lib($o_lib);
1143 $copy->editor($e->requestor->id);
1144 $copy->edit_date('now');
1145 $e->update_asset_copy($copy) or return $e->event;
1148 # Now see if any empty records need to be deleted after all of this
1151 $logger->debug("merge: seeing if we should delete record $_...");
1152 $evt = delete_rec($e, $_) if title_is_empty($e, $_);
1153 return $evt if $evt;
1157 # $evt = remove_empty_objects($e, $override, $_);
1161 $logger->info("merge: transfer succeeded");
1168 sub org_cannot_have_vols {
1172 my $org = $e->retrieve_actor_org_unit($org_id)
1173 or return $e->event;
1175 my $ou_type = $e->retrieve_actor_org_unit_type($org->ou_type)
1176 or return $e->event;
1178 return OpenILS::Event->new('ORG_CANNOT_HAVE_VOLS')
1179 unless $U->is_true($ou_type->can_have_vols);
1187 __PACKAGE__->register_method(
1188 api_name => 'open-ils.cat.call_number.find_or_create',
1189 method => 'find_or_create_volume',
1192 sub find_or_create_volume {
1193 my( $self, $conn, $auth, $label, $record_id, $org_id ) = @_;
1194 my $e = new_editor(authtoken=>$auth, xact=>1);
1195 return $e->die_event unless $e->checkauth;
1199 if($record_id == OILS_PRECAT_RECORD) {
1201 $vol = $e->retrieve_asset_call_number(OILS_PRECAT_CALL_NUMBER)
1202 or return $e->die_event;
1206 $vol = $e->search_asset_call_number(
1207 {label => $label, record => $record_id, owning_lib => $org_id, deleted => 'f'},
1212 # If the volume exists, return the ID
1213 if( $vol ) { $e->rollback; return $vol; }
1215 # -----------------------------------------------------------------
1216 # Otherwise, create a new volume with the given attributes
1217 # -----------------------------------------------------------------
1219 return $e->die_event unless $e->allowed('UPDATE_VOLUME', $org_id);
1221 $vol = Fieldmapper::asset::call_number->new;
1222 $vol->owning_lib($org_id);
1223 $vol->label($label);
1224 $vol->record($record_id);
1226 my $evt = create_volume( 0, $e, $vol );
1227 return $evt if $evt;