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, $oargs, $strip_grps ) = @_;
111 my $override = 1 if $self->api_name =~ /override/;
112 $oargs = { all => 1 } unless defined $oargs;
114 my( $user_obj, $evt ) = $U->checksesperm($login, 'CREATE_MARC');
117 $logger->activity("user ".$user_obj->id." creating new MARC record");
119 my $meth = $self->method_lookup("open-ils.cat.biblio.record.xml.import");
121 $meth = $self->method_lookup(
122 "open-ils.cat.biblio.record.xml.import.override") if $override;
124 my ($s) = $meth->run($login, $xml, $source, $oargs, $strip_grps);
130 __PACKAGE__->register_method(
131 method => "biblio_record_replace_marc",
132 api_name => "open-ils.cat.biblio.record.xml.update",
135 Updates the XML for a given biblio record.
136 This does not change any other aspect of the record entry
137 exception the XML, the editor, and the edit date.
138 @return The update record object
142 __PACKAGE__->register_method(
143 method => 'biblio_record_replace_marc',
144 api_name => 'open-ils.cat.biblio.record.marc.replace',
146 @param auth The authtoken
147 @param recid The record whose MARC we're replacing
148 @param newxml The new xml to use
152 __PACKAGE__->register_method(
153 method => 'biblio_record_replace_marc',
154 api_name => 'open-ils.cat.biblio.record.marc.replace.override',
155 signature => q/@see open-ils.cat.biblio.record.marc.replace/
158 sub biblio_record_replace_marc {
159 my( $self, $conn, $auth, $recid, $newxml, $source, $oargs, $strip_grps ) = @_;
160 my $e = new_editor(authtoken=>$auth, xact=>1);
161 return $e->die_event unless $e->checkauth;
162 return $e->die_event unless $e->allowed('UPDATE_MARC', $e->requestor->ws_ou);
164 my $fix_tcn = $self->api_name =~ /replace/o;
165 if($self->api_name =~ /override/o) {
166 $oargs = { all => 1 } unless defined $oargs;
171 my $res = OpenILS::Application::Cat::BibCommon->biblio_record_replace_marc(
172 $e, $recid, $newxml, $source, $fix_tcn, $oargs, $strip_grps);
174 $e->commit unless $U->event_code($res);
179 __PACKAGE__->register_method(
180 method => "template_overlay_biblio_record_entry",
181 api_name => "open-ils.cat.biblio.record_entry.template_overlay",
184 Overlays biblio.record_entry MARC values
185 @param auth The authtoken
186 @param records The record ids to be updated by the template
187 @param template The overlay template
188 @return Stream of hashes record id in the key "record" and t or f for the success of the overlay operation in key "success"
192 sub template_overlay_biblio_record_entry {
193 my($self, $conn, $auth, $records, $template) = @_;
194 my $e = new_editor(authtoken=>$auth, xact=>1);
195 return $e->die_event unless $e->checkauth;
197 $records = [$records] if (!ref($records));
199 for my $rid ( @$records ) {
200 my $rec = $e->retrieve_biblio_record_entry($rid);
203 unless ($e->allowed('UPDATE_RECORD', $rec->owner, $rec)) {
204 $conn->respond({ record => $rid, success => 'f' });
208 my $success = $e->json_query(
209 { from => [ 'vandelay.template_overlay_bib_record', $template, $rid ] }
210 )->[0]->{'vandelay.template_overlay_bib_record'};
212 $conn->respond({ record => $rid, success => $success });
219 __PACKAGE__->register_method(
220 method => "template_overlay_container",
221 api_name => "open-ils.cat.container.template_overlay",
224 Overlays biblio.record_entry MARC values
225 @param auth The authtoken
226 @param container The container, um, containing the records to be updated by the template
227 @param template The overlay template, or nothing and the method will look for a negative bib id in the container
228 @return Stream of hashes record id in the key "record" and t or f for the success of the overlay operation in key "success"
232 __PACKAGE__->register_method(
233 method => "template_overlay_container",
234 api_name => "open-ils.cat.container.template_overlay.background",
237 Overlays biblio.record_entry MARC values
238 @param auth The authtoken
239 @param container The container, um, containing the records to be updated by the template
240 @param template The overlay template, or nothing and the method will look for a negative bib id in the container
241 @return Cache key to check for status of the container overlay
245 sub template_overlay_container {
246 my($self, $conn, $auth, $container, $template) = @_;
247 my $e = new_editor(authtoken=>$auth, xact=>1);
248 return $e->die_event unless $e->checkauth;
250 my $actor = OpenSRF::AppSession->create('open-ils.actor') if ($self->api_name =~ /background$/);
252 my $items = $e->search_container_biblio_record_entry_bucket_item({ bucket => $container });
256 ($titem) = grep { $_->target_biblio_record_entry < 0 } @$items;
261 $items = [grep { $_->target_biblio_record_entry > 0 } @$items];
263 $template = $e->retrieve_biblio_record_entry( $titem->target_biblio_record_entry )->marc;
269 $conn->respond_complete(
270 $actor->request('open-ils.actor.anon_cache.set_value', $auth, res_list => $responses)->gather(1)
273 for my $item ( @$items ) {
274 my $rec = $e->retrieve_biblio_record_entry($item->target_biblio_record_entry);
278 if ($e->allowed('UPDATE_RECORD', $rec->owner, $rec)) {
279 $success = $e->json_query(
280 { from => [ 'vandelay.template_overlay_bib_record', $template, $rec->id ] }
281 )->[0]->{'vandelay.template_overlay_bib_record'};
284 $some_failed++ if ($success eq 'f');
287 push @$responses, { record => $rec->id, success => $success };
288 $actor->request('open-ils.actor.anon_cache.set_value', $auth, res_list => $responses);
290 $conn->respond({ record => $rec->id, success => $success });
293 if ($success eq 't') {
294 unless ($e->delete_container_biblio_record_entry_bucket_item($item)) {
297 push @$responses, { complete => 1, success => 'f' };
298 $actor->request('open-ils.actor.anon_cache.set_value', $auth, res_list => $responses);
301 return { complete => 1, success => 'f' };
307 if ($titem && !$some_failed) {
308 return $e->die_event unless ($e->delete_container_biblio_record_entry_bucket_item($titem));
313 push @$responses, { complete => 1, success => 't' };
314 $actor->request('open-ils.actor.anon_cache.set_value', $auth, res_list => $responses);
316 return { complete => 1, success => 't' };
320 push @$responses, { complete => 1, success => 'f' };
321 $actor->request('open-ils.actor.anon_cache.set_value', $auth, res_list => $responses);
323 return { complete => 1, success => 'f' };
329 __PACKAGE__->register_method(
330 method => "update_biblio_record_entry",
331 api_name => "open-ils.cat.biblio.record_entry.update",
333 Updates a biblio.record_entry
334 @param auth The authtoken
335 @param record The record with updated values
336 @return 1 on success, Event on error.
340 sub update_biblio_record_entry {
341 my($self, $conn, $auth, $record) = @_;
342 my $e = new_editor(authtoken=>$auth, xact=>1);
343 return $e->die_event unless $e->checkauth;
344 return $e->die_event unless $e->allowed('UPDATE_RECORD');
345 $e->update_biblio_record_entry($record) or return $e->die_event;
350 __PACKAGE__->register_method(
351 method => "undelete_biblio_record_entry",
352 api_name => "open-ils.cat.biblio.record_entry.undelete",
354 Un-deletes a record and sets active=true
355 @param auth The authtoken
356 @param record The record_id to ressurect
357 @return 1 on success, Event on error.
360 sub undelete_biblio_record_entry {
361 my($self, $conn, $auth, $record_id) = @_;
362 my $e = new_editor(authtoken=>$auth, xact=>1);
363 return $e->die_event unless $e->checkauth;
364 return $e->die_event unless $e->allowed('UPDATE_RECORD');
366 my $record = $e->retrieve_biblio_record_entry($record_id)
367 or return $e->die_event;
368 $record->deleted('f');
369 $record->active('t');
371 # Set the leader/05 to indicate that the record has been corrected/revised
372 my $marc = $record->marc();
373 $marc =~ s{(<leader>.{5}).}{$1c};
374 $record->marc($marc);
376 # no 2 non-deleted records can have the same tcn_value
377 my $existing = $e->search_biblio_record_entry(
379 tcn_value => $record->tcn_value,
380 id => {'!=' => $record_id}
382 return OpenILS::Event->new('TCN_EXISTS') if @$existing;
384 $e->update_biblio_record_entry($record) or return $e->die_event;
390 __PACKAGE__->register_method(
391 method => "biblio_record_xml_import",
392 api_name => "open-ils.cat.biblio.record.xml.import.override",
393 signature => q/@see open-ils.cat.biblio.record.xml.import/);
395 __PACKAGE__->register_method(
396 method => "biblio_record_xml_import",
397 api_name => "open-ils.cat.biblio.record.xml.import",
398 notes => <<" NOTES");
399 Takes a marcxml record and imports the record into the database. In this
400 case, the marcxml record is assumed to be a complete record (i.e. valid
401 MARC). The title control number is taken from (whichever comes first)
402 tags 001, 039[ab], 020a, 022a, 010, 035a and whichever does not already exist
404 user_session must have IMPORT_MARC permissions
408 sub biblio_record_xml_import {
409 my( $self, $client, $authtoken, $xml, $source, $auto_tcn, $oargs, $strip_grps) = @_;
410 my $e = new_editor(xact=>1, authtoken=>$authtoken);
411 return $e->die_event unless $e->checkauth;
412 return $e->die_event unless $e->allowed('IMPORT_MARC', $e->requestor->ws_ou);
414 if ($self->api_name =~ /override/) {
415 $oargs = { all => 1 } unless defined $oargs;
419 my $record = OpenILS::Application::Cat::BibCommon->biblio_record_xml_import(
420 $e, $xml, $source, $auto_tcn, $oargs, $strip_grps);
422 return $record if $U->event_code($record);
429 __PACKAGE__->register_method(
430 method => "biblio_record_record_metadata",
431 api_name => "open-ils.cat.biblio.record.metadata.retrieve",
433 argc => 2, #(session_id, list of bre ids )
434 notes => "Returns a list of slim-downed bre objects based on the " .
438 sub biblio_record_record_metadata {
439 my( $self, $client, $authtoken, $ids ) = @_;
441 return [] unless $ids and @$ids;
443 my $editor = new_editor(authtoken => $authtoken);
444 return $editor->event unless $editor->checkauth;
445 return $editor->event unless $editor->allowed('VIEW_USER');
450 return $editor->event unless
451 my $rec = $editor->retrieve_biblio_record_entry($_);
452 $rec->creator($editor->retrieve_actor_user($rec->creator));
453 $rec->editor($editor->retrieve_actor_user($rec->editor));
454 $rec->attrs($U->get_bre_attrs([$rec->id], $editor)->{$rec->id});
455 $rec->clear_marc; # slim the record down
456 push( @results, $rec );
464 __PACKAGE__->register_method(
465 method => "biblio_record_marc_cn",
466 api_name => "open-ils.cat.biblio.record.marc_cn.retrieve",
467 argc => 1, #(bib id )
469 desc => 'Extracts call number candidates from a bibliographic record',
471 {desc => 'Record ID', type => 'number'},
472 {desc => '(Optional) Classification scheme ID', type => 'number'},
475 return => {desc => 'Hash of candidate call numbers identified by tag' }
478 sub biblio_record_marc_cn {
479 my( $self, $client, $id, $class ) = @_;
481 my $e = new_editor();
482 my $marc = $e->retrieve_biblio_record_entry($id)->marc;
484 my $doc = XML::LibXML->new->parse_string($marc);
485 $doc->documentElement->setNamespace( "http://www.loc.gov/MARC21/slim", "marc", 1 );
490 @fields = split(/,/, $e->retrieve_asset_call_number_class($class)->field);
492 @fields = qw/050ab 055ab 060ab 070ab 080ab 082ab 086ab 088ab 090 092 096 098 099/;
495 # Get field/subfield combos based on acnc value; for example "050ab,055ab"
497 foreach my $field (@fields) {
498 my $tag = substr($field, 0, 3);
499 $logger->debug("Tag = $tag");
500 my @node = $doc->findnodes("//marc:datafield[\@tag='$tag']");
502 # Now parse the subfields and build up the subfield XPath
503 my @subfields = split(//, substr($field, 3));
505 # If they give us no subfields to parse, default to just the 'a'
510 foreach my $sf (@subfields) {
511 $subxpath .= "\@code='$sf' or ";
513 $subxpath = substr($subxpath, 0, -4);
514 $logger->debug("subxpath = $subxpath");
516 # Find the contents of the specified subfields
517 foreach my $x (@node) {
518 my $cn = $x->findvalue("marc:subfield[$subxpath]");
519 push @res, {$tag => $cn} if ($cn);
526 __PACKAGE__->register_method(
527 method => 'autogen_barcodes',
528 api_name => "open-ils.cat.item.barcode.autogen",
530 desc => 'Returns N generated barcodes following a specified barcode.',
532 {desc => 'Authentication token', type => 'string'},
533 {desc => 'Barcode which the sequence should follow from', type => 'string'},
534 {desc => 'Number of barcodes to generate', type => 'number'},
535 {desc => 'Options hash. Currently you can pass in checkdigit : false to disable the use of checkdigits.'}
537 return => {desc => 'Array of generated barcodes'}
541 sub autogen_barcodes {
542 my( $self, $client, $auth, $barcode, $num_of_barcodes, $options ) = @_;
543 my $e = new_editor(authtoken => $auth);
544 return $e->event unless $e->checkauth;
545 return $e->event unless $e->allowed('UPDATE_COPY', $e->requestor->ws_ou);
548 my $barcode_text = '';
549 my $barcode_number = 0;
551 if ($barcode =~ /^(\D+)/) { $barcode_text = $1; }
552 if ($barcode =~ /(\d+)$/) { $barcode_number = $1; }
555 for (my $i = 1; $i <= $num_of_barcodes; $i++) {
556 my $calculated_barcode;
558 # default is to use checkdigits, so looking for an explicit false here
559 if (defined $$options{'checkdigit'} && ! $$options{'checkdigit'}) {
560 $calculated_barcode = $barcode_number + $i;
562 if ($barcode_number =~ /^\d{8}$/) {
563 $calculated_barcode = add_codabar_checkdigit($barcode_number + $i, 0);
564 } elsif ($barcode_number =~ /^\d{9}$/) {
565 $calculated_barcode = add_codabar_checkdigit($barcode_number + $i*10, 1); # strip last digit
566 } elsif ($barcode_number =~ /^\d{13}$/) {
567 $calculated_barcode = add_codabar_checkdigit($barcode_number + $i, 0);
568 } elsif ($barcode_number =~ /^\d{14}$/) {
569 $calculated_barcode = add_codabar_checkdigit($barcode_number + $i*10, 1); # strip last digit
571 $calculated_barcode = $barcode_number + $i;
574 push @res, $barcode_text . $calculated_barcode;
579 # Codabar doesn't define a checkdigit algorithm, but this one is typically used by libraries. gmcharlt++
580 sub add_codabar_checkdigit {
582 my $strip_last_digit = shift;
584 return $barcode if $barcode =~ /\D/;
585 $barcode = substr($barcode, 0, length($barcode)-1) if $strip_last_digit;
586 my @digits = split //, $barcode;
588 for (my $i = 1; $i < length($barcode); $i+=2) { # for a 13/14 digit barcode, would expect 1,3,5,7,9,11
589 $total += $digits[$i];
591 for (my $i = 0; $i < length($barcode); $i+=2) { # for a 13/14 digit barcode, would expect 0,2,4,6,8,10,12
592 $total += (2 * $digits[$i] >= 10) ? (2 * $digits[$i] - 9) : (2 * $digits[$i]);
594 my $remainder = $total % 10;
595 my $checkdigit = ($remainder == 0) ? $remainder : 10 - $remainder;
596 return $barcode . $checkdigit;
599 __PACKAGE__->register_method(
600 method => "orgs_for_title",
602 api_name => "open-ils.cat.actor.org_unit.retrieve_by_title"
606 my( $self, $client, $record_id ) = @_;
608 my $vols = $U->simple_scalar_request(
610 "open-ils.cstore.direct.asset.call_number.search.atomic",
611 { record => $record_id, deleted => 'f' });
613 my $orgs = { map {$_->owning_lib => 1 } @$vols };
614 return [ keys %$orgs ];
618 __PACKAGE__->register_method(
619 method => "retrieve_copies",
621 api_name => "open-ils.cat.asset.copy_tree.retrieve");
623 __PACKAGE__->register_method(
624 method => "retrieve_copies",
625 api_name => "open-ils.cat.asset.copy_tree.global.retrieve");
627 # user_session may be null/undef
628 sub retrieve_copies {
630 my( $self, $client, $user_session, $docid, @org_ids ) = @_;
632 if(ref($org_ids[0])) { @org_ids = @{$org_ids[0]}; }
636 # grabbing copy trees should be available for everyone..
637 if(!@org_ids and $user_session) {
638 my($user_obj, $evt) = OpenILS::Application::AppUtils->checkses($user_session);
640 @org_ids = ($user_obj->home_ou);
643 if( $self->api_name =~ /global/ ) {
644 return _build_volume_list( { record => $docid, deleted => 'f', label => { '<>' => '##URI##' } } );
649 for my $orgid (@org_ids) {
650 my $vols = _build_volume_list(
651 { record => $docid, owning_lib => $orgid, deleted => 'f', label => { '<>' => '##URI##' } } );
652 push( @all_vols, @$vols );
662 sub _build_volume_list {
663 my $search_hash = shift;
665 $search_hash->{deleted} = 'f';
666 my $e = new_editor();
668 my $vols = $e->search_asset_call_number([
672 flesh_fields => { acn => ['prefix','suffix','label_class'] },
673 'order_by' => { 'acn' => 'oils_text_as_bytea(label_sortkey), oils_text_as_bytea(label), id, owning_lib' }
679 for my $volume (@$vols) {
681 my $copies = $e->search_asset_copy([
682 { call_number => $volume->id , deleted => 'f' },
688 bmp => { type => 'left' }
693 flesh_fields => { acp => ['stat_cat_entries','parts'] },
695 {'class' => 'bmp', 'field' => 'label_sortkey', 'transform' => 'oils_text_as_bytea'},
696 {'class' => 'bmp', 'field' => 'label', 'transform' => 'oils_text_as_bytea'},
697 {'class' => 'acp', 'field' => 'barcode'}
702 for my $c (@$copies) {
703 if( $c->status == OILS_COPY_STATUS_CHECKED_OUT ) {
705 $e->search_action_circulation(
707 { target_copy => $c->id },
709 order_by => { circ => 'xact_start desc' },
718 $volume->copies($copies);
719 push( @volumes, $volume );
722 #$session->disconnect();
728 __PACKAGE__->register_method(
729 method => "fleshed_copy_update",
730 api_name => "open-ils.cat.asset.copy.fleshed.batch.update",);
732 __PACKAGE__->register_method(
733 method => "fleshed_copy_update",
734 api_name => "open-ils.cat.asset.copy.fleshed.batch.update.override",);
737 sub fleshed_copy_update {
738 my( $self, $conn, $auth, $copies, $delete_stats, $oargs, $create_parts ) = @_;
739 return 1 unless ref $copies;
740 my( $reqr, $evt ) = $U->checkses($auth);
742 my $editor = new_editor(requestor => $reqr, xact => 1);
743 if ($self->api_name =~ /override/) {
744 $oargs = { all => 1 } unless defined $oargs;
748 my $retarget_holds = [];
749 $evt = OpenILS::Application::Cat::AssetCommon->update_fleshed_copies(
750 $editor, $oargs, undef, $copies, $delete_stats, $retarget_holds, undef, $create_parts);
753 $logger->info("fleshed copy update failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
759 $logger->info("fleshed copy update successfully updated ".scalar(@$copies)." copies");
760 reset_hold_list($auth, $retarget_holds);
765 sub reset_hold_list {
766 my($auth, $hold_ids) = @_;
767 return unless @$hold_ids;
768 $logger->info("reseting holds after copy status change: @$hold_ids");
769 my $ses = OpenSRF::AppSession->create('open-ils.circ');
770 $ses->request('open-ils.circ.hold.reset.batch', $auth, $hold_ids);
773 __PACKAGE__->register_method(
774 method => "transfer_copies_to_volume",
775 api_name => "open-ils.cat.transfer_copies_to_volume",
778 desc => 'Transfers specified copies to the specified call number, and changes Circ Lib to match the new Owning Lib.',
780 {desc => 'Authtoken', type => 'string'},
781 {desc => 'Call Number ID', type => 'number'},
782 {desc => 'Array of Copy IDs', type => 'array'},
785 return => {desc => '1 on success, Event on error'}
788 __PACKAGE__->register_method(
789 method => "transfer_copies_to_volume",
790 api_name => "open-ils.cat.transfer_copies_to_volume.override",);
792 sub transfer_copies_to_volume {
793 my( $self, $conn, $auth, $volume, $copies, $oargs ) = @_;
794 my $delete_stats = 1;
795 my $force_delete_empty_bib = undef;
796 my $create_parts = undef;
800 return 1 unless ref $copies;
801 my( $reqr, $evt ) = $U->checkses($auth);
803 my $editor = new_editor(requestor => $reqr, xact => 1);
804 if ($self->api_name =~ /override/) {
805 $oargs = { all => 1 } unless defined $oargs;
810 # does the volume exist? good, we also need its owning_lib later
811 my( $cn, $cn_evt ) = $U->fetch_callnumber( $volume, 0, $editor );
812 return $cn_evt if $cn_evt;
814 # flesh and munge the copies
815 my $fleshed_copies = [];
816 my ($copy, $copy_evt);
817 foreach my $copy_id ( @{ $copies } ) {
818 ($copy, $copy_evt) = $U->fetch_copy($copy_id);
819 return $copy_evt if $copy_evt;
820 $copy->call_number( $volume );
821 $copy->circ_lib( $cn->owning_lib() );
822 $copy->ischanged( 't' );
823 push @$fleshed_copies, $copy;
827 my $retarget_holds = [];
828 $evt = OpenILS::Application::Cat::AssetCommon->update_fleshed_copies(
829 $editor, $oargs, undef, $fleshed_copies, $delete_stats, $retarget_holds, $force_delete_empty_bib, $create_parts);
832 $logger->info("copy to volume transfer failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
838 $logger->info("copy to volume transfer successfully updated ".scalar(@$copies)." copies");
839 reset_hold_list($auth, $retarget_holds);
844 __PACKAGE__->register_method(
845 method => 'in_db_merge',
846 api_name => 'open-ils.cat.biblio.records.merge',
848 Merges a group of records
849 @param auth The login session key
850 @param master The id of the record all other records should be merged into
851 @param records Array of records to be merged into the master record
852 @return 1 on success, Event on error.
857 my( $self, $conn, $auth, $master, $records ) = @_;
859 my $editor = new_editor( authtoken => $auth, xact => 1 );
860 return $editor->die_event unless $editor->checkauth;
861 return $editor->die_event unless $editor->allowed('MERGE_BIB_RECORDS'); # TODO see below about record ownership
864 for my $source ( @$records ) {
865 #XXX we actually /will/ want to check perms for master and sources after record ownership exists
867 # This stored proc (asset.merge_record_assets(target,source)) has the side effects of
868 # moving call_number, title-type (and some volume-type) hold_request and uri-mapping
869 # objects from the source record to the target record, so must be called from within
872 $count += $editor->json_query({
876 transform => 'asset.merge_record_assets',
882 where => { id => $master }
883 })->[0]->{count}; # count of objects moved, of all types
891 __PACKAGE__->register_method(
892 method => 'in_db_auth_merge',
893 api_name => 'open-ils.cat.authority.records.merge',
895 Merges a group of authority records
896 @param auth The login session key
897 @param master The id of the record all other records should be merged into
898 @param records Array of records to be merged into the master record
899 @return 1 on success, Event on error.
903 sub in_db_auth_merge {
904 my( $self, $conn, $auth, $master, $records ) = @_;
906 my $editor = new_editor( authtoken => $auth, xact => 1 );
907 return $editor->die_event unless $editor->checkauth;
908 return $editor->die_event unless $editor->allowed('MERGE_AUTH_RECORDS'); # TODO see below about record ownership
911 for my $source ( @$records ) {
912 $count += $editor->json_query({
916 transform => 'authority.merge_records',
922 where => { id => $master }
923 })->[0]->{count}; # count of objects moved, of all types
930 __PACKAGE__->register_method(
931 method => "fleshed_volume_update",
932 api_name => "open-ils.cat.asset.volume.fleshed.batch.update",);
934 __PACKAGE__->register_method(
935 method => "fleshed_volume_update",
936 api_name => "open-ils.cat.asset.volume.fleshed.batch.update.override",);
938 sub fleshed_volume_update {
939 my( $self, $conn, $auth, $volumes, $delete_stats, $options, $oargs ) = @_;
940 my( $reqr, $evt ) = $U->checkses($auth);
944 if ($self->api_name =~ /override/) {
945 $oargs = { all => 1 } unless defined $oargs;
949 my $editor = new_editor( requestor => $reqr, xact => 1 );
950 my $retarget_holds = [];
951 my $auto_merge_vols = $options->{auto_merge_vols};
952 my $create_parts = $options->{create_parts};
954 for my $vol (@$volumes) {
955 $logger->info("vol-update: investigating volume ".$vol->id);
957 $vol->editor($reqr->id);
958 $vol->edit_date('now');
960 my $copies = $vol->copies;
963 $vol->editor($editor->requestor->id);
964 $vol->edit_date('now');
966 if( $vol->isdeleted ) {
968 $logger->info("vol-update: deleting volume");
969 return $editor->die_event unless
970 $editor->allowed('UPDATE_VOLUME', $vol->owning_lib);
972 if(my $evt = $assetcom->delete_volume($editor, $vol, $oargs, $$options{force_delete_copies})) {
977 return $editor->die_event unless
978 $editor->update_asset_call_number($vol);
980 } elsif( $vol->isnew ) {
981 $logger->info("vol-update: creating volume");
982 $evt = $assetcom->create_volume( $oargs, $editor, $vol );
985 } elsif( $vol->ischanged ) {
986 $logger->info("vol-update: update volume");
987 my $resp = update_volume($vol, $editor, ($oargs->{all} or grep { $_ eq 'VOLUME_LABEL_EXISTS' } @{$oargs->{events}} or $auto_merge_vols));
988 return $resp->{evt} if $resp->{evt};
989 $vol = $resp->{merge_vol} if $resp->{merge_vol};
992 # now update any attached copies
993 if( $copies and @$copies and !$vol->isdeleted ) {
994 $_->call_number($vol->id) for @$copies;
995 $evt = $assetcom->update_fleshed_copies(
996 $editor, $oargs, $vol, $copies, $delete_stats, $retarget_holds, undef, $create_parts);
1002 reset_hold_list($auth, $retarget_holds);
1003 return scalar(@$volumes);
1010 my $auto_merge = shift;
1014 return {evt => $editor->event} unless
1015 $editor->allowed('UPDATE_VOLUME', $vol->owning_lib);
1017 return {evt => $evt}
1018 if ( $evt = OpenILS::Application::Cat::AssetCommon->org_cannot_have_vols($editor, $vol->owning_lib) );
1020 my $vols = $editor->search_asset_call_number({
1021 owning_lib => $vol->owning_lib,
1022 record => $vol->record,
1023 label => $vol->label,
1024 prefix => $vol->prefix,
1025 suffix => $vol->suffix,
1027 id => {'!=' => $vol->id}
1034 # If the auto-merge option is on, merge our updated volume into the existing
1035 # volume with the same record + owner + label.
1036 ($merge_vol, $evt) = OpenILS::Application::Cat::Merge::merge_volumes($editor, [$vol], $vols->[0]);
1037 return {evt => $evt, merge_vol => $merge_vol};
1040 return {evt => OpenILS::Event->new('VOLUME_LABEL_EXISTS', payload => $vol->id)};
1044 return {evt => $editor->die_event} unless $editor->update_asset_call_number($vol);
1050 __PACKAGE__->register_method (
1051 method => 'delete_bib_record',
1052 api_name => 'open-ils.cat.biblio.record_entry.delete');
1054 sub delete_bib_record {
1055 my($self, $conn, $auth, $rec_id) = @_;
1056 my $e = new_editor(xact=>1, authtoken=>$auth);
1057 return $e->die_event unless $e->checkauth;
1058 return $e->die_event unless $e->allowed('DELETE_RECORD', $e->requestor->ws_ou);
1059 my $vols = $e->search_asset_call_number({record=>$rec_id, deleted=>'f'});
1060 return OpenILS::Event->new('RECORD_NOT_EMPTY', payload=>$rec_id) if @$vols;
1061 my $evt = OpenILS::Application::Cat::BibCommon->delete_rec($e, $rec_id);
1062 if($evt) { $e->rollback; return $evt; }
1069 __PACKAGE__->register_method (
1070 method => 'batch_volume_transfer',
1071 api_name => 'open-ils.cat.asset.volume.batch.transfer',
1074 __PACKAGE__->register_method (
1075 method => 'batch_volume_transfer',
1076 api_name => 'open-ils.cat.asset.volume.batch.transfer.override',
1080 sub batch_volume_transfer {
1081 my( $self, $conn, $auth, $args, $oargs ) = @_;
1084 my $rec = $$args{docid};
1085 my $o_lib = $$args{lib};
1086 my $vol_ids = $$args{volumes};
1088 my $override = 1 if $self->api_name =~ /override/;
1089 $oargs = { all => 1 } unless defined $oargs;
1091 $logger->info("merge: transferring volumes to lib=$o_lib and record=$rec");
1093 my $e = new_editor(authtoken => $auth, xact =>1);
1094 return $e->event unless $e->checkauth;
1095 return $e->event unless $e->allowed('UPDATE_VOLUME', $o_lib);
1097 my $dorg = $e->retrieve_actor_org_unit($o_lib)
1098 or return $e->event;
1100 my $ou_type = $e->retrieve_actor_org_unit_type($dorg->ou_type)
1101 or return $e->event;
1103 return $evt if ( $evt = OpenILS::Application::Cat::AssetCommon->org_cannot_have_vols($e, $o_lib) );
1105 my $vols = $e->batch_retrieve_asset_call_number($vol_ids);
1110 for my $vol (@$vols) {
1112 # if we've already looked at this volume, go to the next
1113 next if !$vol or grep { $vol->id == $_ } @seen;
1115 # grab all of the volumes in the list that have
1116 # the same label so they can be merged
1117 my @all = grep { $_->label eq $vol->label } @$vols;
1119 # take note of the fact that we've looked at this set of volumes
1120 push( @seen, $_->id ) for @all;
1121 push( @rec_ids, $_->record ) for @all;
1123 # for each volume, see if there are any copies that have a
1124 # remote circ_lib (circ_lib != vol->owning_lib and != $o_lib ).
1126 unless( $override && ($oargs->{all} || grep { $_ eq 'COPY_REMOTE_CIRC_LIB' } @{$oargs->{events}}) ) {
1129 $logger->debug("merge: searching for copies with remote circ_lib for volume ".$v->id);
1131 call_number => $v->id,
1132 circ_lib => { "not in" => [ $o_lib, $v->owning_lib ] },
1136 my $copies = $e->search_asset_copy($args, {idlist=>1});
1138 # if the copy's circ_lib matches the destination lib,
1140 return OpenILS::Event->new('COPY_REMOTE_CIRC_LIB') if @$copies;
1144 # see if there is a volume at the destination lib that
1145 # already has the requested label
1146 my $existing_vol = $e->search_asset_call_number(
1148 label => $vol->label,
1149 prefix => $vol->prefix,
1150 suffix => $vol->suffix,
1152 owning_lib => $o_lib,
1157 if( $existing_vol ) {
1159 if( grep { $_->id == $existing_vol->id } @all ) {
1160 # this volume is already accounted for in our list of volumes to merge
1161 $existing_vol = undef;
1164 # this volume exists on the destination record/owning_lib and must
1165 # be used as the destination for merging
1166 $logger->debug("merge: volume already exists at destination record: ".
1167 $existing_vol->id.' : '.$existing_vol->label) if $existing_vol;
1171 if( @all > 1 || $existing_vol ) {
1172 $logger->info("merge: found collisions in volume transfer");
1173 my @args = ($e, \@all);
1174 @args = ($e, \@all, $existing_vol) if $existing_vol;
1175 ($vol, $evt) = OpenILS::Application::Cat::Merge::merge_volumes(@args);
1176 return $evt if $evt;
1179 if( !$existing_vol ) {
1181 $vol->owning_lib($o_lib);
1183 $vol->editor($e->requestor->id);
1184 $vol->edit_date('now');
1186 $logger->info("merge: updating volume ".$vol->id);
1187 $e->update_asset_call_number($vol) or return $e->event;
1190 $logger->info("merge: bypassing volume update because existing volume used as target");
1193 # regardless of what volume was used as the destination,
1194 # update any copies that have moved over to the new lib
1195 my $copies = $e->search_asset_copy({call_number=>$vol->id, deleted => 'f'});
1197 # update circ lib on the copies - make this a method flag?
1198 for my $copy (@$copies) {
1199 next if $copy->circ_lib == $o_lib;
1200 $logger->info("merge: transfer moving circ lib on copy ".$copy->id);
1201 $copy->circ_lib($o_lib);
1202 $copy->editor($e->requestor->id);
1203 $copy->edit_date('now');
1204 $e->update_asset_copy($copy) or return $e->event;
1207 # Now see if any empty records need to be deleted after all of this
1210 $logger->debug("merge: seeing if we should delete record $_...");
1211 $evt = OpenILS::Application::Cat::BibCommon->delete_rec($e, $_)
1212 if OpenILS::Application::Cat::BibCommon->title_is_empty($e, $_);
1213 return $evt if $evt;
1217 $logger->info("merge: transfer succeeded");
1225 __PACKAGE__->register_method(
1226 api_name => 'open-ils.cat.call_number.find_or_create',
1227 method => 'find_or_create_volume',
1230 sub find_or_create_volume {
1231 my( $self, $conn, $auth, $label, $record_id, $org_id, $prefix, $suffix, $label_class ) = @_;
1232 my $e = new_editor(authtoken=>$auth, xact=>1);
1233 return $e->die_event unless $e->checkauth;
1234 my ($vol, $evt, $exists) =
1235 OpenILS::Application::Cat::AssetCommon->find_or_create_volume($e, $label, $record_id, $org_id, $prefix, $suffix, $label_class);
1236 return $evt if $evt;
1237 $e->rollback if $exists;
1239 return { 'acn_id' => $vol->id, 'existed' => $exists };
1243 __PACKAGE__->register_method(
1244 method => "create_serial_record_xml",
1245 api_name => "open-ils.cat.serial.record.xml.create.override",
1246 signature => q/@see open-ils.cat.serial.record.xml.create/);
1248 __PACKAGE__->register_method(
1249 method => "create_serial_record_xml",
1250 api_name => "open-ils.cat.serial.record.xml.create",
1252 Inserts a new serial record with the given XML
1256 sub create_serial_record_xml {
1257 my( $self, $client, $login, $source, $owning_lib, $record_id, $xml, $oargs ) = @_;
1259 my $override = 1 if $self->api_name =~ /override/; # not currently used
1260 $oargs = { all => 1 } unless defined $oargs; # Not currently used, but here for consistency.
1262 my $e = new_editor(xact=>1, authtoken=>$login);
1263 return $e->die_event unless $e->checkauth;
1264 return $e->die_event unless $e->allowed('CREATE_MFHD_RECORD', $owning_lib);
1266 # Auto-populate the location field of a placeholder MFHD record with the library name
1267 my $aou = $e->retrieve_actor_org_unit($owning_lib) or return $e->die_event;
1269 my $mfhd = Fieldmapper::serial::record_entry->new;
1271 $mfhd->source($source) if $source;
1272 $mfhd->record($record_id);
1273 $mfhd->creator($e->requestor->id);
1274 $mfhd->editor($e->requestor->id);
1275 $mfhd->create_date('now');
1276 $mfhd->edit_date('now');
1277 $mfhd->owning_lib($owning_lib);
1279 # If the caller did not pass in MFHD XML, create a placeholder record.
1280 # The placeholder will only contain the name of the owning library.
1281 # The goal is to generate common patterns for the caller in the UI that
1282 # then get passed in here.
1284 my $aou_name = $aou->name;
1287 xsi:schemaLocation="http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd"
1288 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1289 xmlns="http://www.loc.gov/MARC21/slim">
1290 <leader>00307ny a22001094 4500</leader>
1291 <controlfield tag="001">42153</controlfield>
1292 <controlfield tag="005">20090601182414.0</controlfield>
1293 <controlfield tag="004">$record_id</controlfield>
1294 <controlfield tag="008"> 4u####8###l# 4 uueng1 </controlfield>
1295 <datafield tag="852" ind1=" " ind2=" "> <subfield code="b">$aou_name</subfield></datafield>
1299 my $marcxml = XML::LibXML->new->parse_string($xml);
1300 $marcxml->documentElement->setNamespace("http://www.loc.gov/MARC21/slim", "marc", 1 );
1301 $marcxml->documentElement->setNamespace("http://www.loc.gov/MARC21/slim");
1303 $mfhd->marc($U->entityize($marcxml->documentElement->toString));
1305 $e->create_serial_record_entry($mfhd) or return $e->die_event;
1311 __PACKAGE__->register_method(
1312 method => "create_update_asset_copy_template",
1313 api_name => "open-ils.cat.asset.copy_template.create_or_update"
1316 sub create_update_asset_copy_template {
1317 my ($self, $client, $authtoken, $act) = @_;
1319 my $e = new_editor("xact" => 1, "authtoken" => $authtoken);
1320 return $e->die_event unless $e->checkauth;
1321 return $e->die_event unless $e->allowed(
1322 "ADMIN_ASSET_COPY_TEMPLATE", $act->owning_lib
1325 $act->editor($e->requestor->id);
1326 $act->edit_date("now");
1330 $act->creator($e->requestor->id);
1331 $act->create_date("now");
1333 $e->create_asset_copy_template($act) or return $e->die_event;
1336 $e->update_asset_copy_template($act) or return $e->die_event;
1337 $retval = $e->retrieve_asset_copy_template($e->data);
1339 $e->commit and return $retval;
1342 __PACKAGE__->register_method(
1343 method => "acn_sms_msg",
1344 api_name => "open-ils.cat.acn.send_sms_text",
1346 Send an SMS text from an A/T template for specified call numbers.
1348 First parameter is null or an auth token (whether a null is allowed
1349 depends on the sms.disable_authentication_requirement.callnumbers OU
1352 Second parameter is the id of the context org.
1354 Third parameter is the code of the SMS carrier from the
1355 config.sms_carrier table.
1357 Fourth parameter is the SMS number.
1359 Fifth parameter is the ACN id's to target, though currently only the
1360 first ACN is used by the template (and the UI is only sending one).
1365 my($self, $conn, $auth, $org_id, $carrier, $number, $target_ids) = @_;
1367 my $sms_enable = $U->ou_ancestor_setting_value(
1368 $org_id || $U->get_org_tree->id,
1371 # We could maybe make a Validator for this on the templates
1372 if (! $U->is_true($sms_enable)) {
1376 my $disable_auth = $U->ou_ancestor_setting_value(
1377 $org_id || $U->get_org_tree->id,
1378 'sms.disable_authentication_requirement.callnumbers'
1383 ? (authtoken => $auth, xact => 1)
1386 return $e->event unless $disable_auth || $e->checkauth;
1388 my $targets = $e->batch_retrieve_asset_call_number($target_ids);
1390 $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1391 # simply making this method authoritative because of weirdness
1392 # with transaction handling in A/T code that causes rollback
1393 # failure down the line if handling many targets
1395 return undef unless @$targets;
1396 return $U->fire_object_event(
1398 'acn.format.sms_text', # hook
1401 undef, # granularity
1403 sms_carrier => $carrier,
1404 sms_notify => $number
1411 __PACKAGE__->register_method(
1412 method => "fixed_field_values_by_rec_type",
1413 api_name => "open-ils.cat.biblio.fixed_field_values.by_rec_type",
1416 desc => 'Given a record type (as in cmfpm.rec_type), return fixed fields and their possible values as known to the DB',
1418 {desc => 'Record Type', type => 'string'},
1419 {desc => '(Optional) Fixed field', type => 'string'},
1422 return => {desc => 'an object in which the keys are fixed fields and the values are arrays representing the set of all unique values for that fixed field in that record type', type => 'object' }
1426 sub fixed_field_values_by_rec_type {
1427 my ($self, $conn, $rec_type, $fixed_field) = @_;
1430 my $values = $e->json_query({
1432 crad => ["fixed_field"],
1433 ccvm => [qw/code value/],
1434 cmfpm => [qw/length default_val/],
1442 fkey => "fixed_field",
1443 field => "fixed_field"
1450 "+cmfpm" => {rec_type => $rec_type},
1451 defined $fixed_field ?
1452 ("+crad" => {fixed_field => $fixed_field}) : ()
1455 {class => "crad", field => "fixed_field"},
1456 {class => "ccvm", field => "code"}
1458 }) or return $e->die_event;
1461 for my $row (@$values) {
1462 $result->{$row->{fixed_field}} ||= [];
1463 push @{$result->{$row->{fixed_field}}}, [@$row{qw/code value length default_val/}];
1469 __PACKAGE__->register_method(
1470 method => "retrieve_tag_table",
1471 api_name => "open-ils.cat.tag_table.all.retrieve.local",
1475 desc => "Retrieve set of MARC tags, subfields, and indicator values for the user's OU",
1477 {desc => 'Authtoken', type => 'string'},
1478 {desc => 'MARC Format', type => 'string'},
1479 {desc => 'MARC Record Type', type => 'string'},
1482 return => {desc => 'Structure representing the tag table available to that user', type => 'object' }
1484 __PACKAGE__->register_method(
1485 method => "retrieve_tag_table",
1486 api_name => "open-ils.cat.tag_table.all.retrieve.stock",
1490 desc => 'Retrieve set of MARC tags, subfields, and indicator values for stock MARC standard',
1492 {desc => 'Authtoken', type => 'string'},
1493 {desc => 'MARC Format', type => 'string'},
1494 {desc => 'MARC Record Type', type => 'string'},
1497 return => {desc => 'Structure representing the stock tag table', type => 'object' }
1499 __PACKAGE__->register_method(
1500 method => "retrieve_tag_table",
1501 api_name => "open-ils.cat.tag_table.field_list.retrieve.local",
1505 desc => "Retrieve set of MARC tags for available to the user's OU",
1507 {desc => 'Authtoken', type => 'string'},
1508 {desc => 'MARC Format', type => 'string'},
1509 {desc => 'MARC Record Type', type => 'string'},
1512 return => {desc => 'Structure representing the tags available to that user', type => 'object' }
1514 __PACKAGE__->register_method(
1515 method => "retrieve_tag_table",
1516 api_name => "open-ils.cat.tag_table.field_list.retrieve.stock",
1520 desc => 'Retrieve set of MARC tags for stock MARC standard',
1522 {desc => 'Authtoken', type => 'string'},
1523 {desc => 'MARC Format', type => 'string'},
1524 {desc => 'MARC Record Type', type => 'string'},
1527 return => {desc => 'Structure representing the stock MARC tags', type => 'object' }
1530 sub retrieve_tag_table {
1531 my( $self, $conn, $auth, $marc_format, $marc_record_type ) = @_;
1532 my $e = new_editor( authtoken=>$auth, xact=>1 );
1533 return $e->die_event unless $e->checkauth;
1534 return $e->die_event unless $e->allowed('UPDATE_MARC', $e->requestor->ws_ou);
1536 my $field_list_only = ($self->api_name =~ /\.field_list\./) ? 1 : 0;
1538 if ($self->api_name =~ /\.local$/) {
1539 $context_ou = $e->requestor->ws_ou;
1543 unless ($field_list_only) {
1544 my $subfields = $e->json_query(
1545 { from => [ 'config.ou_marc_subfields', 1, $marc_record_type, $context_ou ] }
1547 foreach my $sf (@$subfields) {
1549 code => $sf->{code},
1550 description => $sf->{description},
1551 mandatory => $sf->{mandatory},
1552 repeatable => $sf->{repeatable},
1554 if ($sf->{value_ctype}) {
1555 $sf_data->{value_list} = $e->json_query({
1556 select => { ccvm => [
1558 { column => 'value', alias => 'description' }
1562 where => { ctype => $sf->{value_ctype} },
1563 order_by => { ccvm => { code => {} } },
1566 push @{ $sf_by_tag{$sf->{tag}} }, $sf_data;
1570 my $fields = $e->json_query(
1571 { from => [ 'config.ou_marc_fields', 1, $marc_record_type, $context_ou ] }
1574 foreach my $field (@$fields) {
1575 next if $field->{hidden} eq 't';
1576 unless ($field_list_only) {
1577 my $tag = $field->{tag};
1578 if ($tag ge '010') {
1579 for my $pos (1..2) {
1580 my $ind_ccvm_key = "${marc_format}_${marc_record_type}_${tag}_ind_${pos}";
1581 my $indvals = $e->json_query({
1582 select => { ccvm => [
1584 { column => 'value', alias => 'description' }
1588 where => { ctype => $ind_ccvm_key }
1590 next unless defined($indvals);
1591 $field->{"ind$pos"} = $indvals;
1593 $field->{subfields} = exists($sf_by_tag{$tag}) ? $sf_by_tag{$tag} : [];
1596 $conn->respond($field);