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($U->get_bre_attrs([$rec->id], $editor)->{$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;
1244 __PACKAGE__->register_method(
1245 method => "acn_sms_msg",
1246 api_name => "open-ils.cat.acn.send_sms_text",
1248 Send an SMS text from an A/T template for specified call numbers.
1250 First parameter is null or an auth token (whether a null is allowed
1251 depends on the sms.disable_authentication_requirement.callnumbers OU
1254 Second parameter is the id of the context org.
1256 Third parameter is the code of the SMS carrier from the
1257 config.sms_carrier table.
1259 Fourth parameter is the SMS number.
1261 Fifth parameter is the ACN id's to target, though currently only the
1262 first ACN is used by the template (and the UI is only sending one).
1267 my($self, $conn, $auth, $org_id, $carrier, $number, $target_ids) = @_;
1269 my $sms_enable = $U->ou_ancestor_setting_value(
1270 $org_id || $U->fetch_org_tree->id,
1273 # We could maybe make a Validator for this on the templates
1274 if (! $U->is_true($sms_enable)) {
1278 my $disable_auth = $U->ou_ancestor_setting_value(
1279 $org_id || $U->fetch_org_tree->id,
1280 'sms.disable_authentication_requirement.callnumbers'
1285 ? (authtoken => $auth, xact => 1)
1288 return $e->event unless $disable_auth || $e->checkauth;
1290 my $targets = $e->batch_retrieve_asset_call_number($target_ids);
1292 $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1293 # simply making this method authoritative because of weirdness
1294 # with transaction handling in A/T code that causes rollback
1295 # failure down the line if handling many targets
1297 return undef unless @$targets;
1298 return $U->fire_object_event(
1300 'acn.format.sms_text', # hook
1303 undef, # granularity
1305 sms_carrier => $carrier,
1306 sms_notify => $number