]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Cat.pm
LP#1748924 Enhanced Billing Timestamp Support
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / OpenILS / Application / Cat.pm
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;
14 use OpenILS::Event;
15 use OpenILS::Const qw/:const/;
16
17 use XML::LibXML;
18 use Unicode::Normalize;
19 use Data::Dumper;
20 use OpenILS::Utils::CStoreEditor q/:funcs/;
21 use OpenILS::Perm;
22 use OpenSRF::Utils::SettingsClient;
23 use OpenSRF::Utils::Logger qw($logger);
24 use OpenSRF::AppSession;
25
26 my $U = "OpenILS::Application::AppUtils";
27 my $conf;
28 my %marctemplates;
29 my $assetcom = 'OpenILS::Application::Cat::AssetCommon';
30
31 __PACKAGE__->register_method(
32     method   => "retrieve_marc_template",
33     api_name => "open-ils.cat.biblio.marc_template.retrieve",
34     notes    => <<"    NOTES");
35     Returns a MARC 'record tree' based on a set of pre-defined templates.
36     Templates include : book
37     NOTES
38
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};
44 }
45
46 __PACKAGE__->register_method(
47     method   => 'fetch_marc_template_types',
48     api_name => 'open-ils.cat.marc_template.types.retrieve'
49 );
50
51 my $marc_template_files;
52
53 sub fetch_marc_template_types {
54     my( $self, $conn ) = @_;
55     __load_marc_templates();
56     return [ keys %$marc_template_files ];
57 }
58
59 sub __load_marc_templates {
60     return if $marc_template_files;
61     if(!$conf) { $conf = OpenSRF::Utils::SettingsClient->new; }
62
63     $marc_template_files = $conf->config_value(                    
64         "apps", "open-ils.cat","app_settings", "marctemplates" );
65
66     $logger->info("Loaded marc templates: " . Dumper($marc_template_files));
67 }
68
69 sub _load_marc_template {
70     my $type = shift;
71
72     __load_marc_templates();
73
74     my $template = $$marc_template_files{$type};
75     open( F, $template ) or 
76         throw OpenSRF::EX::ERROR ("Unable to open MARC template file: $template : $@");
77
78     my @xml = <F>;
79     close(F);
80     my $xml = join('', @xml);
81
82     return XML::LibXML->new->parse_string($xml)->documentElement->toString;
83 }
84
85
86
87 __PACKAGE__->register_method(
88     method   => 'fetch_bib_sources',
89     api_name => 'open-ils.cat.bib_sources.retrieve.all');
90
91 sub fetch_bib_sources {
92     return OpenILS::Application::Cat::BibCommon->fetch_bib_sources();
93 }
94
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/);
99
100 __PACKAGE__->register_method(
101     method    => "create_record_xml",
102     api_name  => "open-ils.cat.biblio.record.xml.create",
103     signature => q/
104         Inserts a new biblio with the given XML
105     /
106 );
107
108 sub create_record_xml {
109     my( $self, $client, $login, $xml, $source, $oargs, $strip_grps ) = @_;
110
111     my $override = 1 if $self->api_name =~ /override/;
112     $oargs = { all => 1 } unless defined $oargs;
113
114     my( $user_obj, $evt ) = $U->checksesperm($login, 'CREATE_MARC');
115     return $evt if $evt;
116
117     $logger->activity("user ".$user_obj->id." creating new MARC record");
118
119     my $meth = $self->method_lookup("open-ils.cat.biblio.record.xml.import");
120
121     $meth = $self->method_lookup(
122         "open-ils.cat.biblio.record.xml.import.override") if $override;
123
124     my ($s) = $meth->run($login, $xml, $source, $oargs, $strip_grps);
125     return $s;
126 }
127
128
129
130 __PACKAGE__->register_method(
131     method    => "biblio_record_replace_marc",
132     api_name  => "open-ils.cat.biblio.record.xml.update",
133     argc      => 3, 
134     signature => q/
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
139     /
140 );
141
142 __PACKAGE__->register_method(
143     method    => 'biblio_record_replace_marc',
144     api_name  => 'open-ils.cat.biblio.record.marc.replace',
145     signature => q/
146         @param auth The authtoken
147         @param recid The record whose MARC we're replacing
148         @param newxml The new xml to use
149     /
150 );
151
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/
156 );
157
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);
163
164     my $fix_tcn = $self->api_name =~ /replace/o;
165     if($self->api_name =~ /override/o) {
166         $oargs = { all => 1 } unless defined $oargs;
167     } else {
168         $oargs = {};
169     }
170
171     my $res = OpenILS::Application::Cat::BibCommon->biblio_record_replace_marc(
172         $e, $recid, $newxml, $source, $fix_tcn, $oargs, $strip_grps);
173
174     $e->commit unless $U->event_code($res);
175
176     return $res;
177 }
178
179 __PACKAGE__->register_method(
180     method    => "template_overlay_biblio_record_entry",
181     api_name  => "open-ils.cat.biblio.record_entry.template_overlay",
182     stream    => 1,
183     signature => q#
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"
189     #
190 );
191
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;
196
197     $records = [$records] if (!ref($records));
198
199     for my $rid ( @$records ) {
200         my $rec = $e->retrieve_biblio_record_entry($rid);
201         next unless $rec;
202
203         unless ($e->allowed('UPDATE_RECORD', $rec->owner, $rec)) {
204             $conn->respond({ record => $rid, success => 'f' });
205             next;
206         }
207
208         my $success = $e->json_query(
209             { from => [ 'vandelay.template_overlay_bib_record', $template, $rid ] }
210         )->[0]->{'vandelay.template_overlay_bib_record'};
211
212         $conn->respond({ record => $rid, success => $success });
213     }
214
215     $e->commit;
216     return undef;
217 }
218
219 __PACKAGE__->register_method(
220     method    => "template_overlay_container",
221     api_name  => "open-ils.cat.container.template_overlay",
222     stream    => 1,
223     signature => q#
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"
229     #
230 );
231
232 __PACKAGE__->register_method(
233     method    => "template_overlay_container",
234     api_name  => "open-ils.cat.container.template_overlay.background",
235     stream    => 1,
236     signature => q#
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
242     #
243 );
244
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;
249
250     my $actor = OpenSRF::AppSession->create('open-ils.actor') if ($self->api_name =~ /background$/);
251
252     my $items = $e->search_container_biblio_record_entry_bucket_item({ bucket => $container });
253
254     my $titem;
255     if (!$template) {
256         ($titem) = grep { $_->target_biblio_record_entry < 0 } @$items;
257         if (!$titem) {
258             $e->rollback;
259             return undef;
260         }
261         $items = [grep { $_->target_biblio_record_entry > 0 } @$items];
262
263         $template = $e->retrieve_biblio_record_entry( $titem->target_biblio_record_entry )->marc;
264     }
265
266     my $num_failed = 0;
267     my $num_succeeded = 0;
268
269     $conn->respond_complete(
270         $actor->request('open-ils.actor.anon_cache.set_value', $auth, batch_edit_progress => {})->gather(1)
271     ) if ($actor);
272
273     for my $item ( @$items ) {
274         my $rec = $e->retrieve_biblio_record_entry($item->target_biblio_record_entry);
275         next unless $rec;
276
277         my $success = 'f';
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'};
282         }
283
284         if ($success eq 'f') {
285             $num_failed++;
286         } else {
287             $num_succeeded++;
288         }
289
290         if ($actor) {
291             $actor->request(
292                 'open-ils.actor.anon_cache.set_value', $auth,
293                 batch_edit_progress => {
294                     succeeded => $num_succeeded,
295                     failed    => $num_failed
296                 },
297             );
298         } else {
299             $conn->respond({ record => $rec->id, success => $success });
300         }
301
302         if ($success eq 't') {
303             unless ($e->delete_container_biblio_record_entry_bucket_item($item)) {
304                 $e->rollback;
305                 if ($actor) {
306                     $actor->request(
307                         'open-ils.actor.anon_cache.set_value', $auth,
308                         batch_edit_progress => {
309                             complete => 1,
310                             success  => 'f',
311                             succeeded => $num_succeeded,
312                             failed    => $num_failed,
313                         }
314                     );
315                     return undef;
316                 } else {
317                     return { complete => 1, success => 'f' };
318                 }
319             }
320         }
321     }
322
323     if ($titem && !$num_failed) {
324         return $e->die_event unless ($e->delete_container_biblio_record_entry_bucket_item($titem));
325     }
326
327     if ($e->commit) {
328         if ($actor) {
329             $actor->request(
330                 'open-ils.actor.anon_cache.set_value', $auth,
331                 batch_edit_progress => {
332                     complete => 1,
333                     success  => 't',
334                     succeeded => $num_succeeded,
335                     failed    => $num_failed,
336                 }
337             );
338         } else {
339             return { complete => 1, success => 't' };
340         }
341     } else {
342         if ($actor) {
343             $actor->request(
344                 'open-ils.actor.anon_cache.set_value', $auth,
345                 batch_edit_progress => {
346                     complete => 1,
347                     success  => 'f',
348                     succeeded => $num_succeeded,
349                     failed    => $num_failed,
350                 }
351             );
352         } else {
353             return { complete => 1, success => 'f' };
354         }
355     }
356     return undef;
357 }
358
359 __PACKAGE__->register_method(
360     method    => "update_biblio_record_entry",
361     api_name  => "open-ils.cat.biblio.record_entry.update",
362     signature => q/
363         Updates a biblio.record_entry
364         @param auth The authtoken
365         @param record The record with updated values
366         @return 1 on success, Event on error.
367     /
368 );
369
370 sub update_biblio_record_entry {
371     my($self, $conn, $auth, $record) = @_;
372     my $e = new_editor(authtoken=>$auth, xact=>1);
373     return $e->die_event unless $e->checkauth;
374     return $e->die_event unless $e->allowed('UPDATE_RECORD');
375     $e->update_biblio_record_entry($record) or return $e->die_event;
376     $e->commit;
377     return 1;
378 }
379
380 __PACKAGE__->register_method(
381     method    => "undelete_biblio_record_entry",
382     api_name  => "open-ils.cat.biblio.record_entry.undelete",
383     signature => q/
384         Un-deletes a record and sets active=true
385         @param auth The authtoken
386         @param record The record_id to ressurect
387         @return 1 on success, Event on error.
388     /
389 );
390 sub undelete_biblio_record_entry {
391     my($self, $conn, $auth, $record_id) = @_;
392     my $e = new_editor(authtoken=>$auth, xact=>1);
393     return $e->die_event unless $e->checkauth;
394     return $e->die_event unless $e->allowed('UPDATE_RECORD');
395
396     my $record = $e->retrieve_biblio_record_entry($record_id)
397         or return $e->die_event;
398     $record->deleted('f');
399     $record->active('t');
400
401     # Set the leader/05 to indicate that the record has been corrected/revised
402     my $marc = $record->marc();
403     $marc =~ s{(<leader>.{5}).}{$1c};
404     $record->marc($marc);
405
406     # no 2 non-deleted records can have the same tcn_value
407     my $existing = $e->search_biblio_record_entry(
408         {   deleted => 'f', 
409             tcn_value => $record->tcn_value, 
410             id => {'!=' => $record_id}
411         }, {idlist => 1});
412     return OpenILS::Event->new('TCN_EXISTS') if @$existing;
413
414     $e->update_biblio_record_entry($record) or return $e->die_event;
415     $e->commit;
416     return 1;
417 }
418
419
420 __PACKAGE__->register_method(
421     method    => "biblio_record_xml_import",
422     api_name  => "open-ils.cat.biblio.record.xml.import.override",
423     signature => q/@see open-ils.cat.biblio.record.xml.import/);
424
425 __PACKAGE__->register_method(
426     method    => "biblio_record_xml_import",
427     api_name  => "open-ils.cat.biblio.record.xml.import",
428     notes     => <<"    NOTES");
429     Takes a marcxml record and imports the record into the database.  In this
430     case, the marcxml record is assumed to be a complete record (i.e. valid
431     MARC).  The title control number is taken from (whichever comes first)
432     tags 001, 039[ab], 020a, 022a, 010, 035a and whichever does not already exist
433     in the database.
434     user_session must have IMPORT_MARC permissions
435     NOTES
436
437
438 sub biblio_record_xml_import {
439     my( $self, $client, $authtoken, $xml, $source, $auto_tcn, $oargs, $strip_grps) = @_;
440     my $e = new_editor(xact=>1, authtoken=>$authtoken);
441     return $e->die_event unless $e->checkauth;
442     return $e->die_event unless $e->allowed('IMPORT_MARC', $e->requestor->ws_ou);
443
444     if ($self->api_name =~ /override/) {
445         $oargs = { all => 1 } unless defined $oargs;
446     } else {
447         $oargs = {};
448     }
449     my $record = OpenILS::Application::Cat::BibCommon->biblio_record_xml_import(
450         $e, $xml, $source, $auto_tcn, $oargs, $strip_grps);
451
452     return $record if $U->event_code($record);
453
454     $e->commit;
455
456     return $record;
457 }
458
459 __PACKAGE__->register_method(
460     method        => "biblio_record_record_metadata",
461     api_name      => "open-ils.cat.biblio.record.metadata.retrieve",
462     authoritative => 1,
463     argc          => 2, #(session_id, list of bre ids )
464     notes         => "Returns a list of slim-downed bre objects based on the " .
465                      "ids passed in",
466 );
467
468 sub biblio_record_record_metadata {
469     my( $self, $client, $authtoken, $ids ) = @_;
470
471     return [] unless $ids and @$ids;
472
473     my $editor = new_editor(authtoken => $authtoken);
474     return $editor->event unless $editor->checkauth;
475     return $editor->event unless $editor->allowed('VIEW_USER');
476
477     my @results;
478
479     for(@$ids) {
480         return $editor->event unless 
481             my $rec = $editor->retrieve_biblio_record_entry($_);
482         $rec->creator($editor->retrieve_actor_user($rec->creator));
483         $rec->editor($editor->retrieve_actor_user($rec->editor));
484         $rec->attrs($U->get_bre_attrs([$rec->id], $editor)->{$rec->id});
485         $rec->clear_marc; # slim the record down
486         push( @results, $rec );
487     }
488
489     return \@results;
490 }
491
492
493
494 __PACKAGE__->register_method(
495     method    => "biblio_record_marc_cn",
496     api_name  => "open-ils.cat.biblio.record.marc_cn.retrieve",
497     argc      => 1, #(bib id ) 
498     signature => {
499         desc   => 'Extracts call number candidates from a bibliographic record',
500         params => [
501             {desc => 'Record ID', type => 'number'},
502             {desc => '(Optional) Classification scheme ID', type => 'number'},
503         ]
504     },
505     return => {desc => 'Hash of candidate call numbers identified by tag' }
506 );
507
508 sub biblio_record_marc_cn {
509     my( $self, $client, $id, $class ) = @_;
510
511     my $e = new_editor();
512     my $marc = $e->retrieve_biblio_record_entry($id)->marc;
513
514     my $doc = XML::LibXML->new->parse_string($marc);
515     $doc->documentElement->setNamespace( "http://www.loc.gov/MARC21/slim", "marc", 1 );
516
517     my @fields;
518     my @res;
519     if ($class) {
520         @fields = split(/,/, $e->retrieve_asset_call_number_class($class)->field);
521     } else {
522         @fields = qw/050ab 055ab 060ab 070ab 080ab 082ab 086ab 088ab 090 092 096 098 099/;
523     }
524
525     # Get field/subfield combos based on acnc value; for example "050ab,055ab"
526
527     foreach my $field (@fields) {
528         my $tag = substr($field, 0, 3);
529         $logger->debug("Tag = $tag");
530         my @node = $doc->findnodes("//marc:datafield[\@tag='$tag']");
531
532         # Now parse the subfields and build up the subfield XPath
533         my @subfields = split(//, substr($field, 3));
534
535         # If they give us no subfields to parse, default to just the 'a'
536         if (!@subfields) {
537             @subfields = ('a');
538         }
539         my $subxpath;
540         foreach my $sf (@subfields) {
541             $subxpath .= "\@code='$sf' or ";
542         }
543         $subxpath = substr($subxpath, 0, -4);
544         $logger->debug("subxpath = $subxpath");
545
546         # Find the contents of the specified subfields
547         foreach my $x (@node) {
548             my $cn = $x->findvalue("marc:subfield[$subxpath]");
549             push @res, {$tag => $cn} if ($cn);
550         }
551     }
552
553     return \@res;
554 }
555
556 __PACKAGE__->register_method(
557     method    => 'autogen_barcodes',
558     api_name  => "open-ils.cat.item.barcode.autogen",
559     signature => {
560         desc   => 'Returns N generated barcodes following a specified barcode.',
561         params => [
562             {desc => 'Authentication token', type => 'string'},
563             {desc => 'Barcode which the sequence should follow from', type => 'string'},
564             {desc => 'Number of barcodes to generate', type => 'number'},
565             {desc => 'Options hash.  Currently you can pass in checkdigit : false to disable the use of checkdigits.'}
566         ],
567         return => {desc => 'Array of generated barcodes'}
568     }
569 );
570
571 sub autogen_barcodes {
572     my( $self, $client, $auth, $barcode, $num_of_barcodes, $options ) = @_;
573     my $e = new_editor(authtoken => $auth);
574     return $e->event unless $e->checkauth;
575     return $e->event unless $e->allowed('UPDATE_COPY', $e->requestor->ws_ou);
576     $options ||= {};
577
578     my $barcode_text = '';
579     my $barcode_number = 0;
580
581     if ($barcode =~ /^(\D+)/) { $barcode_text = $1; }
582     if ($barcode =~ /(\d+)$/) { $barcode_number = $1; }
583
584     my @res;
585     for (my $i = 1; $i <= $num_of_barcodes; $i++) {
586         my $calculated_barcode;
587
588         # default is to use checkdigits, so looking for an explicit false here
589         if (defined $$options{'checkdigit'} && ! $$options{'checkdigit'}) { 
590             $calculated_barcode = $barcode_number + $i;
591         } else {
592             if ($barcode_number =~ /^\d{8}$/) {
593                 $calculated_barcode = add_codabar_checkdigit($barcode_number + $i, 0);
594             } elsif ($barcode_number =~ /^\d{9}$/) {
595                 $calculated_barcode = add_codabar_checkdigit($barcode_number + $i*10, 1); # strip last digit
596             } elsif ($barcode_number =~ /^\d{13}$/) {
597                 $calculated_barcode = add_codabar_checkdigit($barcode_number + $i, 0);
598             } elsif ($barcode_number =~ /^\d{14}$/) {
599                 $calculated_barcode = add_codabar_checkdigit($barcode_number + $i*10, 1); # strip last digit
600             } else {
601                 $calculated_barcode = $barcode_number + $i;
602             }
603         }
604         push @res, $barcode_text . $calculated_barcode;
605     }
606     return \@res
607 }
608
609 # Codabar doesn't define a checkdigit algorithm, but this one is typically used by libraries.  gmcharlt++
610 sub add_codabar_checkdigit {
611     my $barcode = shift;
612     my $strip_last_digit = shift;
613
614     return $barcode if $barcode =~ /\D/;
615     $barcode = substr($barcode, 0, length($barcode)-1) if $strip_last_digit;
616     my @digits = split //, $barcode;
617     my $total = 0;
618     for (my $i = 1; $i < length($barcode); $i+=2) { # for a 13/14 digit barcode, would expect 1,3,5,7,9,11
619         $total += $digits[$i];
620     }
621     for (my $i = 0; $i < length($barcode); $i+=2) { # for a 13/14 digit barcode, would expect 0,2,4,6,8,10,12
622         $total += (2 * $digits[$i] >= 10) ? (2 * $digits[$i] - 9) : (2 * $digits[$i]);
623     }
624     my $remainder = $total % 10;
625     my $checkdigit = ($remainder == 0) ? $remainder : 10 - $remainder;
626     return $barcode . $checkdigit;
627 }
628
629 __PACKAGE__->register_method(
630     method        => "orgs_for_title",
631     authoritative => 1,
632     api_name      => "open-ils.cat.actor.org_unit.retrieve_by_title"
633 );
634
635 sub orgs_for_title {
636     my( $self, $client, $record_id ) = @_;
637
638     my $vols = $U->simple_scalar_request(
639         "open-ils.cstore",
640         "open-ils.cstore.direct.asset.call_number.search.atomic",
641         { record => $record_id, deleted => 'f' });
642
643     my $orgs = { map {$_->owning_lib => 1 } @$vols };
644     return [ keys %$orgs ];
645 }
646
647
648 __PACKAGE__->register_method(
649     method        => "retrieve_copies",
650     authoritative => 1,
651     api_name      => "open-ils.cat.asset.copy_tree.retrieve");
652
653 __PACKAGE__->register_method(
654     method   => "retrieve_copies",
655     api_name => "open-ils.cat.asset.copy_tree.global.retrieve");
656
657 # user_session may be null/undef
658 sub retrieve_copies {
659
660     my( $self, $client, $user_session, $docid, @org_ids ) = @_;
661
662     if(ref($org_ids[0])) { @org_ids = @{$org_ids[0]}; }
663
664     $docid = "$docid";
665
666     # grabbing copy trees should be available for everyone..
667     if(!@org_ids and $user_session) {
668         my($user_obj, $evt) = OpenILS::Application::AppUtils->checkses($user_session); 
669         return $evt if $evt;
670         @org_ids = ($user_obj->home_ou);
671     }
672
673     # Create an editor that can be shared across all iterations of 
674     # _build_volume_list().  Otherwise, .authoritative calls can result 
675     # in creating too many cstore connections.
676     my $e = new_editor();
677
678     if( $self->api_name =~ /global/ ) {
679         return _build_volume_list($e, { record => $docid, deleted => 'f', label => { '<>' => '##URI##' } } );
680
681     } else {
682
683         my @all_vols;
684         for my $orgid (@org_ids) {
685             my $vols = _build_volume_list($e,
686                     { record => $docid, owning_lib => $orgid, deleted => 'f', label => { '<>' => '##URI##' } } );
687             push( @all_vols, @$vols );
688         }
689         
690         return \@all_vols;
691     }
692
693     return undef;
694 }
695
696
697 sub _build_volume_list {
698     my $e = shift;
699     my $search_hash = shift;
700
701     $e ||= new_editor();
702
703     $search_hash->{deleted} = 'f';
704
705     my $vols = $e->search_asset_call_number([
706         $search_hash,
707         {
708             flesh => 1,
709             flesh_fields => { acn => ['prefix','suffix','label_class'] },
710             'order_by' => { 'acn' => 'oils_text_as_bytea(label_sortkey), oils_text_as_bytea(label), id, owning_lib' }
711         }
712     ]);
713
714     my @volumes;
715
716     for my $volume (@$vols) {
717
718         my $copies = $e->search_asset_copy([
719             { call_number => $volume->id , deleted => 'f' },
720             {
721                 join => {
722                     acpm => {
723                         type => 'left',
724                         join => {
725                             bmp => { type => 'left' }
726                         }
727                     }
728                 },
729                 flesh => 1,
730                 flesh_fields => { acp => ['stat_cat_entries','parts'] },
731                 order_by => [
732                     {'class' => 'bmp', 'field' => 'label_sortkey', 'transform' => 'oils_text_as_bytea'},
733                     {'class' => 'bmp', 'field' => 'label', 'transform' => 'oils_text_as_bytea'},
734                     {'class' => 'acp', 'field' => 'barcode'}
735                 ]
736             }
737         ]);
738
739         for my $c (@$copies) {
740             if( $c->status == OILS_COPY_STATUS_CHECKED_OUT ) {
741                 $c->circulations(
742                     $e->search_action_circulation(
743                         [
744                             { target_copy => $c->id },
745                             {
746                                 order_by => { circ => 'xact_start desc' },
747                                 limit => 1
748                             }
749                         ]
750                     )
751                 )
752             }
753         }
754
755         $volume->copies($copies);
756         push( @volumes, $volume );
757     }
758
759     #$session->disconnect();
760     return \@volumes;
761
762 }
763
764
765 __PACKAGE__->register_method(
766     method   => "fleshed_copy_update",
767     api_name => "open-ils.cat.asset.copy.fleshed.batch.update",);
768
769 __PACKAGE__->register_method(
770     method   => "fleshed_copy_update",
771     api_name => "open-ils.cat.asset.copy.fleshed.batch.update.override",);
772
773
774 sub fleshed_copy_update {
775     my( $self, $conn, $auth, $copies, $delete_stats, $oargs, $create_parts ) = @_;
776     return 1 unless ref $copies;
777     my( $reqr, $evt ) = $U->checkses($auth);
778     return $evt if $evt;
779     my $editor = new_editor(requestor => $reqr, xact => 1);
780     if ($self->api_name =~ /override/) {
781         $oargs = { all => 1 } unless defined $oargs;
782     } else {
783         $oargs = {};
784     }
785     my $retarget_holds = [];
786     $evt = OpenILS::Application::Cat::AssetCommon->update_fleshed_copies(
787         $editor, $oargs, undef, $copies, $delete_stats, $retarget_holds, undef, $create_parts);
788
789     if( $evt ) { 
790         $logger->info("fleshed copy update failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
791         $editor->rollback; 
792         return $evt; 
793     }
794
795     $editor->commit;
796     $logger->info("fleshed copy update successfully updated ".scalar(@$copies)." copies");
797     reset_hold_list($auth, $retarget_holds);
798
799     return 1;
800 }
801
802 sub reset_hold_list {
803     my($auth, $hold_ids) = @_;
804     return unless @$hold_ids;
805     $logger->info("reseting holds after copy status change: @$hold_ids");
806     my $ses = OpenSRF::AppSession->create('open-ils.circ');
807     $ses->request('open-ils.circ.hold.reset.batch', $auth, $hold_ids);
808 }
809
810 __PACKAGE__->register_method(
811     method    => "transfer_copies_to_volume",
812     api_name  => "open-ils.cat.transfer_copies_to_volume",
813     argc      => 3,
814     signature => {
815         desc   => 'Transfers specified copies to the specified call number, and changes Circ Lib to match the new Owning Lib.',
816         params => [
817             {desc => 'Authtoken', type => 'string'},
818             {desc => 'Call Number ID', type => 'number'},
819             {desc => 'Array of Copy IDs', type => 'array'},
820         ]
821     },
822     return => {desc => '1 on success, Event on error'}
823 );
824
825 __PACKAGE__->register_method(
826     method   => "transfer_copies_to_volume",
827     api_name => "open-ils.cat.transfer_copies_to_volume.override",);
828
829 sub transfer_copies_to_volume {
830     my( $self, $conn, $auth, $volume, $copies, $oargs ) = @_;
831     my $delete_stats = 1;
832     my $force_delete_empty_bib = undef;
833     my $create_parts = undef;
834
835     # initial tests
836
837     return 1 unless ref $copies;
838     my( $reqr, $evt ) = $U->checkses($auth);
839     return $evt if $evt;
840     my $editor = new_editor(requestor => $reqr, xact => 1);
841     if ($self->api_name =~ /override/) {
842         $oargs = { all => 1 } unless defined $oargs;
843     } else {
844         $oargs = {};
845     }
846
847     # does the volume exist?  good, we also need its owning_lib later
848     my( $cn, $cn_evt ) = $U->fetch_callnumber( $volume, 0, $editor );
849     return $cn_evt if $cn_evt;
850
851     # flesh and munge the copies
852     my $fleshed_copies = [];
853     my ($copy, $copy_evt);
854     foreach my $copy_id ( @{ $copies } ) {
855         ($copy, $copy_evt) = $U->fetch_copy($copy_id);
856         return $copy_evt if $copy_evt;
857         $copy->call_number( $volume );
858         $copy->circ_lib( $cn->owning_lib() );
859         $copy->ischanged( 't' );
860         push @$fleshed_copies, $copy;
861     }
862
863     # actual work
864     my $retarget_holds = [];
865     $evt = OpenILS::Application::Cat::AssetCommon->update_fleshed_copies(
866         $editor, $oargs, undef, $fleshed_copies, $delete_stats, $retarget_holds, $force_delete_empty_bib, $create_parts);
867
868     if( $evt ) { 
869         $logger->info("copy to volume transfer failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
870         $editor->rollback; 
871         return $evt; 
872     }
873
874     $editor->commit;
875     $logger->info("copy to volume transfer successfully updated ".scalar(@$copies)." copies");
876     reset_hold_list($auth, $retarget_holds);
877
878     return 1;
879 }
880
881 __PACKAGE__->register_method(
882     method    => 'in_db_merge',
883     api_name  => 'open-ils.cat.biblio.records.merge',
884     signature => q/
885         Merges a group of records
886         @param auth The login session key
887         @param master The id of the record all other records should be merged into
888         @param records Array of records to be merged into the master record
889         @return 1 on success, Event on error.
890     /
891 );
892
893 sub in_db_merge {
894     my( $self, $conn, $auth, $master, $records ) = @_;
895
896     my $editor = new_editor( authtoken => $auth, xact => 1 );
897     return $editor->die_event unless $editor->checkauth;
898     return $editor->die_event unless $editor->allowed('MERGE_BIB_RECORDS'); # TODO see below about record ownership
899
900     my $count = 0;
901     for my $source ( @$records ) {
902         #XXX we actually /will/ want to check perms for master and sources after record ownership exists
903
904         # This stored proc (asset.merge_record_assets(target,source)) has the side effects of
905         # moving call_number, title-type (and some volume-type) hold_request and uri-mapping
906         # objects from the source record to the target record, so must be called from within
907         # a transaction.
908
909         $count += $editor->json_query({
910             select => {
911                 bre => [{
912                     alias => 'count',
913                     transform => 'asset.merge_record_assets',
914                     column => 'id',
915                     params => [$source]
916                 }]
917             },
918             from   => 'bre',
919             where  => { id => $master }
920         })->[0]->{count}; # count of objects moved, of all types
921
922     }
923
924     $editor->commit;
925     return $count;
926 }
927
928 __PACKAGE__->register_method(
929     method    => 'in_db_auth_merge',
930     api_name  => 'open-ils.cat.authority.records.merge',
931     signature => q/
932         Merges a group of authority records
933         @param auth The login session key
934         @param master The id of the record all other records should be merged into
935         @param records Array of records to be merged into the master record
936         @return 1 on success, Event on error.
937     /
938 );
939
940 sub in_db_auth_merge {
941     my( $self, $conn, $auth, $master, $records ) = @_;
942
943     my $editor = new_editor( authtoken => $auth, xact => 1 );
944     return $editor->die_event unless $editor->checkauth;
945     return $editor->die_event unless $editor->allowed('MERGE_AUTH_RECORDS'); # TODO see below about record ownership
946
947     my $count = 0;
948     for my $source ( @$records ) {
949         $count += $editor->json_query({
950             select => {
951                 are => [{
952                     alias => 'count',
953                     transform => 'authority.merge_records',
954                     column => 'id',
955                     params => [$source]
956                 }]
957             },
958             from   => 'are',
959             where  => { id => $master }
960         })->[0]->{count}; # count of objects moved, of all types
961     }
962
963     $editor->commit;
964     return $count;
965 }
966
967 __PACKAGE__->register_method(
968     method    => 'calculate_marc_merge',
969     api_name  => 'open-ils.cat.merge.marc.per_profile',
970     signature => q/
971         Calculate the result of merging one or more MARC records
972         per the specified merge profile
973         @param auth The login session key
974         @param merge_profile ID of the record merge profile
975         @param records Array of two or more MARCXML records to be
976                        merged. If two are supplied, the first
977                        is treated as the record to be overlaid,
978                        and the the incoming record that will
979                        overlay the first. If more than two are
980                        supplied, the first is treated as the
981                        record to be overlaid, and each following
982                        record in turn will be merged into that
983                        record.
984         @return MARCXML string of the results of the merge
985     /
986 );
987 __PACKAGE__->register_method(
988     method    => 'calculate_bib_marc_merge',
989     api_name  => 'open-ils.cat.merge.biblio.per_profile',
990     signature => q/
991         Calculate the result of merging one or more bib records
992         per the specified merge profile
993         @param auth The login session key
994         @param merge_profile ID of the record merge profile
995         @param records Array of two or more bib record IDs of
996                        the bibs to be merged.
997         @return MARCXML string of the results of the merge
998     /
999 );
1000 __PACKAGE__->register_method(
1001     method    => 'calculate_authority_marc_merge',
1002     api_name  => 'open-ils.cat.merge.authority.per_profile',
1003     signature => q/
1004         Calculate the result of merging one or more authority records
1005         per the specified merge profile
1006         @param auth The login session key
1007         @param merge_profile ID of the record merge profile
1008         @param records Array of two or more bib record IDs of
1009                        the bibs to be merged.
1010         @return MARCXML string of the results of the merge
1011     /
1012 );
1013
1014 sub _handle_marc_merge {
1015     my ($e, $merge_profile_id, $records) = @_;
1016
1017     my $result = shift @$records;
1018     foreach my $incoming (@$records) {
1019         my $response = $e->json_query({
1020             from => [
1021                 'vandelay.merge_record_xml_using_profile',
1022                 $incoming, $result,
1023                 $merge_profile_id
1024             ]
1025         });
1026         return unless ref($response);
1027         $result = $response->[0]->{'vandelay.merge_record_xml_using_profile'};
1028     }
1029     return $result;
1030 }
1031
1032 sub calculate_marc_merge {
1033     my( $self, $conn, $auth, $merge_profile_id, $records ) = @_;
1034
1035     my $e = new_editor(authtoken=>$auth, xact=>1);
1036     return $e->die_event unless $e->checkauth;
1037
1038     my $merge_profile = $e->retrieve_vandelay_merge_profile($merge_profile_id)
1039         or return $e->die_event;
1040     return $e->die_event unless ref($records) && @$records >= 2;
1041
1042     return _handle_marc_merge($e, $merge_profile_id, $records)
1043 }
1044
1045 sub calculate_bib_marc_merge {
1046     my( $self, $conn, $auth, $merge_profile_id, $bib_ids ) = @_;
1047
1048     my $e = new_editor(authtoken=>$auth, xact=>1);
1049     return $e->die_event unless $e->checkauth;
1050
1051     my $merge_profile = $e->retrieve_vandelay_merge_profile($merge_profile_id)
1052         or return $e->die_event;
1053     return $e->die_event unless ref($bib_ids) && @$bib_ids >= 2;
1054
1055     my $records = [];
1056     foreach my $id (@$bib_ids) {
1057         my $bre = $e->retrieve_biblio_record_entry($id) or return $e->die_event;
1058         push @$records, $bre->marc();
1059     }
1060
1061     return _handle_marc_merge($e, $merge_profile_id, $records)
1062 }
1063
1064 sub calculate_authority_marc_merge {
1065     my( $self, $conn, $auth, $merge_profile_id, $authority_ids ) = @_;
1066
1067     my $e = new_editor(authtoken=>$auth, xact=>1);
1068     return $e->die_event unless $e->checkauth;
1069
1070     my $merge_profile = $e->retrieve_vandelay_merge_profile($merge_profile_id)
1071         or return $e->die_event;
1072     return $e->die_event unless ref($authority_ids) && @$authority_ids >= 2;
1073
1074     my $records = [];
1075     foreach my $id (@$authority_ids) {
1076         my $are = $e->retrieve_authority_record_entry($id) or return $e->die_event;
1077         push @$records, $are->marc();
1078     }
1079
1080     return _handle_marc_merge($e, $merge_profile_id, $records)
1081 }
1082
1083 __PACKAGE__->register_method(
1084     method   => "fleshed_volume_update",
1085     api_name => "open-ils.cat.asset.volume.fleshed.batch.update",);
1086
1087 __PACKAGE__->register_method(
1088     method   => "fleshed_volume_update",
1089     api_name => "open-ils.cat.asset.volume.fleshed.batch.update.override",);
1090
1091 sub fleshed_volume_update {
1092     my( $self, $conn, $auth, $volumes, $delete_stats, $options, $oargs ) = @_;
1093     my( $reqr, $evt ) = $U->checkses($auth);
1094     return $evt if $evt;
1095     $options ||= {};
1096
1097     if ($self->api_name =~ /override/) {
1098         $oargs = { all => 1 } unless defined $oargs;
1099     } else {
1100         $oargs = {};
1101     }
1102     my $editor = new_editor( requestor => $reqr, xact => 1 );
1103     my $retarget_holds = [];
1104     my $auto_merge_vols = $options->{auto_merge_vols};
1105     my $create_parts = $options->{create_parts};
1106     my $copy_ids = [];
1107
1108     for my $vol (@$volumes) {
1109         $logger->info("vol-update: investigating volume ".$vol->id);
1110
1111         $vol->editor($reqr->id);
1112         $vol->edit_date('now');
1113
1114         my $copies = $vol->copies;
1115         $vol->clear_copies;
1116
1117         $vol->editor($editor->requestor->id);
1118         $vol->edit_date('now');
1119
1120         if( $vol->isdeleted ) {
1121
1122             $logger->info("vol-update: deleting volume");
1123             return $editor->die_event unless
1124                 $editor->allowed('UPDATE_VOLUME', $vol->owning_lib);
1125
1126             if(my $evt = $assetcom->delete_volume($editor, $vol, $oargs, $$options{force_delete_copies})) {
1127                 $editor->rollback;
1128                 return $evt;
1129             }
1130
1131             return $editor->die_event unless
1132                 $editor->update_asset_call_number($vol);
1133
1134         } elsif( $vol->isnew ) {
1135             $logger->info("vol-update: creating volume");
1136             ($vol,$evt) = $assetcom->create_volume( $auto_merge_vols ? { all => 1} : $oargs, $editor, $vol );
1137             return $evt if $evt;
1138
1139         } elsif( $vol->ischanged ) {
1140             $logger->info("vol-update: update volume");
1141             my $resp = update_volume($vol, $editor, ($oargs->{all} or grep { $_ eq 'VOLUME_LABEL_EXISTS' } @{$oargs->{events}} or $auto_merge_vols));
1142             return $resp->{evt} if $resp->{evt};
1143             $vol = $resp->{merge_vol} if $resp->{merge_vol};
1144         }
1145
1146         # now update any attached copies
1147         if( $copies and @$copies and !$vol->isdeleted ) {
1148             $_->call_number($vol->id) for @$copies;
1149             $evt = $assetcom->update_fleshed_copies(
1150                 $editor, $oargs, $vol, $copies, $delete_stats, $retarget_holds, undef, $create_parts);
1151             return $evt if $evt;
1152             push( @$copy_ids, $_->id ) for @$copies;
1153         }
1154     }
1155
1156     $editor->finish;
1157     reset_hold_list($auth, $retarget_holds);
1158     if ($options->{return_copy_ids}) {
1159         return $copy_ids;
1160     } else {
1161         return scalar(@$volumes);
1162     }
1163 }
1164
1165
1166 sub update_volume {
1167     my $vol = shift;
1168     my $editor = shift;
1169     my $auto_merge = shift;
1170     my $evt;
1171     my $merge_vol;
1172
1173     return {evt => $editor->event} unless
1174         $editor->allowed('UPDATE_VOLUME', $vol->owning_lib);
1175
1176     return {evt => $evt} 
1177         if ( $evt = OpenILS::Application::Cat::AssetCommon->org_cannot_have_vols($editor, $vol->owning_lib) );
1178
1179     my $vols = $editor->search_asset_call_number({ 
1180         owning_lib => $vol->owning_lib,
1181         record     => $vol->record,
1182         label      => $vol->label,
1183         prefix     => $vol->prefix,
1184         suffix     => $vol->suffix,
1185         deleted    => 'f',
1186         id         => {'!=' => $vol->id}
1187     });
1188
1189     if(@$vols) {
1190
1191         if($auto_merge) {
1192
1193             # If the auto-merge option is on, merge our updated volume into the existing
1194             # volume with the same record + owner + label.
1195             ($merge_vol, $evt) = OpenILS::Application::Cat::Merge::merge_volumes($editor, [$vol], $vols->[0]);
1196             return {evt => $evt, merge_vol => $merge_vol};
1197
1198         } else {
1199             return {evt => OpenILS::Event->new('VOLUME_LABEL_EXISTS', payload => $vol->id)};
1200         }
1201     }
1202
1203     return {evt => $editor->die_event} unless $editor->update_asset_call_number($vol);
1204     return {};
1205 }
1206
1207
1208
1209 __PACKAGE__->register_method (
1210     method   => 'delete_bib_record',
1211     api_name => 'open-ils.cat.biblio.record_entry.delete');
1212
1213 sub delete_bib_record {
1214     my($self, $conn, $auth, $rec_id) = @_;
1215     my $e = new_editor(xact=>1, authtoken=>$auth);
1216     return $e->die_event unless $e->checkauth;
1217     return $e->die_event unless $e->allowed('DELETE_RECORD', $e->requestor->ws_ou);
1218     my $vols = $e->search_asset_call_number({record=>$rec_id, deleted=>'f'});
1219     return OpenILS::Event->new('RECORD_NOT_EMPTY', payload=>$rec_id) if @$vols;
1220     my $evt = OpenILS::Application::Cat::BibCommon->delete_rec($e, $rec_id);
1221     if($evt) { $e->rollback; return $evt; }   
1222     $e->commit;
1223     return 1;
1224 }
1225
1226
1227
1228 __PACKAGE__->register_method (
1229     method   => 'batch_volume_transfer',
1230     api_name => 'open-ils.cat.asset.volume.batch.transfer',
1231 );
1232
1233 __PACKAGE__->register_method (
1234     method   => 'batch_volume_transfer',
1235     api_name => 'open-ils.cat.asset.volume.batch.transfer.override',
1236 );
1237
1238
1239 sub batch_volume_transfer {
1240     my( $self, $conn, $auth, $args, $oargs ) = @_;
1241
1242     my $evt;
1243     my $rec     = $$args{docid};
1244     my $o_lib   = $$args{lib};
1245     my $vol_ids = $$args{volumes};
1246
1247     my $override = 1 if $self->api_name =~ /override/;
1248     $oargs = { all => 1 } unless defined $oargs;
1249
1250     $logger->info("merge: transferring volumes to lib=$o_lib and record=$rec");
1251
1252     my $e = new_editor(authtoken => $auth, xact =>1);
1253     return $e->event unless $e->checkauth;
1254     return $e->event unless $e->allowed('UPDATE_VOLUME', $o_lib);
1255
1256     my $dorg = $e->retrieve_actor_org_unit($o_lib)
1257         or return $e->event;
1258
1259     my $ou_type = $e->retrieve_actor_org_unit_type($dorg->ou_type)
1260         or return $e->event;
1261
1262     return $evt if ( $evt = OpenILS::Application::Cat::AssetCommon->org_cannot_have_vols($e, $o_lib) );
1263
1264     my $vols = $e->batch_retrieve_asset_call_number($vol_ids);
1265     my @seen;
1266
1267    my @rec_ids;
1268
1269     for my $vol (@$vols) {
1270
1271         # if we've already looked at this volume, go to the next
1272         next if !$vol or grep { $vol->id == $_ } @seen;
1273
1274         # grab all of the volumes in the list that have 
1275         # the same label so they can be merged
1276         my @all = grep { $_->label eq $vol->label } @$vols;
1277
1278         # take note of the fact that we've looked at this set of volumes
1279         push( @seen, $_->id ) for @all;
1280         push( @rec_ids, $_->record ) for @all;
1281
1282         # for each volume, see if there are any copies that have a 
1283         # remote circ_lib (circ_lib != vol->owning_lib and != $o_lib ).  
1284         # if so, warn them
1285         unless( $override && ($oargs->{all} || grep { $_ eq 'COPY_REMOTE_CIRC_LIB' } @{$oargs->{events}}) ) {
1286             for my $v (@all) {
1287
1288                 $logger->debug("merge: searching for copies with remote circ_lib for volume ".$v->id);
1289                 my $args = { 
1290                     call_number => $v->id, 
1291                     circ_lib    => { "not in" => [ $o_lib, $v->owning_lib ] },
1292                     deleted     => 'f'
1293                 };
1294
1295                 my $copies = $e->search_asset_copy($args, {idlist=>1});
1296
1297                 # if the copy's circ_lib matches the destination lib,
1298                 # that's ok too
1299                 return OpenILS::Event->new('COPY_REMOTE_CIRC_LIB') if @$copies;
1300             }
1301         }
1302
1303         # see if there is a volume at the destination lib that 
1304         # already has the requested label
1305         my $existing_vol = $e->search_asset_call_number(
1306             {
1307                 label      => $vol->label, 
1308                 prefix     => $vol->prefix, 
1309                 suffix     => $vol->suffix, 
1310                 record     => $rec, 
1311                 owning_lib => $o_lib,
1312                 deleted    => 'f'
1313             }
1314         )->[0];
1315
1316         if( $existing_vol ) {
1317
1318             if( grep { $_->id == $existing_vol->id } @all ) {
1319                 # this volume is already accounted for in our list of volumes to merge
1320                 $existing_vol = undef;
1321
1322             } else {
1323                 # this volume exists on the destination record/owning_lib and must
1324                 # be used as the destination for merging
1325                 $logger->debug("merge: volume already exists at destination record: ".
1326                     $existing_vol->id.' : '.$existing_vol->label) if $existing_vol;
1327             }
1328         } 
1329
1330         if( @all > 1 || $existing_vol ) {
1331             $logger->info("merge: found collisions in volume transfer");
1332             my @args = ($e, \@all);
1333             @args = ($e, \@all, $existing_vol) if $existing_vol;
1334             ($vol, $evt) = OpenILS::Application::Cat::Merge::merge_volumes(@args);
1335             return $evt if $evt;
1336         } 
1337         
1338         if( !$existing_vol ) {
1339
1340             $vol->owning_lib($o_lib);
1341             $vol->record($rec);
1342             $vol->editor($e->requestor->id);
1343             $vol->edit_date('now');
1344     
1345             $logger->info("merge: updating volume ".$vol->id);
1346             $e->update_asset_call_number($vol) or return $e->event;
1347
1348         } else {
1349             $logger->info("merge: bypassing volume update because existing volume used as target");
1350         }
1351
1352         # regardless of what volume was used as the destination, 
1353         # update any copies that have moved over to the new lib
1354         my $copies = $e->search_asset_copy({call_number=>$vol->id, deleted => 'f'});
1355
1356         # update circ lib on the copies - make this a method flag?
1357         for my $copy (@$copies) {
1358             next if $copy->circ_lib == $o_lib;
1359             $logger->info("merge: transfer moving circ lib on copy ".$copy->id);
1360             $copy->circ_lib($o_lib);
1361             $copy->editor($e->requestor->id);
1362             $copy->edit_date('now');
1363             $e->update_asset_copy($copy) or return $e->event;
1364         }
1365
1366         # Now see if any empty records need to be deleted after all of this
1367
1368         for(@rec_ids) {
1369             $logger->debug("merge: seeing if we should delete record $_...");
1370             $evt = OpenILS::Application::Cat::BibCommon->delete_rec($e, $_) 
1371                 if OpenILS::Application::Cat::BibCommon->title_is_empty($e, $_);
1372             return $evt if $evt;
1373         }
1374     }
1375
1376     $logger->info("merge: transfer succeeded");
1377     $e->commit;
1378     return 1;
1379 }
1380
1381
1382
1383
1384 __PACKAGE__->register_method(
1385     api_name => 'open-ils.cat.call_number.find_or_create',
1386     method   => 'find_or_create_volume',
1387 );
1388
1389 sub find_or_create_volume {
1390     my( $self, $conn, $auth, $label, $record_id, $org_id, $prefix, $suffix, $label_class ) = @_;
1391     my $e = new_editor(authtoken=>$auth, xact=>1);
1392     return $e->die_event unless $e->checkauth;
1393     my ($vol, $evt, $exists) = 
1394         OpenILS::Application::Cat::AssetCommon->find_or_create_volume($e, $label, $record_id, $org_id, $prefix, $suffix, $label_class);
1395     return $evt if $evt;
1396     $e->rollback if $exists;
1397     $e->commit if $vol;
1398     return { 'acn_id' => $vol->id, 'existed' => $exists };
1399 }
1400
1401
1402 __PACKAGE__->register_method(
1403     method    => "create_serial_record_xml",
1404     api_name  => "open-ils.cat.serial.record.xml.create.override",
1405     signature => q/@see open-ils.cat.serial.record.xml.create/);
1406
1407 __PACKAGE__->register_method(
1408     method    => "create_serial_record_xml",
1409     api_name  => "open-ils.cat.serial.record.xml.create",
1410     signature => q/
1411         Inserts a new serial record with the given XML
1412     /
1413 );
1414
1415 sub create_serial_record_xml {
1416     my( $self, $client, $login, $source, $owning_lib, $record_id, $xml, $oargs ) = @_;
1417
1418     my $override = 1 if $self->api_name =~ /override/; # not currently used
1419     $oargs = { all => 1 } unless defined $oargs; # Not currently used, but here for consistency.
1420
1421     my $e = new_editor(xact=>1, authtoken=>$login);
1422     return $e->die_event unless $e->checkauth;
1423     return $e->die_event unless $e->allowed('CREATE_MFHD_RECORD', $owning_lib);
1424
1425     # Auto-populate the location field of a placeholder MFHD record with the library name
1426     my $aou = $e->retrieve_actor_org_unit($owning_lib) or return $e->die_event;
1427
1428     my $mfhd = Fieldmapper::serial::record_entry->new;
1429
1430     $mfhd->source($source) if $source;
1431     $mfhd->record($record_id);
1432     $mfhd->creator($e->requestor->id);
1433     $mfhd->editor($e->requestor->id);
1434     $mfhd->create_date('now');
1435     $mfhd->edit_date('now');
1436     $mfhd->owning_lib($owning_lib);
1437
1438     # If the caller did not pass in MFHD XML, create a placeholder record.
1439     # The placeholder will only contain the name of the owning library.
1440     # The goal is to generate common patterns for the caller in the UI that
1441     # then get passed in here.
1442     if (!$xml) {
1443         my $aou_name = $aou->name;
1444         $xml = <<HERE;
1445 <record 
1446  xsi:schemaLocation="http://www.loc.gov/MARC21/slim http://www.loc.gov/standards/marcxml/schema/MARC21slim.xsd"
1447  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
1448  xmlns="http://www.loc.gov/MARC21/slim">
1449 <leader>00307ny  a22001094  4500</leader>
1450 <controlfield tag="001">42153</controlfield>
1451 <controlfield tag="005">20090601182414.0</controlfield>
1452 <controlfield tag="004">$record_id</controlfield>
1453 <controlfield tag="008">      4u####8###l# 4   uueng1      </controlfield>
1454 <datafield tag="852" ind1=" " ind2=" "> <subfield code="b">$aou_name</subfield></datafield>
1455 </record>
1456 HERE
1457     }
1458     my $marcxml = XML::LibXML->new->parse_string($xml);
1459     $marcxml->documentElement->setNamespace("http://www.loc.gov/MARC21/slim", "marc", 1 );
1460     $marcxml->documentElement->setNamespace("http://www.loc.gov/MARC21/slim");
1461
1462     $mfhd->marc($U->entityize($marcxml->documentElement->toString));
1463
1464     $e->create_serial_record_entry($mfhd) or return $e->die_event;
1465
1466     $e->commit;
1467     return $mfhd->id;
1468 }
1469
1470 __PACKAGE__->register_method(
1471     method   => "create_update_asset_copy_template",
1472     api_name => "open-ils.cat.asset.copy_template.create_or_update"
1473 );
1474
1475 sub create_update_asset_copy_template {
1476     my ($self, $client, $authtoken, $act) = @_;
1477
1478     my $e = new_editor("xact" => 1, "authtoken" => $authtoken);
1479     return $e->die_event unless $e->checkauth;
1480     return $e->die_event unless $e->allowed(
1481         "ADMIN_ASSET_COPY_TEMPLATE", $act->owning_lib
1482     );
1483
1484     $act->editor($e->requestor->id);
1485     $act->edit_date("now");
1486
1487     my $retval;
1488     if (!$act->id) {
1489         $act->creator($e->requestor->id);
1490         $act->create_date("now");
1491
1492         $e->create_asset_copy_template($act) or return $e->die_event;
1493         $retval = $e->data;
1494     } else {
1495         $e->update_asset_copy_template($act) or return $e->die_event;
1496         $retval = $e->retrieve_asset_copy_template($e->data);
1497     }
1498     $e->commit and return $retval;
1499 }
1500
1501 __PACKAGE__->register_method(
1502     method      => "acn_sms_msg",
1503     api_name    => "open-ils.cat.acn.send_sms_text",
1504     signature   => q^
1505         Send an SMS text from an A/T template for specified call numbers.
1506
1507         First parameter is null or an auth token (whether a null is allowed
1508         depends on the sms.disable_authentication_requirement.callnumbers OU
1509         setting).
1510
1511         Second parameter is the id of the context org.
1512
1513         Third parameter is the code of the SMS carrier from the
1514         config.sms_carrier table.
1515
1516         Fourth parameter is the SMS number.
1517
1518         Fifth parameter is the ACN id's to target, though currently only the
1519         first ACN is used by the template (and the UI is only sending one).
1520     ^
1521 );
1522
1523 sub acn_sms_msg {
1524     my($self, $conn, $auth, $org_id, $carrier, $number, $target_ids) = @_;
1525
1526     my $sms_enable = $U->ou_ancestor_setting_value(
1527         $org_id || $U->get_org_tree->id,
1528         'sms.enable'
1529     );
1530     # We could maybe make a Validator for this on the templates
1531     if (! $U->is_true($sms_enable)) {
1532         return -1;
1533     }
1534
1535     my $disable_auth = $U->ou_ancestor_setting_value(
1536         $org_id || $U->get_org_tree->id,
1537         'sms.disable_authentication_requirement.callnumbers'
1538     );
1539
1540     my $e = new_editor(
1541         (defined $auth)
1542         ? (authtoken => $auth, xact => 1)
1543         : (xact => 1)
1544     );
1545     return $e->event unless $disable_auth || $e->checkauth;
1546
1547     my $targets = $e->batch_retrieve_asset_call_number($target_ids);
1548
1549     $e->rollback; # FIXME using transaction because of pgpool/slony setups, but not
1550                   # simply making this method authoritative because of weirdness
1551                   # with transaction handling in A/T code that causes rollback
1552                   # failure down the line if handling many targets
1553
1554     return undef unless @$targets;
1555     return $U->fire_object_event(
1556         undef,                    # event_def
1557         'acn.format.sms_text',    # hook
1558         $targets,
1559         $org_id,
1560         undef,                    # granularity
1561         {                         # user_data
1562             sms_carrier => $carrier,
1563             sms_notify => $number
1564         }
1565     );
1566 }
1567
1568
1569
1570 __PACKAGE__->register_method(
1571     method    => "fixed_field_values_by_rec_type",
1572     api_name  => "open-ils.cat.biblio.fixed_field_values.by_rec_type",
1573     argc      => 2,
1574     signature => {
1575         desc   => 'Given a record type (as in cmfpm.rec_type), return fixed fields and their possible values as known to the DB',
1576         params => [
1577             {desc => 'Record Type', type => 'string'},
1578             {desc => '(Optional) Fixed field', type => 'string'},
1579         ]
1580     },
1581     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' }
1582 );
1583
1584
1585 sub fixed_field_values_by_rec_type {
1586     my ($self, $conn, $rec_type, $fixed_field) = @_;
1587
1588     my $e = new_editor;
1589     my $values = $e->json_query({
1590         select => {
1591             crad  => ["fixed_field"],
1592             ccvm  => [qw/code value/],
1593             cmfpm => [qw/length default_val/],
1594         },
1595         distinct => 1,
1596         from => {
1597             ccvm => {
1598                 crad => {
1599                     join => {
1600                         cmfpm => {
1601                             fkey => "fixed_field",
1602                             field => "fixed_field"
1603                         }
1604                     }
1605                 }
1606             }
1607         },
1608         where => {
1609             "+cmfpm" => {rec_type => $rec_type},
1610             defined $fixed_field ?
1611                 ("+crad" => {fixed_field => $fixed_field}) : ()
1612         },
1613         order_by => [
1614             {class => "crad", field => "fixed_field"},
1615             {class => "ccvm", field => "code"}
1616         ]
1617     }) or return $e->die_event;
1618
1619     my $result = {};
1620     for my $row (@$values) {
1621         $result->{$row->{fixed_field}} ||= [];
1622         push @{$result->{$row->{fixed_field}}}, [@$row{qw/code value length default_val/}];
1623     }
1624
1625     return $result;
1626 }
1627
1628 __PACKAGE__->register_method(
1629     method    => "retrieve_tag_table",
1630     api_name  => "open-ils.cat.tag_table.all.retrieve.local",
1631     stream    => 1,
1632     argc      => 3,
1633     signature => {
1634         desc   => "Retrieve set of MARC tags, subfields, and indicator values for the user's OU",
1635         params => [
1636             {desc => 'Authtoken', type => 'string'},
1637             {desc => 'MARC Format', type => 'string'},
1638             {desc => 'MARC Record Type', type => 'string'},
1639         ]
1640     },
1641     return => {desc => 'Structure representing the tag table available to that user', type => 'object' }
1642 );
1643 __PACKAGE__->register_method(
1644     method    => "retrieve_tag_table",
1645     api_name  => "open-ils.cat.tag_table.all.retrieve.stock",
1646     stream    => 1,
1647     argc      => 3,
1648     signature => {
1649         desc   => 'Retrieve set of MARC tags, subfields, and indicator values for stock MARC standard',
1650         params => [
1651             {desc => 'Authtoken', type => 'string'},
1652             {desc => 'MARC Format', type => 'string'},
1653             {desc => 'MARC Record Type', type => 'string'},
1654         ]
1655     },
1656     return => {desc => 'Structure representing the stock tag table', type => 'object' }
1657 );
1658 __PACKAGE__->register_method(
1659     method    => "retrieve_tag_table",
1660     api_name  => "open-ils.cat.tag_table.field_list.retrieve.local",
1661     stream    => 1,
1662     argc      => 3,
1663     signature => {
1664         desc   => "Retrieve set of MARC tags for available to the user's OU",
1665         params => [
1666             {desc => 'Authtoken', type => 'string'},
1667             {desc => 'MARC Format', type => 'string'},
1668             {desc => 'MARC Record Type', type => 'string'},
1669         ]
1670     },
1671     return => {desc => 'Structure representing the tags available to that user', type => 'object' }
1672 );
1673 __PACKAGE__->register_method(
1674     method    => "retrieve_tag_table",
1675     api_name  => "open-ils.cat.tag_table.field_list.retrieve.stock",
1676     stream    => 1,
1677     argc      => 3,
1678     signature => {
1679         desc   => 'Retrieve set of MARC tags for stock MARC standard',
1680         params => [
1681             {desc => 'Authtoken', type => 'string'},
1682             {desc => 'MARC Format', type => 'string'},
1683             {desc => 'MARC Record Type', type => 'string'},
1684         ]
1685     },
1686     return => {desc => 'Structure representing the stock MARC tags', type => 'object' }
1687 );
1688
1689 sub retrieve_tag_table {
1690     my( $self, $conn, $auth, $marc_format, $marc_record_type ) = @_;
1691     my $e = new_editor( authtoken=>$auth, xact=>1 );
1692     return $e->die_event unless $e->checkauth;
1693
1694     my $field_list_only = ($self->api_name =~ /\.field_list\./) ? 1 : 0;
1695     my $context_ou;
1696     if ($self->api_name =~ /\.local$/) {
1697         $context_ou = $e->requestor->ws_ou;
1698     }
1699
1700     my %sf_by_tag;
1701     unless ($field_list_only) {
1702         my $subfields = $e->json_query(
1703             { from => [ 'config.ou_marc_subfields', 1, $marc_record_type, $context_ou ] }
1704         );
1705         foreach my $sf (@$subfields) {
1706             my $sf_data = {
1707                 code        => $sf->{code},
1708                 description => $sf->{description},
1709                 mandatory   => $sf->{mandatory},
1710                 repeatable   => $sf->{repeatable},
1711             };
1712             if ($sf->{value_ctype}) {
1713                 $sf_data->{value_list} = $e->json_query({
1714                     select => { ccvm => [
1715                                             'code',
1716                                             { column => 'value', alias => 'description' }
1717                                         ]
1718                               },
1719                     from   => 'ccvm',
1720                     where  => { ctype => $sf->{value_ctype} },
1721                     order_by => { ccvm => { code => {} } },
1722                 });
1723             }
1724             push @{ $sf_by_tag{$sf->{tag}} }, $sf_data;
1725         }
1726     }
1727
1728     my $fields = $e->json_query(
1729         { from => [ 'config.ou_marc_fields', 1, $marc_record_type, $context_ou ] }
1730     );
1731
1732     foreach my $field (@$fields) {
1733         next if $field->{hidden} eq 't';
1734         unless ($field_list_only) {
1735             my $tag = $field->{tag};
1736             if ($tag ge '010') {
1737                 for my $pos (1..2) {
1738                     my $ind_ccvm_key = "${marc_format}_${marc_record_type}_${tag}_ind_${pos}";
1739                     my $indvals = $e->json_query({
1740                         select => { ccvm => [
1741                                                 'code',
1742                                                 { column => 'value', alias => 'description' }
1743                                             ]
1744                                   },
1745                         from   => 'ccvm',
1746                         where  => { ctype => $ind_ccvm_key }
1747                     });
1748                     next unless defined($indvals);
1749                     $field->{"ind$pos"} = $indvals;
1750                 }
1751                 $field->{subfields} = exists($sf_by_tag{$tag}) ? $sf_by_tag{$tag} : [];
1752             }
1753         }
1754         $conn->respond($field);
1755     }
1756 }
1757
1758 1;
1759
1760 # vi:et:ts=4:sw=4