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;
86 __PACKAGE__->register_method(
87 method => 'fetch_bib_sources',
88 api_name => 'open-ils.cat.bib_sources.retrieve.all');
90 sub fetch_bib_sources {
91 return OpenILS::Application::Cat::BibCommon->fetch_bib_sources();
94 __PACKAGE__->register_method(
95 method => "create_record_xml",
96 api_name => "open-ils.cat.biblio.record.xml.create.override",
97 signature => q/@see open-ils.cat.biblio.record.xml.create/);
99 __PACKAGE__->register_method(
100 method => "create_record_xml",
101 api_name => "open-ils.cat.biblio.record.xml.create",
103 Inserts a new biblio with the given XML
107 sub create_record_xml {
108 my( $self, $client, $login, $xml, $source ) = @_;
110 my $override = 1 if $self->api_name =~ /override/;
112 my( $user_obj, $evt ) = $U->checksesperm($login, 'CREATE_MARC');
115 $logger->activity("user ".$user_obj->id." creating new MARC record");
117 my $meth = $self->method_lookup("open-ils.cat.biblio.record.xml.import");
119 $meth = $self->method_lookup(
120 "open-ils.cat.biblio.record.xml.import.override") if $override;
122 my ($s) = $meth->run($login, $xml, $source);
128 __PACKAGE__->register_method(
129 method => "biblio_record_replace_marc",
130 api_name => "open-ils.cat.biblio.record.xml.update",
133 Updates the XML for a given biblio record.
134 This does not change any other aspect of the record entry
135 exception the XML, the editor, and the edit date.
136 @return The update record object
140 __PACKAGE__->register_method(
141 method => 'biblio_record_replace_marc',
142 api_name => 'open-ils.cat.biblio.record.marc.replace',
144 @param auth The authtoken
145 @param recid The record whose MARC we're replacing
146 @param newxml The new xml to use
150 __PACKAGE__->register_method(
151 method => 'biblio_record_replace_marc',
152 api_name => 'open-ils.cat.biblio.record.marc.replace.override',
153 signature => q/@see open-ils.cat.biblio.record.marc.replace/
156 sub biblio_record_replace_marc {
157 my( $self, $conn, $auth, $recid, $newxml, $source ) = @_;
158 my $e = new_editor(authtoken=>$auth, xact=>1);
159 return $e->die_event unless $e->checkauth;
160 return $e->die_event unless $e->allowed('CREATE_MARC', $e->requestor->ws_ou);
162 my $res = OpenILS::Application::Cat::BibCommon->biblio_record_replace_marc(
163 $e, $recid, $newxml, $source,
164 $self->api_name =~ /replace/o,
165 $self->api_name =~ /override/o);
167 $e->commit unless $U->event_code($res);
171 __PACKAGE__->register_method(
172 method => "update_biblio_record_entry",
173 api_name => "open-ils.cat.biblio.record_entry.update",
175 Updates a biblio.record_entry
176 @param auth The authtoken
177 @param record The record with updated values
178 @return 1 on success, Event on error.
182 sub update_biblio_record_entry {
183 my($self, $conn, $auth, $record) = @_;
184 my $e = new_editor(authtoken=>$auth, xact=>1);
185 return $e->die_event unless $e->checkauth;
186 return $e->die_event unless $e->allowed('UPDATE_RECORD');
187 $e->update_biblio_record_entry($record) or return $e->die_event;
192 __PACKAGE__->register_method(
193 method => "undelete_biblio_record_entry",
194 api_name => "open-ils.cat.biblio.record_entry.undelete",
196 Un-deletes a record and sets active=true
197 @param auth The authtoken
198 @param record The record_id to ressurect
199 @return 1 on success, Event on error.
202 sub undelete_biblio_record_entry {
203 my($self, $conn, $auth, $record_id) = @_;
204 my $e = new_editor(authtoken=>$auth, xact=>1);
205 return $e->die_event unless $e->checkauth;
206 return $e->die_event unless $e->allowed('UPDATE_RECORD');
208 my $record = $e->retrieve_biblio_record_entry($record_id)
209 or return $e->die_event;
210 $record->deleted('f');
211 $record->active('t');
213 # no 2 non-deleted records can have the same tcn_value
214 my $existing = $e->search_biblio_record_entry(
216 tcn_value => $record->tcn_value,
217 id => {'!=' => $record_id}
219 return OpenILS::Event->new('TCN_EXISTS') if @$existing;
221 $e->update_biblio_record_entry($record) or return $e->die_event;
227 __PACKAGE__->register_method(
228 method => "biblio_record_xml_import",
229 api_name => "open-ils.cat.biblio.record.xml.import.override",
230 signature => q/@see open-ils.cat.biblio.record.xml.import/);
232 __PACKAGE__->register_method(
233 method => "biblio_record_xml_import",
234 api_name => "open-ils.cat.biblio.record.xml.import",
235 notes => <<" NOTES");
236 Takes a marcxml record and imports the record into the database. In this
237 case, the marcxml record is assumed to be a complete record (i.e. valid
238 MARC). The title control number is taken from (whichever comes first)
239 tags 001, 039[ab], 020a, 022a, 010, 035a and whichever does not already exist
241 user_session must have IMPORT_MARC permissions
245 sub biblio_record_xml_import {
246 my( $self, $client, $authtoken, $xml, $source, $auto_tcn) = @_;
247 my $e = new_editor(xact=>1, authtoken=>$authtoken);
248 return $e->die_event unless $e->checkauth;
249 return $e->die_event unless $e->allowed('IMPORT_MARC', $e->requestor->ws_ou);
251 my $res = OpenILS::Application::Cat::BibCommon->biblio_record_xml_import(
252 $e, $xml, $source, $auto_tcn, $self->api_name =~ /override/);
254 $e->commit unless $U->event_code($res);
258 __PACKAGE__->register_method(
259 method => "biblio_record_record_metadata",
260 api_name => "open-ils.cat.biblio.record.metadata.retrieve",
262 argc => 1, #(session_id, biblio_tree )
263 notes => "Walks the tree and commits any changed nodes " .
264 "adds any new nodes, and deletes any deleted nodes",
267 sub biblio_record_record_metadata {
268 my( $self, $client, $authtoken, $ids ) = @_;
270 return [] unless $ids and @$ids;
272 my $editor = new_editor(authtoken => $authtoken);
273 return $editor->event unless $editor->checkauth;
274 return $editor->event unless $editor->allowed('VIEW_USER');
279 return $editor->event unless
280 my $rec = $editor->retrieve_biblio_record_entry($_);
281 $rec->creator($editor->retrieve_actor_user($rec->creator));
282 $rec->editor($editor->retrieve_actor_user($rec->editor));
283 $rec->clear_marc; # slim the record down
284 push( @results, $rec );
292 __PACKAGE__->register_method(
293 method => "biblio_record_marc_cn",
294 api_name => "open-ils.cat.biblio.record.marc_cn.retrieve",
295 argc => 1, #(bib id )
298 sub biblio_record_marc_cn {
299 my( $self, $client, $id ) = @_;
301 my $session = OpenSRF::AppSession->create("open-ils.cstore");
303 ->request("open-ils.cstore.direct.biblio.record_entry.retrieve", $id )
307 my $doc = XML::LibXML->new->parse_string($marc);
308 $doc->documentElement->setNamespace( "http://www.loc.gov/MARC21/slim", "marc", 1 );
311 for my $tag ( qw/050 055 060 070 080 082 086 088 090 092 096 098 099/ ) {
312 my @node = $doc->findnodes("//marc:datafield[\@tag='$tag']");
314 my $cn = $x->findvalue("marc:subfield[\@code='a' or \@code='b']");
315 push @res, {$tag => $cn} if ($cn);
323 __PACKAGE__->register_method(
324 method => "orgs_for_title",
326 api_name => "open-ils.cat.actor.org_unit.retrieve_by_title"
330 my( $self, $client, $record_id ) = @_;
332 my $vols = $U->simple_scalar_request(
334 "open-ils.cstore.direct.asset.call_number.search.atomic",
335 { record => $record_id, deleted => 'f' });
337 my $orgs = { map {$_->owning_lib => 1 } @$vols };
338 return [ keys %$orgs ];
342 __PACKAGE__->register_method(
343 method => "retrieve_copies",
345 api_name => "open-ils.cat.asset.copy_tree.retrieve");
347 __PACKAGE__->register_method(
348 method => "retrieve_copies",
349 api_name => "open-ils.cat.asset.copy_tree.global.retrieve");
351 # user_session may be null/undef
352 sub retrieve_copies {
354 my( $self, $client, $user_session, $docid, @org_ids ) = @_;
356 if(ref($org_ids[0])) { @org_ids = @{$org_ids[0]}; }
360 # grabbing copy trees should be available for everyone..
361 if(!@org_ids and $user_session) {
363 OpenILS::Application::AppUtils->check_user_session( $user_session ); #throws EX on error
364 @org_ids = ($user_obj->home_ou);
367 if( $self->api_name =~ /global/ ) {
368 return _build_volume_list( { record => $docid, deleted => 'f' } );
373 for my $orgid (@org_ids) {
374 my $vols = _build_volume_list(
375 { record => $docid, owning_lib => $orgid, deleted => 'f' } );
376 push( @all_vols, @$vols );
386 sub _build_volume_list {
387 my $search_hash = shift;
389 $search_hash->{deleted} = 'f';
390 my $e = new_editor();
392 my $vols = $e->search_asset_call_number($search_hash);
396 for my $volume (@$vols) {
398 my $copies = $e->search_asset_copy(
399 { call_number => $volume->id , deleted => 'f' });
401 $copies = [ sort { $a->barcode cmp $b->barcode } @$copies ];
403 for my $c (@$copies) {
404 if( $c->status == OILS_COPY_STATUS_CHECKED_OUT ) {
406 $e->search_action_circulation(
408 { target_copy => $c->id },
410 order_by => { circ => 'xact_start desc' },
419 $volume->copies($copies);
420 push( @volumes, $volume );
423 #$session->disconnect();
429 __PACKAGE__->register_method(
430 method => "fleshed_copy_update",
431 api_name => "open-ils.cat.asset.copy.fleshed.batch.update",);
433 __PACKAGE__->register_method(
434 method => "fleshed_copy_update",
435 api_name => "open-ils.cat.asset.copy.fleshed.batch.update.override",);
438 sub fleshed_copy_update {
439 my( $self, $conn, $auth, $copies, $delete_stats ) = @_;
440 return 1 unless ref $copies;
441 my( $reqr, $evt ) = $U->checkses($auth);
443 my $editor = new_editor(requestor => $reqr, xact => 1);
444 my $override = $self->api_name =~ /override/;
445 $evt = update_fleshed_copies($editor, $override, undef, $copies, $delete_stats);
447 $logger->info("fleshed copy update failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
452 $logger->info("fleshed copy update successfully updated ".scalar(@$copies)." copies");
457 __PACKAGE__->register_method(
459 api_name => 'open-ils.cat.biblio.records.merge',
461 Merges a group of records
462 @param auth The login session key
463 @param master The id of the record all other records should be merged into
464 @param records Array of records to be merged into the master record
465 @return 1 on success, Event on error.
470 my( $self, $conn, $auth, $master, $records ) = @_;
471 my( $reqr, $evt ) = $U->checkses($auth);
473 my $editor = new_editor( requestor => $reqr, xact => 1 );
474 my $v = OpenILS::Application::Cat::Merge::merge_records($editor, $master, $records);
477 # tell the client the merge is complete, then merge the holds
478 $conn->respond_complete(1);
479 merge_holds($master, $records);
484 my($master, $records) = @_;
485 return unless $master and @$records;
486 return if @$records == 1 and $master == $$records[0];
488 my $e = new_editor(xact=>1);
489 my $holds = $e->search_action_hold_request(
490 { cancel_time => undef,
491 fulfillment_time => undef,
498 for my $hold_id (@$holds) {
500 my $hold = $e->retrieve_action_hold_request($hold_id);
502 $logger->info("Changing hold ".$hold->id.
503 " target from ".$hold->target." to $master in record merge");
505 $hold->target($master);
506 unless($e->update_action_hold_request($hold)) {
508 $logger->error("Error updating hold ". $evt->textcode .":". $evt->desc .":". $evt->stacktrace);
519 # ---------------------------------------------------------------------------
520 # returns true if the given title (id) has no un-deleted volumes or
521 # copies attached. If a context volume is defined, a record
522 # is considered empty only if the context volume is the only
523 # remaining volume on the record.
524 # ---------------------------------------------------------------------------
526 my( $editor, $rid, $vol_id ) = @_;
528 return 0 if $rid == OILS_PRECAT_RECORD;
530 my $cnlist = $editor->search_asset_call_number(
531 { record => $rid, deleted => 'f' }, { idlist => 1 } );
533 return 1 unless @$cnlist; # no attached volumes
534 return 0 if @$cnlist > 1; # multiple attached volumes
535 return 0 unless $$cnlist[0] == $vol_id; # attached volume is not the context vol.
537 # see if the sole remaining context volume has any attached copies
538 for my $cn (@$cnlist) {
539 my $copylist = $editor->search_asset_copy(
541 { call_number => $cn, deleted => 'f' },
544 return 0 if @$copylist; # false if we find any copies
551 __PACKAGE__->register_method(
552 method => "fleshed_volume_update",
553 api_name => "open-ils.cat.asset.volume.fleshed.batch.update",);
555 __PACKAGE__->register_method(
556 method => "fleshed_volume_update",
557 api_name => "open-ils.cat.asset.volume.fleshed.batch.update.override",);
559 sub fleshed_volume_update {
560 my( $self, $conn, $auth, $volumes, $delete_stats ) = @_;
561 my( $reqr, $evt ) = $U->checkses($auth);
564 my $override = ($self->api_name =~ /override/);
565 my $editor = new_editor( requestor => $reqr, xact => 1 );
567 for my $vol (@$volumes) {
568 $logger->info("vol-update: investigating volume ".$vol->id);
570 $vol->editor($reqr->id);
571 $vol->edit_date('now');
573 my $copies = $vol->copies;
576 $vol->editor($editor->requestor->id);
577 $vol->edit_date('now');
579 if( $vol->isdeleted ) {
581 $logger->info("vol-update: deleting volume");
582 my $cs = $editor->search_asset_copy(
583 { call_number => $vol->id, deleted => 'f' } );
584 return OpenILS::Event->new(
585 'VOLUME_NOT_EMPTY', payload => $vol->id ) if @$cs;
588 return $editor->event unless
589 $editor->update_asset_call_number($vol);
592 } elsif( $vol->isnew ) {
593 $logger->info("vol-update: creating volume");
594 $evt = create_volume( $override, $editor, $vol );
597 } elsif( $vol->ischanged ) {
598 $logger->info("vol-update: update volume");
599 $evt = update_volume($vol, $editor);
603 # now update any attached copies
604 if( $copies and @$copies and !$vol->isdeleted ) {
605 $_->call_number($vol->id) for @$copies;
606 $evt = update_fleshed_copies( $editor, $override, $vol, $copies, $delete_stats );
612 return scalar(@$volumes);
621 return $evt if ( $evt = org_cannot_have_vols($editor, $vol->owning_lib) );
623 my $vols = $editor->search_asset_call_number( {
624 owning_lib => $vol->owning_lib,
625 record => $vol->record,
626 label => $vol->label,
631 # There exists a different volume in the DB with the same properties
632 return OpenILS::Event->new('VOLUME_LABEL_EXISTS', payload => $vol->id)
633 if grep { $_->id ne $vol->id } @$vols;
635 return $editor->event unless $editor->update_asset_call_number($vol);
642 my( $vol, $copy ) = @_;
643 my $org = $vol->owning_lib;
644 if( $vol->id == OILS_PRECAT_CALL_NUMBER ) {
645 $org = ref($copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
647 $logger->debug("using copy perm org $org");
652 # this does the actual work
653 sub update_fleshed_copies {
654 my( $editor, $override, $vol, $copies, $delete_stats ) = @_;
657 my $fetchvol = ($vol) ? 0 : 1;
660 $cache{$vol->id} = $vol if $vol;
662 for my $copy (@$copies) {
664 my $copyid = $copy->id;
665 $logger->info("vol-update: inspecting copy $copyid");
667 if( !($vol = $cache{$copy->call_number}) ) {
668 $vol = $cache{$copy->call_number} =
669 $editor->retrieve_asset_call_number($copy->call_number);
670 return $editor->event unless $vol;
673 return $editor->event unless
674 $editor->allowed('UPDATE_COPY', copy_perm_org($vol, $copy));
676 $copy->editor($editor->requestor->id);
677 $copy->edit_date('now');
679 $copy->status( $copy->status->id ) if ref($copy->status);
680 $copy->location( $copy->location->id ) if ref($copy->location);
681 $copy->circ_lib( $copy->circ_lib->id ) if ref($copy->circ_lib);
683 my $sc_entries = $copy->stat_cat_entries;
684 $copy->clear_stat_cat_entries;
686 if( $copy->isdeleted ) {
687 $evt = delete_copy($editor, $override, $vol, $copy);
690 } elsif( $copy->isnew ) {
691 $evt = create_copy( $editor, $vol, $copy );
694 } elsif( $copy->ischanged ) {
696 $evt = update_copy( $editor, $override, $vol, $copy );
700 $copy->stat_cat_entries( $sc_entries );
701 $evt = update_copy_stat_entries($editor, $copy, $delete_stats);
705 $logger->debug("vol-update: done updating copy batch");
713 if(defined $copy->price) {
714 my $p = $copy->price || 0;
719 my $d = $copy->deposit_amount || 0;
721 $copy->deposit_amount($d);
726 my( $editor, $override, $vol, $copy ) = @_;
729 my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
730 return $evt if ( $evt = org_cannot_have_vols($editor, $org) );
732 $logger->info("vol-update: updating copy ".$copy->id);
733 my $orig_copy = $editor->retrieve_asset_copy($copy->id);
734 my $orig_vol = $editor->retrieve_asset_call_number($copy->call_number);
736 $copy->editor($editor->requestor->id);
737 $copy->edit_date('now');
739 $copy->age_protect( $copy->age_protect->id )
740 if ref $copy->age_protect;
742 fix_copy_price($copy);
744 return $editor->event unless $editor->update_asset_copy($copy);
745 return remove_empty_objects($editor, $override, $orig_vol);
749 sub remove_empty_objects {
750 my( $editor, $override, $vol ) = @_;
752 my $koe = $U->ou_ancestor_setting_value(
753 $editor->requestor->ws_ou, 'cat.bib.keep_on_empty', $editor);
754 my $aoe = $U->ou_ancestor_setting_value(
755 $editor->requestor->ws_ou, 'cat.bib.alert_on_empty', $editor);
757 if( title_is_empty($editor, $vol->record, $vol->id) ) {
759 # delete this volume if it's not already marked as deleted
760 unless( $U->is_true($vol->deleted) || $vol->isdeleted ) {
762 $vol->editor($editor->requestor->id);
763 $vol->edit_date('now');
764 $editor->update_asset_call_number($vol) or return $editor->event;
768 # delete the bib record if the keep-on-empty setting is not set
769 my $evt = delete_rec($editor, $vol->record);
773 # return the empty alert if the alert-on-empty setting is set
774 return OpenILS::Event->new('TITLE_LAST_COPY', payload => $vol->record ) if $aoe;
781 __PACKAGE__->register_method (
782 method => 'delete_bib_record',
783 api_name => 'open-ils.cat.biblio.record_entry.delete');
785 sub delete_bib_record {
786 my($self, $conn, $auth, $rec_id) = @_;
787 my $e = new_editor(xact=>1, authtoken=>$auth);
788 return $e->die_event unless $e->checkauth;
789 return $e->die_event unless $e->allowed('DELETE_RECORD', $e->requestor->ws_ou);
790 my $vols = $e->search_asset_call_number({record=>$rec_id, deleted=>'f'});
791 return OpenILS::Event->new('RECORD_NOT_EMPTY', payload=>$rec_id) if @$vols;
792 my $evt = delete_rec($e, $rec_id);
793 if($evt) { $e->rollback; return $evt; }
799 # marks a record as deleted
801 my( $editor, $rec_id ) = @_;
803 my $rec = $editor->retrieve_biblio_record_entry($rec_id)
804 or return $editor->event;
806 return undef if $U->is_true($rec->deleted);
810 $rec->editor( $editor->requestor->id );
811 $rec->edit_date('now');
812 $editor->update_biblio_record_entry($rec) or return $editor->event;
819 my( $editor, $override, $vol, $copy ) = @_;
821 return $editor->event unless
822 $editor->allowed('DELETE_COPY',copy_perm_org($vol, $copy));
824 my $stat = $U->copy_status($copy->status)->id;
827 return OpenILS::Event->new('COPY_DELETE_WARNING', payload => $copy->id )
828 if $stat == OILS_COPY_STATUS_CHECKED_OUT or
829 $stat == OILS_COPY_STATUS_IN_TRANSIT or
830 $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF or
831 $stat == OILS_COPY_STATUS_ILL;
834 $logger->info("vol-update: deleting copy ".$copy->id);
837 $copy->editor($editor->requestor->id);
838 $copy->edit_date('now');
839 $editor->update_asset_copy($copy) or return $editor->event;
841 # Delete any open transits for this copy
842 my $transits = $editor->search_action_transit_copy(
843 { target_copy=>$copy->id, dest_recv_time => undef } );
845 for my $t (@$transits) {
846 $editor->delete_action_transit_copy($t)
847 or return $editor->event;
850 return remove_empty_objects($editor, $override, $vol);
855 my( $editor, $vol, $copy ) = @_;
857 my $existing = $editor->search_asset_copy(
858 { barcode => $copy->barcode, deleted => 'f' } );
860 return OpenILS::Event->new('ITEM_BARCODE_EXISTS') if @$existing;
862 # see if the volume this copy references is marked as deleted
863 my $evol = $editor->retrieve_asset_call_number($copy->call_number)
864 or return $editor->event;
865 return OpenILS::Event->new('VOLUME_DELETED', vol => $evol->id)
866 if $U->is_true($evol->deleted);
869 my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
870 return $evt if ( $evt = org_cannot_have_vols($editor, $org) );
873 $copy->creator($editor->requestor->id);
874 $copy->create_date('now');
875 fix_copy_price($copy);
877 $editor->create_asset_copy($copy) or return $editor->event;
881 # if 'delete_stats' is true, the copy->stat_cat_entries data is
882 # treated as the authoritative list for the copy. existing entries
883 # that are not in said list will be deleted from the DB
884 sub update_copy_stat_entries {
885 my( $editor, $copy, $delete_stats ) = @_;
887 return undef if $copy->isdeleted;
888 return undef unless $copy->ischanged or $copy->isnew;
891 my $entries = $copy->stat_cat_entries;
893 if( $delete_stats ) {
894 $entries = ($entries and @$entries) ? $entries : [];
896 return undef unless ($entries and @$entries);
899 my $maps = $editor->search_asset_stat_cat_entry_copy_map({owning_copy=>$copy->id});
902 # if there is no stat cat entry on the copy who's id matches the
903 # current map's id, remove the map from the database
904 for my $map (@$maps) {
905 if(! grep { $_->id == $map->stat_cat_entry } @$entries ) {
907 $logger->info("copy update found stale ".
908 "stat cat entry map ".$map->id. " on copy ".$copy->id);
910 $editor->delete_asset_stat_cat_entry_copy_map($map)
911 or return $editor->event;
916 # go through the stat cat update/create process
917 for my $entry (@$entries) {
920 # if this link already exists in the DB, don't attempt to re-create it
921 next if( grep{$_->stat_cat_entry == $entry->id} @$maps );
923 my $new_map = Fieldmapper::asset::stat_cat_entry_copy_map->new();
925 my $sc = ref($entry->stat_cat) ? $entry->stat_cat->id : $entry->stat_cat;
927 $new_map->stat_cat( $sc );
928 $new_map->stat_cat_entry( $entry->id );
929 $new_map->owning_copy( $copy->id );
931 $editor->create_asset_stat_cat_entry_copy_map($new_map)
932 or return $editor->event;
934 $logger->info("copy update created new stat cat entry map ".$editor->data);
942 my( $override, $editor, $vol ) = @_;
945 return $evt if ( $evt = org_cannot_have_vols($editor, $vol->owning_lib) );
947 # see if the record this volume references is marked as deleted
948 my $rec = $editor->retrieve_biblio_record_entry($vol->record)
949 or return $editor->event;
950 return OpenILS::Event->new('BIB_RECORD_DELETED', rec => $rec->id)
951 if $U->is_true($rec->deleted);
953 # first lets see if there are any collisions
954 my $vols = $editor->search_asset_call_number( {
955 owning_lib => $vol->owning_lib,
956 record => $vol->record,
957 label => $vol->label,
964 # we've found an exising volume
966 $label = $vol->label;
968 return OpenILS::Event->new(
969 'VOLUME_LABEL_EXISTS', payload => $vol->id);
973 # create a temp label so we can create the new volume,
974 # then de-dup it with the existing volume
975 $vol->label( "__SYSTEM_TMP_$$".time) if $label;
977 $vol->creator($editor->requestor->id);
978 $vol->create_date('now');
979 $vol->editor($editor->requestor->id);
980 $vol->edit_date('now');
983 $editor->create_asset_call_number($vol) or return $editor->event;
986 # now restore the label and merge into the existing record
989 OpenILS::Application::Cat::Merge::merge_volumes($editor, [$vol], $$vols[0]);
997 __PACKAGE__->register_method (
998 method => 'batch_volume_transfer',
999 api_name => 'open-ils.cat.asset.volume.batch.transfer',
1002 __PACKAGE__->register_method (
1003 method => 'batch_volume_transfer',
1004 api_name => 'open-ils.cat.asset.volume.batch.transfer.override',
1008 sub batch_volume_transfer {
1009 my( $self, $conn, $auth, $args ) = @_;
1012 my $rec = $$args{docid};
1013 my $o_lib = $$args{lib};
1014 my $vol_ids = $$args{volumes};
1016 my $override = 1 if $self->api_name =~ /override/;
1018 $logger->info("merge: transferring volumes to lib=$o_lib and record=$rec");
1020 my $e = new_editor(authtoken => $auth, xact =>1);
1021 return $e->event unless $e->checkauth;
1022 return $e->event unless $e->allowed('UPDATE_VOLUME', $o_lib);
1024 my $dorg = $e->retrieve_actor_org_unit($o_lib)
1025 or return $e->event;
1027 my $ou_type = $e->retrieve_actor_org_unit_type($dorg->ou_type)
1028 or return $e->event;
1030 return $evt if ( $evt = org_cannot_have_vols($e, $o_lib) );
1032 my $vols = $e->batch_retrieve_asset_call_number($vol_ids);
1037 for my $vol (@$vols) {
1039 # if we've already looked at this volume, go to the next
1040 next if !$vol or grep { $vol->id == $_ } @seen;
1042 # grab all of the volumes in the list that have
1043 # the same label so they can be merged
1044 my @all = grep { $_->label eq $vol->label } @$vols;
1046 # take note of the fact that we've looked at this set of volumes
1047 push( @seen, $_->id ) for @all;
1048 push( @rec_ids, $_->record ) for @all;
1050 # for each volume, see if there are any copies that have a
1051 # remote circ_lib (circ_lib != vol->owning_lib and != $o_lib ).
1053 unless( $override ) {
1056 $logger->debug("merge: searching for copies with remote circ_lib for volume ".$v->id);
1058 call_number => $v->id,
1059 circ_lib => { "not in" => [ $o_lib, $v->owning_lib ] },
1063 my $copies = $e->search_asset_copy($args, {idlist=>1});
1065 # if the copy's circ_lib matches the destination lib,
1067 return OpenILS::Event->new('COPY_REMOTE_CIRC_LIB') if @$copies;
1071 # see if there is a volume at the destination lib that
1072 # already has the requested label
1073 my $existing_vol = $e->search_asset_call_number(
1075 label => $vol->label,
1077 owning_lib =>$o_lib,
1082 if( $existing_vol ) {
1084 if( grep { $_->id == $existing_vol->id } @all ) {
1085 # this volume is already accounted for in our list of volumes to merge
1086 $existing_vol = undef;
1089 # this volume exists on the destination record/owning_lib and must
1090 # be used as the destination for merging
1091 $logger->debug("merge: volume already exists at destination record: ".
1092 $existing_vol->id.' : '.$existing_vol->label) if $existing_vol;
1096 if( @all > 1 || $existing_vol ) {
1097 $logger->info("merge: found collisions in volume transfer");
1098 my @args = ($e, \@all);
1099 @args = ($e, \@all, $existing_vol) if $existing_vol;
1100 ($vol, $evt) = OpenILS::Application::Cat::Merge::merge_volumes(@args);
1101 return $evt if $evt;
1104 if( !$existing_vol ) {
1106 $vol->owning_lib($o_lib);
1108 $vol->editor($e->requestor->id);
1109 $vol->edit_date('now');
1111 $logger->info("merge: updating volume ".$vol->id);
1112 $e->update_asset_call_number($vol) or return $e->event;
1115 $logger->info("merge: bypassing volume update because existing volume used as target");
1118 # regardless of what volume was used as the destination,
1119 # update any copies that have moved over to the new lib
1120 my $copies = $e->search_asset_copy({call_number=>$vol->id, deleted => 'f'});
1122 # update circ lib on the copies - make this a method flag?
1123 for my $copy (@$copies) {
1124 next if $copy->circ_lib == $o_lib;
1125 $logger->info("merge: transfer moving circ lib on copy ".$copy->id);
1126 $copy->circ_lib($o_lib);
1127 $copy->editor($e->requestor->id);
1128 $copy->edit_date('now');
1129 $e->update_asset_copy($copy) or return $e->event;
1132 # Now see if any empty records need to be deleted after all of this
1135 $logger->debug("merge: seeing if we should delete record $_...");
1136 $evt = delete_rec($e, $_) if title_is_empty($e, $_);
1137 return $evt if $evt;
1141 # $evt = remove_empty_objects($e, $override, $_);
1145 $logger->info("merge: transfer succeeded");
1152 sub org_cannot_have_vols {
1156 my $org = $e->retrieve_actor_org_unit($org_id)
1157 or return $e->event;
1159 my $ou_type = $e->retrieve_actor_org_unit_type($org->ou_type)
1160 or return $e->event;
1162 return OpenILS::Event->new('ORG_CANNOT_HAVE_VOLS')
1163 unless $U->is_true($ou_type->can_have_vols);
1171 __PACKAGE__->register_method(
1172 api_name => 'open-ils.cat.call_number.find_or_create',
1173 method => 'find_or_create_volume',
1176 sub find_or_create_volume {
1177 my( $self, $conn, $auth, $label, $record_id, $org_id ) = @_;
1178 my $e = new_editor(authtoken=>$auth, xact=>1);
1179 return $e->die_event unless $e->checkauth;
1183 if($record_id == OILS_PRECAT_RECORD) {
1185 $vol = $e->retrieve_asset_call_number(OILS_PRECAT_CALL_NUMBER)
1186 or return $e->die_event;
1190 $vol = $e->search_asset_call_number(
1191 {label => $label, record => $record_id, owning_lib => $org_id, deleted => 'f'},
1196 # If the volume exists, return the ID
1197 if( $vol ) { $e->rollback; return $vol; }
1199 # -----------------------------------------------------------------
1200 # Otherwise, create a new volume with the given attributes
1201 # -----------------------------------------------------------------
1203 return $e->die_event unless $e->allowed('UPDATE_VOLUME', $org_id);
1205 $vol = Fieldmapper::asset::call_number->new;
1206 $vol->owning_lib($org_id);
1207 $vol->label($label);
1208 $vol->record($record_id);
1210 my $evt = create_volume( 0, $e, $vol );
1211 return $evt if $evt;