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::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";
29 my $assetcom = 'OpenILS::Application::Cat::AssetCommon';
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 $fix_tcn = $self->api_name =~ /replace/o;
164 my $override = $self->api_name =~ /override/o;
166 my $res = OpenILS::Application::Cat::BibCommon->biblio_record_replace_marc(
167 $e, $recid, $newxml, $source, $fix_tcn, $override);
169 $e->commit unless $U->event_code($res);
171 #my $ses = OpenSRF::AppSession->create('open-ils.ingest');
172 #$ses->request('open-ils.ingest.full.biblio.record', $recid);
177 __PACKAGE__->register_method(
178 method => "template_overlay_biblio_record_entry",
179 api_name => "open-ils.cat.biblio.record_entry.template_overlay",
182 Overlays biblio.record_entry MARC values
183 @param auth The authtoken
184 @param records The record ids to be updated by the template
185 @param template The overlay template
186 @return Stream of hashes record id in the key "record" and t or f for the success of the overlay operation in key "success"
190 sub template_overlay_biblio_record_entry {
191 my($self, $conn, $auth, $records, $template) = @_;
192 my $e = new_editor(authtoken=>$auth, xact=>1);
193 return $e->die_event unless $e->checkauth;
195 $records = [$records] if (!ref($records));
197 for my $rid ( @$records ) {
198 my $rec = $e->retrieve_biblio_record_entry($rid);
201 unless ($e->allowed('UPDATE_RECORD', $rec->owner, $rec)) {
202 $conn->respond({ record => $rid, success => 'f' });
206 my $success = $e->json_query(
207 { from => [ 'vandelay.template_overlay_bib_record', $template, $rid ] }
208 )->[0]->{'vandelay.template_overlay_bib_record'};
210 $conn->respond({ record => $rid, success => $success });
217 __PACKAGE__->register_method(
218 method => "template_overlay_container",
219 api_name => "open-ils.cat.container.template_overlay",
222 Overlays biblio.record_entry MARC values
223 @param auth The authtoken
224 @param container The container, um, containing the records to be updated by the template
225 @param template The overlay template, or nothing and the method will look for a negative bib id in the container
226 @return Stream of hashes record id in the key "record" and t or f for the success of the overlay operation in key "success"
230 __PACKAGE__->register_method(
231 method => "template_overlay_container",
232 api_name => "open-ils.cat.container.template_overlay.background",
235 Overlays biblio.record_entry MARC values
236 @param auth The authtoken
237 @param container The container, um, containing the records to be updated by the template
238 @param template The overlay template, or nothing and the method will look for a negative bib id in the container
239 @return Cache key to check for status of the container overlay
243 sub template_overlay_container {
244 my($self, $conn, $auth, $container, $template) = @_;
245 my $e = new_editor(authtoken=>$auth, xact=>1);
246 return $e->die_event unless $e->checkauth;
248 my $actor = OpenSRF::AppSession->create('open-ils.actor') if ($self->api_name =~ /background$/);
250 my $items = $e->search_container_biblio_record_entry_bucket_item({ bucket => $container });
254 ($titem) = grep { $_->target_biblio_record_entry < 0 } @$items;
259 $items = [grep { $_->target_biblio_record_entry > 0 } @$items];
261 $template = $e->retrieve_biblio_record_entry( $titem->target_biblio_record_entry )->marc;
267 $self->respond_complete(
268 $actor->request('open-ils.actor.anon_cache.set_value', $auth, res_list => $responses)->gather(1)
271 for my $item ( @$items ) {
272 my $rec = $e->retrieve_biblio_record_entry($item->target_biblio_record_entry);
276 if ($e->allowed('UPDATE_RECORD', $rec->owner, $rec)) {
277 $success = $e->json_query(
278 { from => [ 'vandelay.template_overlay_bib_record', $template, $rec->id ] }
279 )->[0]->{'vandelay.template_overlay_bib_record'};
282 $some_failed++ if ($success eq 'f');
285 push @$responses, { record => $rec->id, success => $success };
286 $actor->request('open-ils.actor.anon_cache.set_value', $auth, res_list => $responses);
288 $conn->respond({ record => $rec->id, success => $success });
291 if ($success eq 't') {
292 unless ($e->delete_container_biblio_record_entry_bucket_item($item)) {
295 push @$responses, { complete => 1, success => 'f' };
296 $actor->request('open-ils.actor.anon_cache.set_value', $auth, res_list => $responses);
299 return { complete => 1, success => 'f' };
305 if ($titem && !$some_failed) {
306 return $e->die_event unless ($e->delete_container_biblio_record_entry_bucket_item($titem));
311 push @$responses, { complete => 1, success => 't' };
312 $actor->request('open-ils.actor.anon_cache.set_value', $auth, res_list => $responses);
314 return { complete => 1, success => 't' };
318 push @$responses, { complete => 1, success => 'f' };
319 $actor->request('open-ils.actor.anon_cache.set_value', $auth, res_list => $responses);
321 return { complete => 1, success => 'f' };
327 __PACKAGE__->register_method(
328 method => "update_biblio_record_entry",
329 api_name => "open-ils.cat.biblio.record_entry.update",
331 Updates a biblio.record_entry
332 @param auth The authtoken
333 @param record The record with updated values
334 @return 1 on success, Event on error.
338 sub update_biblio_record_entry {
339 my($self, $conn, $auth, $record) = @_;
340 my $e = new_editor(authtoken=>$auth, xact=>1);
341 return $e->die_event unless $e->checkauth;
342 return $e->die_event unless $e->allowed('UPDATE_RECORD');
343 $e->update_biblio_record_entry($record) or return $e->die_event;
348 __PACKAGE__->register_method(
349 method => "undelete_biblio_record_entry",
350 api_name => "open-ils.cat.biblio.record_entry.undelete",
352 Un-deletes a record and sets active=true
353 @param auth The authtoken
354 @param record The record_id to ressurect
355 @return 1 on success, Event on error.
358 sub undelete_biblio_record_entry {
359 my($self, $conn, $auth, $record_id) = @_;
360 my $e = new_editor(authtoken=>$auth, xact=>1);
361 return $e->die_event unless $e->checkauth;
362 return $e->die_event unless $e->allowed('UPDATE_RECORD');
364 my $record = $e->retrieve_biblio_record_entry($record_id)
365 or return $e->die_event;
366 $record->deleted('f');
367 $record->active('t');
369 # Set the leader/05 to indicate that the record has been corrected/revised
370 my $marc = $record->marc();
371 $marc =~ s{(<leader>.{5}).}{$1c};
372 $record->marc($marc);
374 # no 2 non-deleted records can have the same tcn_value
375 my $existing = $e->search_biblio_record_entry(
377 tcn_value => $record->tcn_value,
378 id => {'!=' => $record_id}
380 return OpenILS::Event->new('TCN_EXISTS') if @$existing;
382 $e->update_biblio_record_entry($record) or return $e->die_event;
388 __PACKAGE__->register_method(
389 method => "biblio_record_xml_import",
390 api_name => "open-ils.cat.biblio.record.xml.import.override",
391 signature => q/@see open-ils.cat.biblio.record.xml.import/);
393 __PACKAGE__->register_method(
394 method => "biblio_record_xml_import",
395 api_name => "open-ils.cat.biblio.record.xml.import",
396 notes => <<" NOTES");
397 Takes a marcxml record and imports the record into the database. In this
398 case, the marcxml record is assumed to be a complete record (i.e. valid
399 MARC). The title control number is taken from (whichever comes first)
400 tags 001, 039[ab], 020a, 022a, 010, 035a and whichever does not already exist
402 user_session must have IMPORT_MARC permissions
406 sub biblio_record_xml_import {
407 my( $self, $client, $authtoken, $xml, $source, $auto_tcn) = @_;
408 my $e = new_editor(xact=>1, authtoken=>$authtoken);
409 return $e->die_event unless $e->checkauth;
410 return $e->die_event unless $e->allowed('IMPORT_MARC', $e->requestor->ws_ou);
412 my $override = $self->api_name =~ /override/;
413 my $record = OpenILS::Application::Cat::BibCommon->biblio_record_xml_import(
414 $e, $xml, $source, $auto_tcn, $override);
416 return $record if $U->event_code($record);
420 #my $ses = OpenSRF::AppSession->create('open-ils.ingest');
421 #$ses->request('open-ils.ingest.full.biblio.record', $record->id);
426 __PACKAGE__->register_method(
427 method => "biblio_record_record_metadata",
428 api_name => "open-ils.cat.biblio.record.metadata.retrieve",
430 argc => 2, #(session_id, list of bre ids )
431 notes => "Returns a list of slim-downed bre objects based on the " .
435 sub biblio_record_record_metadata {
436 my( $self, $client, $authtoken, $ids ) = @_;
438 return [] unless $ids and @$ids;
440 my $editor = new_editor(authtoken => $authtoken);
441 return $editor->event unless $editor->checkauth;
442 return $editor->event unless $editor->allowed('VIEW_USER');
447 return $editor->event unless
448 my $rec = $editor->retrieve_biblio_record_entry($_);
449 $rec->creator($editor->retrieve_actor_user($rec->creator));
450 $rec->editor($editor->retrieve_actor_user($rec->editor));
451 $rec->attrs($editor->retrieve_metabib_record_attr($rec->id));
452 $rec->clear_marc; # slim the record down
453 push( @results, $rec );
461 __PACKAGE__->register_method(
462 method => "biblio_record_marc_cn",
463 api_name => "open-ils.cat.biblio.record.marc_cn.retrieve",
464 argc => 1, #(bib id )
466 desc => 'Extracts call number candidates from a bibliographic record',
468 {desc => 'Record ID', type => 'number'},
469 {desc => '(Optional) Classification scheme ID', type => 'number'},
472 return => {desc => 'Hash of candidate call numbers identified by tag' }
475 sub biblio_record_marc_cn {
476 my( $self, $client, $id, $class ) = @_;
478 my $e = new_editor();
479 my $marc = $e->retrieve_biblio_record_entry($id)->marc;
481 my $doc = XML::LibXML->new->parse_string($marc);
482 $doc->documentElement->setNamespace( "http://www.loc.gov/MARC21/slim", "marc", 1 );
487 @fields = split(/,/, $e->retrieve_asset_call_number_class($class)->field);
489 @fields = qw/050ab 055ab 060ab 070ab 080ab 082ab 086ab 088ab 090 092 096 098 099/;
492 # Get field/subfield combos based on acnc value; for example "050ab,055ab"
494 foreach my $field (@fields) {
495 my $tag = substr($field, 0, 3);
496 $logger->debug("Tag = $tag");
497 my @node = $doc->findnodes("//marc:datafield[\@tag='$tag']");
499 # Now parse the subfields and build up the subfield XPath
500 my @subfields = split(//, substr($field, 3));
502 # If they give us no subfields to parse, default to just the 'a'
507 foreach my $sf (@subfields) {
508 $subxpath .= "\@code='$sf' or ";
510 $subxpath = substr($subxpath, 0, -4);
511 $logger->debug("subxpath = $subxpath");
513 # Find the contents of the specified subfields
514 foreach my $x (@node) {
515 my $cn = $x->findvalue("marc:subfield[$subxpath]");
516 push @res, {$tag => $cn} if ($cn);
523 __PACKAGE__->register_method(
524 method => 'autogen_barcodes',
525 api_name => "open-ils.cat.item.barcode.autogen",
527 desc => 'Returns N generated barcodes following a specified barcode.',
529 {desc => 'Authentication token', type => 'string'},
530 {desc => 'Barcode which the sequence should follow from', type => 'string'},
531 {desc => 'Number of barcodes to generate', type => 'number'},
532 {desc => 'Options hash. Currently you can pass in checkdigit : false to disable the use of checkdigits.'}
534 return => {desc => 'Array of generated barcodes'}
538 sub autogen_barcodes {
539 my( $self, $client, $auth, $barcode, $num_of_barcodes, $options ) = @_;
540 my $e = new_editor(authtoken => $auth);
541 return $e->event unless $e->checkauth;
542 return $e->event unless $e->allowed('UPDATE_COPY', $e->requestor->ws_ou);
545 my $barcode_text = '';
546 my $barcode_number = 0;
548 if ($barcode =~ /^(\D+)/) { $barcode_text = $1; }
549 if ($barcode =~ /(\d+)$/) { $barcode_number = $1; }
552 for (my $i = 1; $i <= $num_of_barcodes; $i++) {
553 my $calculated_barcode;
555 # default is to use checkdigits, so looking for an explicit false here
556 if (defined $$options{'checkdigit'} && ! $$options{'checkdigit'}) {
557 $calculated_barcode = $barcode_number + $i;
559 if ($barcode_number =~ /^\d{8}$/) {
560 $calculated_barcode = add_codabar_checkdigit($barcode_number + $i, 0);
561 } elsif ($barcode_number =~ /^\d{9}$/) {
562 $calculated_barcode = add_codabar_checkdigit($barcode_number + $i*10, 1); # strip last digit
563 } elsif ($barcode_number =~ /^\d{13}$/) {
564 $calculated_barcode = add_codabar_checkdigit($barcode_number + $i, 0);
565 } elsif ($barcode_number =~ /^\d{14}$/) {
566 $calculated_barcode = add_codabar_checkdigit($barcode_number + $i*10, 1); # strip last digit
568 $calculated_barcode = $barcode_number + $i;
571 push @res, $barcode_text . $calculated_barcode;
576 # Codabar doesn't define a checkdigit algorithm, but this one is typically used by libraries. gmcharlt++
577 sub add_codabar_checkdigit {
579 my $strip_last_digit = shift;
581 return $barcode if $barcode =~ /\D/;
582 $barcode = substr($barcode, 0, length($barcode)-1) if $strip_last_digit;
583 my @digits = split //, $barcode;
585 for (my $i = 1; $i < length($barcode); $i+=2) { # for a 13/14 digit barcode, would expect 1,3,5,7,9,11
586 $total += $digits[$i];
588 for (my $i = 0; $i < length($barcode); $i+=2) { # for a 13/14 digit barcode, would expect 0,2,4,6,8,10,12
589 $total += (2 * $digits[$i] >= 10) ? (2 * $digits[$i] - 9) : (2 * $digits[$i]);
591 my $remainder = $total % 10;
592 my $checkdigit = ($remainder == 0) ? $remainder : 10 - $remainder;
593 return $barcode . $checkdigit;
596 __PACKAGE__->register_method(
597 method => "orgs_for_title",
599 api_name => "open-ils.cat.actor.org_unit.retrieve_by_title"
603 my( $self, $client, $record_id ) = @_;
605 my $vols = $U->simple_scalar_request(
607 "open-ils.cstore.direct.asset.call_number.search.atomic",
608 { record => $record_id, deleted => 'f' });
610 my $orgs = { map {$_->owning_lib => 1 } @$vols };
611 return [ keys %$orgs ];
615 __PACKAGE__->register_method(
616 method => "retrieve_copies",
618 api_name => "open-ils.cat.asset.copy_tree.retrieve");
620 __PACKAGE__->register_method(
621 method => "retrieve_copies",
622 api_name => "open-ils.cat.asset.copy_tree.global.retrieve");
624 # user_session may be null/undef
625 sub retrieve_copies {
627 my( $self, $client, $user_session, $docid, @org_ids ) = @_;
629 if(ref($org_ids[0])) { @org_ids = @{$org_ids[0]}; }
633 # grabbing copy trees should be available for everyone..
634 if(!@org_ids and $user_session) {
635 my($user_obj, $evt) = OpenILS::Application::AppUtils->checkses($user_session);
637 @org_ids = ($user_obj->home_ou);
640 if( $self->api_name =~ /global/ ) {
641 return _build_volume_list( { record => $docid, deleted => 'f', label => { '<>' => '##URI##' } } );
646 for my $orgid (@org_ids) {
647 my $vols = _build_volume_list(
648 { record => $docid, owning_lib => $orgid, deleted => 'f', label => { '<>' => '##URI##' } } );
649 push( @all_vols, @$vols );
659 sub _build_volume_list {
660 my $search_hash = shift;
662 $search_hash->{deleted} = 'f';
663 my $e = new_editor();
665 my $vols = $e->search_asset_call_number([
669 flesh_fields => { acn => ['prefix','suffix','label_class'] },
670 'order_by' => { 'acn' => 'oils_text_as_bytea(label_sortkey), oils_text_as_bytea(label), id, owning_lib' }
676 for my $volume (@$vols) {
678 my $copies = $e->search_asset_copy([
679 { call_number => $volume->id , deleted => 'f' },
680 { flesh => 1, flesh_fields => { acp => ['stat_cat_entries','parts'] } }
683 $copies = [ sort { $a->barcode cmp $b->barcode } @$copies ];
685 for my $c (@$copies) {
686 if( $c->status == OILS_COPY_STATUS_CHECKED_OUT ) {
688 $e->search_action_circulation(
690 { target_copy => $c->id },
692 order_by => { circ => 'xact_start desc' },
701 $volume->copies($copies);
702 push( @volumes, $volume );
705 #$session->disconnect();
711 __PACKAGE__->register_method(
712 method => "fleshed_copy_update",
713 api_name => "open-ils.cat.asset.copy.fleshed.batch.update",);
715 __PACKAGE__->register_method(
716 method => "fleshed_copy_update",
717 api_name => "open-ils.cat.asset.copy.fleshed.batch.update.override",);
720 sub fleshed_copy_update {
721 my( $self, $conn, $auth, $copies, $delete_stats ) = @_;
722 return 1 unless ref $copies;
723 my( $reqr, $evt ) = $U->checkses($auth);
725 my $editor = new_editor(requestor => $reqr, xact => 1);
726 my $override = $self->api_name =~ /override/;
727 my $retarget_holds = [];
728 $evt = OpenILS::Application::Cat::AssetCommon->update_fleshed_copies(
729 $editor, $override, undef, $copies, $delete_stats, $retarget_holds, undef);
732 $logger->info("fleshed copy update failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
738 $logger->info("fleshed copy update successfully updated ".scalar(@$copies)." copies");
739 reset_hold_list($auth, $retarget_holds);
744 sub reset_hold_list {
745 my($auth, $hold_ids) = @_;
746 return unless @$hold_ids;
747 $logger->info("reseting holds after copy status change: @$hold_ids");
748 my $ses = OpenSRF::AppSession->create('open-ils.circ');
749 $ses->request('open-ils.circ.hold.reset.batch', $auth, $hold_ids);
753 __PACKAGE__->register_method(
754 method => 'in_db_merge',
755 api_name => 'open-ils.cat.biblio.records.merge',
757 Merges a group of records
758 @param auth The login session key
759 @param master The id of the record all other records should be merged into
760 @param records Array of records to be merged into the master record
761 @return 1 on success, Event on error.
766 my( $self, $conn, $auth, $master, $records ) = @_;
768 my $editor = new_editor( authtoken => $auth, xact => 1 );
769 return $editor->die_event unless $editor->checkauth;
770 return $editor->die_event unless $editor->allowed('MERGE_BIB_RECORDS'); # TODO see below about record ownership
773 for my $source ( @$records ) {
774 #XXX we actually /will/ want to check perms for master and sources after record ownership exists
776 # This stored proc (asset.merge_record_assets(target,source)) has the side effects of
777 # moving call_number, title-type (and some volume-type) hold_request and uri-mapping
778 # objects from the source record to the target record, so must be called from within
781 $count += $editor->json_query({
785 transform => 'asset.merge_record_assets',
791 where => { id => $master }
792 })->[0]->{count}; # count of objects moved, of all types
800 __PACKAGE__->register_method(
801 method => 'in_db_auth_merge',
802 api_name => 'open-ils.cat.authority.records.merge',
804 Merges a group of authority records
805 @param auth The login session key
806 @param master The id of the record all other records should be merged into
807 @param records Array of records to be merged into the master record
808 @return 1 on success, Event on error.
812 sub in_db_auth_merge {
813 my( $self, $conn, $auth, $master, $records ) = @_;
815 my $editor = new_editor( authtoken => $auth, xact => 1 );
816 return $editor->die_event unless $editor->checkauth;
817 return $editor->die_event unless $editor->allowed('MERGE_AUTH_RECORDS'); # TODO see below about record ownership
820 for my $source ( @$records ) {
821 $count += $editor->json_query({
825 transform => 'authority.merge_records',
831 where => { id => $master }
832 })->[0]->{count}; # count of objects moved, of all types
839 __PACKAGE__->register_method(
840 method => "fleshed_volume_update",
841 api_name => "open-ils.cat.asset.volume.fleshed.batch.update",);
843 __PACKAGE__->register_method(
844 method => "fleshed_volume_update",
845 api_name => "open-ils.cat.asset.volume.fleshed.batch.update.override",);
847 sub fleshed_volume_update {
848 my( $self, $conn, $auth, $volumes, $delete_stats, $options ) = @_;
849 my( $reqr, $evt ) = $U->checkses($auth);
853 my $override = ($self->api_name =~ /override/);
854 my $editor = new_editor( requestor => $reqr, xact => 1 );
855 my $retarget_holds = [];
856 my $auto_merge_vols = $options->{auto_merge_vols};
858 for my $vol (@$volumes) {
859 $logger->info("vol-update: investigating volume ".$vol->id);
861 $vol->editor($reqr->id);
862 $vol->edit_date('now');
864 my $copies = $vol->copies;
867 $vol->editor($editor->requestor->id);
868 $vol->edit_date('now');
870 if( $vol->isdeleted ) {
872 $logger->info("vol-update: deleting volume");
873 return $editor->die_event unless
874 $editor->allowed('UPDATE_VOLUME', $vol->owning_lib);
876 if(my $evt = $assetcom->delete_volume($editor, $vol, $override, $$options{force_delete_copies})) {
881 return $editor->die_event unless
882 $editor->update_asset_call_number($vol);
884 } elsif( $vol->isnew ) {
885 $logger->info("vol-update: creating volume");
886 $evt = $assetcom->create_volume( $override, $editor, $vol );
889 } elsif( $vol->ischanged ) {
890 $logger->info("vol-update: update volume");
891 my $resp = update_volume($vol, $editor, ($override or $auto_merge_vols));
892 return $resp->{evt} if $resp->{evt};
893 $vol = $resp->{merge_vol};
896 # now update any attached copies
897 if( $copies and @$copies and !$vol->isdeleted ) {
898 $_->call_number($vol->id) for @$copies;
899 $evt = $assetcom->update_fleshed_copies(
900 $editor, $override, $vol, $copies, $delete_stats, $retarget_holds, undef);
906 reset_hold_list($auth, $retarget_holds);
907 return scalar(@$volumes);
914 my $auto_merge = shift;
918 return {evt => $editor->event} unless
919 $editor->allowed('UPDATE_VOLUME', $vol->owning_lib);
922 if ( $evt = OpenILS::Application::Cat::AssetCommon->org_cannot_have_vols($editor, $vol->owning_lib) );
924 my $vols = $editor->search_asset_call_number({
925 owning_lib => $vol->owning_lib,
926 record => $vol->record,
927 label => $vol->label,
928 prefix => $vol->prefix,
929 suffix => $vol->suffix,
931 id => {'!=' => $vol->id}
938 # If the auto-merge option is on, merge our updated volume into the existing
939 # volume with the same record + owner + label.
940 ($merge_vol, $evt) = OpenILS::Application::Cat::Merge::merge_volumes($editor, [$vol], $vols->[0]);
941 return {evt => $evt, merge_vol => $merge_vol};
944 return {evt => OpenILS::Event->new('VOLUME_LABEL_EXISTS', payload => $vol->id)};
948 return {evt => $editor->die_event} unless $editor->update_asset_call_number($vol);
954 __PACKAGE__->register_method (
955 method => 'delete_bib_record',
956 api_name => 'open-ils.cat.biblio.record_entry.delete');
958 sub delete_bib_record {
959 my($self, $conn, $auth, $rec_id) = @_;
960 my $e = new_editor(xact=>1, authtoken=>$auth);
961 return $e->die_event unless $e->checkauth;
962 return $e->die_event unless $e->allowed('DELETE_RECORD', $e->requestor->ws_ou);
963 my $vols = $e->search_asset_call_number({record=>$rec_id, deleted=>'f'});
964 return OpenILS::Event->new('RECORD_NOT_EMPTY', payload=>$rec_id) if @$vols;
965 my $evt = OpenILS::Application::Cat::BibCommon->delete_rec($e, $rec_id);
966 if($evt) { $e->rollback; return $evt; }
973 __PACKAGE__->register_method (
974 method => 'batch_volume_transfer',
975 api_name => 'open-ils.cat.asset.volume.batch.transfer',
978 __PACKAGE__->register_method (
979 method => 'batch_volume_transfer',
980 api_name => 'open-ils.cat.asset.volume.batch.transfer.override',
984 sub batch_volume_transfer {
985 my( $self, $conn, $auth, $args ) = @_;
988 my $rec = $$args{docid};
989 my $o_lib = $$args{lib};
990 my $vol_ids = $$args{volumes};
992 my $override = 1 if $self->api_name =~ /override/;
994 $logger->info("merge: transferring volumes to lib=$o_lib and record=$rec");
996 my $e = new_editor(authtoken => $auth, xact =>1);
997 return $e->event unless $e->checkauth;
998 return $e->event unless $e->allowed('UPDATE_VOLUME', $o_lib);
1000 my $dorg = $e->retrieve_actor_org_unit($o_lib)
1001 or return $e->event;
1003 my $ou_type = $e->retrieve_actor_org_unit_type($dorg->ou_type)
1004 or return $e->event;
1006 return $evt if ( $evt = OpenILS::Application::Cat::AssetCommon->org_cannot_have_vols($e, $o_lib) );
1008 my $vols = $e->batch_retrieve_asset_call_number($vol_ids);
1013 for my $vol (@$vols) {
1015 # if we've already looked at this volume, go to the next
1016 next if !$vol or grep { $vol->id == $_ } @seen;
1018 # grab all of the volumes in the list that have
1019 # the same label so they can be merged
1020 my @all = grep { $_->label eq $vol->label } @$vols;
1022 # take note of the fact that we've looked at this set of volumes
1023 push( @seen, $_->id ) for @all;
1024 push( @rec_ids, $_->record ) for @all;
1026 # for each volume, see if there are any copies that have a
1027 # remote circ_lib (circ_lib != vol->owning_lib and != $o_lib ).
1029 unless( $override ) {
1032 $logger->debug("merge: searching for copies with remote circ_lib for volume ".$v->id);
1034 call_number => $v->id,
1035 circ_lib => { "not in" => [ $o_lib, $v->owning_lib ] },
1039 my $copies = $e->search_asset_copy($args, {idlist=>1});
1041 # if the copy's circ_lib matches the destination lib,
1043 return OpenILS::Event->new('COPY_REMOTE_CIRC_LIB') if @$copies;
1047 # see if there is a volume at the destination lib that
1048 # already has the requested label
1049 my $existing_vol = $e->search_asset_call_number(
1051 label => $vol->label,
1052 prefix => $vol->prefix,
1053 suffix => $vol->suffix,
1055 owning_lib => $o_lib,
1060 if( $existing_vol ) {
1062 if( grep { $_->id == $existing_vol->id } @all ) {
1063 # this volume is already accounted for in our list of volumes to merge
1064 $existing_vol = undef;
1067 # this volume exists on the destination record/owning_lib and must
1068 # be used as the destination for merging
1069 $logger->debug("merge: volume already exists at destination record: ".
1070 $existing_vol->id.' : '.$existing_vol->label) if $existing_vol;
1074 if( @all > 1 || $existing_vol ) {
1075 $logger->info("merge: found collisions in volume transfer");
1076 my @args = ($e, \@all);
1077 @args = ($e, \@all, $existing_vol) if $existing_vol;
1078 ($vol, $evt) = OpenILS::Application::Cat::Merge::merge_volumes(@args);
1079 return $evt if $evt;
1082 if( !$existing_vol ) {
1084 $vol->owning_lib($o_lib);
1086 $vol->editor($e->requestor->id);
1087 $vol->edit_date('now');
1089 $logger->info("merge: updating volume ".$vol->id);
1090 $e->update_asset_call_number($vol) or return $e->event;
1093 $logger->info("merge: bypassing volume update because existing volume used as target");
1096 # regardless of what volume was used as the destination,
1097 # update any copies that have moved over to the new lib
1098 my $copies = $e->search_asset_copy({call_number=>$vol->id, deleted => 'f'});
1100 # update circ lib on the copies - make this a method flag?
1101 for my $copy (@$copies) {
1102 next if $copy->circ_lib == $o_lib;
1103 $logger->info("merge: transfer moving circ lib on copy ".$copy->id);
1104 $copy->circ_lib($o_lib);
1105 $copy->editor($e->requestor->id);
1106 $copy->edit_date('now');
1107 $e->update_asset_copy($copy) or return $e->event;
1110 # Now see if any empty records need to be deleted after all of this
1113 $logger->debug("merge: seeing if we should delete record $_...");
1114 $evt = OpenILS::Application::Cat::BibCommon->delete_rec($e, $_)
1115 if OpenILS::Application::Cat::BibCommon->title_is_empty($e, $_);
1116 return $evt if $evt;
1120 $logger->info("merge: transfer succeeded");
1128 __PACKAGE__->register_method(
1129 api_name => 'open-ils.cat.call_number.find_or_create',
1130 method => 'find_or_create_volume',
1133 sub find_or_create_volume {
1134 my( $self, $conn, $auth, $label, $record_id, $org_id, $prefix, $suffix, $label_class ) = @_;
1135 my $e = new_editor(authtoken=>$auth, xact=>1);
1136 return $e->die_event unless $e->checkauth;
1137 my ($vol, $evt, $exists) =
1138 OpenILS::Application::Cat::AssetCommon->find_or_create_volume($e, $label, $record_id, $org_id, $prefix, $suffix, $label_class);
1139 return $evt if $evt;
1140 $e->rollback if $exists;
1142 return { 'acn_id' => $vol->id, 'existed' => $exists };
1146 __PACKAGE__->register_method(
1147 method => "create_serial_record_xml",
1148 api_name => "open-ils.cat.serial.record.xml.create.override",
1149 signature => q/@see open-ils.cat.serial.record.xml.create/);
1151 __PACKAGE__->register_method(
1152 method => "create_serial_record_xml",
1153 api_name => "open-ils.cat.serial.record.xml.create",
1155 Inserts a new serial record with the given XML
1159 sub create_serial_record_xml {
1160 my( $self, $client, $login, $source, $owning_lib, $record_id, $xml ) = @_;
1162 my $override = 1 if $self->api_name =~ /override/; # not currently used
1164 my $e = new_editor(xact=>1, authtoken=>$login);
1165 return $e->die_event unless $e->checkauth;
1166 return $e->die_event unless $e->allowed('CREATE_MFHD_RECORD', $owning_lib);
1168 # Auto-populate the location field of a placeholder MFHD record with the library name
1169 my $aou = $e->retrieve_actor_org_unit($owning_lib) or return $e->die_event;
1171 my $mfhd = Fieldmapper::serial::record_entry->new;
1173 $mfhd->source($source) if $source;
1174 $mfhd->record($record_id);
1175 $mfhd->creator($e->requestor->id);
1176 $mfhd->editor($e->requestor->id);
1177 $mfhd->create_date('now');
1178 $mfhd->edit_date('now');
1179 $mfhd->owning_lib($owning_lib);
1181 # If the caller did not pass in MFHD XML, create a placeholder record.
1182 # The placeholder will only contain the name of the owning library.
1183 # The goal is to generate common patterns for the caller in the UI that
1184 # then get passed in here.
1186 my $aou_name = $aou->name;
1189 xsi:schemaLocation="http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd"
1190 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1191 xmlns="http://www.loc.gov/MARC21/slim">
1192 <leader>00307ny a22001094 4500</leader>
1193 <controlfield tag="001">42153</controlfield>
1194 <controlfield tag="005">20090601182414.0</controlfield>
1195 <controlfield tag="004">$record_id</controlfield>
1196 <controlfield tag="008"> 4u####8###l# 4 uueng1 </controlfield>
1197 <datafield tag="852" ind1=" " ind2=" "> <subfield code="b">$aou_name</subfield></datafield>
1201 my $marcxml = XML::LibXML->new->parse_string($xml);
1202 $marcxml->documentElement->setNamespace("http://www.loc.gov/MARC21/slim", "marc", 1 );
1203 $marcxml->documentElement->setNamespace("http://www.loc.gov/MARC21/slim");
1205 $mfhd->marc($U->entityize($marcxml->documentElement->toString));
1207 $e->create_serial_record_entry($mfhd) or return $e->die_event;
1213 __PACKAGE__->register_method(
1214 method => "create_update_asset_copy_template",
1215 api_name => "open-ils.cat.asset.copy_template.create_or_update"
1218 sub create_update_asset_copy_template {
1219 my ($self, $client, $authtoken, $act) = @_;
1221 my $e = new_editor("xact" => 1, "authtoken" => $authtoken);
1222 return $e->die_event unless $e->checkauth;
1223 return $e->die_event unless $e->allowed(
1224 "ADMIN_ASSET_COPY_TEMPLATE", $act->owning_lib
1227 $act->editor($e->requestor->id);
1228 $act->edit_date("now");
1232 $act->creator($e->requestor->id);
1233 $act->create_date("now");
1235 $e->create_asset_copy_template($act) or return $e->die_event;
1238 $e->update_asset_copy_template($act) or return $e->die_event;
1239 $retval = $e->retrieve_asset_copy_template($e->data);
1241 $e->commit and return $retval;