]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Cat.pm
moved marc edit/import functionaly out to an external module so it can be imported...
[Evergreen.git] / Open-ILS / src / perlmods / 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 base qw/OpenILS::Application/;
9 use Time::HiRes qw(time);
10 use OpenSRF::EX qw(:try);
11 use OpenSRF::Utils::JSON;
12 use OpenILS::Utils::Fieldmapper;
13 use OpenILS::Event;
14 use OpenILS::Const qw/:const/;
15
16 use XML::LibXML;
17 use Unicode::Normalize;
18 use Data::Dumper;
19 use OpenILS::Utils::FlatXML;
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
30 __PACKAGE__->register_method(
31         method  => "retrieve_marc_template",
32         api_name        => "open-ils.cat.biblio.marc_template.retrieve",
33         notes           => <<"  NOTES");
34         Returns a MARC 'record tree' based on a set of pre-defined templates.
35         Templates include : book
36         NOTES
37
38 sub retrieve_marc_template {
39         my( $self, $client, $type ) = @_;
40         return $marctemplates{$type} if defined($marctemplates{$type});
41         $marctemplates{$type} = _load_marc_template($type);
42         return $marctemplates{$type};
43 }
44
45 __PACKAGE__->register_method(
46         method => 'fetch_marc_template_types',
47         api_name => 'open-ils.cat.marc_template.types.retrieve'
48 );
49
50 my $marc_template_files;
51
52 sub fetch_marc_template_types {
53         my( $self, $conn ) = @_;
54         __load_marc_templates();
55         return [ keys %$marc_template_files ];
56 }
57
58 sub __load_marc_templates {
59         return if $marc_template_files;
60         if(!$conf) { $conf = OpenSRF::Utils::SettingsClient->new; }
61
62         $marc_template_files = $conf->config_value(                                     
63                 "apps", "open-ils.cat","app_settings", "marctemplates" );
64
65         $logger->info("Loaded marc templates: " . Dumper($marc_template_files));
66 }
67
68 sub _load_marc_template {
69         my $type = shift;
70
71         __load_marc_templates();
72
73         my $template = $$marc_template_files{$type};
74         open( F, $template ) or 
75                 throw OpenSRF::EX::ERROR ("Unable to open MARC template file: $template : $@");
76
77         my @xml = <F>;
78         close(F);
79         my $xml = join('', @xml);
80
81         return XML::LibXML->new->parse_string($xml)->documentElement->toString;
82 }
83
84 my $__bib_sources;
85 sub bib_source_from_name {
86         my $name = shift;
87         $logger->debug("searching for bib source: $name");
88
89         fetch_bib_sources();
90
91         my ($s) = grep { lc($_->source) eq lc($name) } @$__bib_sources;
92
93         return $s->id if $s;
94         return undef;
95 }
96
97
98 __PACKAGE__->register_method(
99         method => 'fetch_bib_sources',
100         api_name => 'open-ils.cat.bib_sources.retrieve.all');
101
102 sub fetch_bib_sources {
103         $__bib_sources = new_editor()->retrieve_all_config_bib_source()
104                 unless $__bib_sources;
105         return $__bib_sources;
106 }
107
108
109
110 __PACKAGE__->register_method(
111         method  => "create_record_xml",
112         api_name        => "open-ils.cat.biblio.record.xml.create.override",
113         signature       => q/@see open-ils.cat.biblio.record.xml.create/);
114
115 __PACKAGE__->register_method(
116         method          => "create_record_xml",
117         api_name                => "open-ils.cat.biblio.record.xml.create",
118         signature       => q/
119                 Inserts a new biblio with the given XML
120         /
121 );
122
123 sub create_record_xml {
124         my( $self, $client, $login, $xml, $source ) = @_;
125
126         my $override = 1 if $self->api_name =~ /override/;
127
128         my( $user_obj, $evt ) = $U->checksesperm($login, 'CREATE_MARC');
129         return $evt if $evt;
130
131         $logger->activity("user ".$user_obj->id." creating new MARC record");
132
133         my $meth = $self->method_lookup("open-ils.cat.biblio.record.xml.import");
134
135         $meth = $self->method_lookup(
136                 "open-ils.cat.biblio.record.xml.import.override") if $override;
137
138         my ($s) = $meth->run($login, $xml, $source);
139         return $s;
140 }
141
142
143
144 __PACKAGE__->register_method(
145         method  => "biblio_record_replace_marc",
146         api_name        => "open-ils.cat.biblio.record.xml.update",
147         argc            => 3, 
148         signature       => q/
149                 Updates the XML for a given biblio record.
150                 This does not change any other aspect of the record entry
151                 exception the XML, the editor, and the edit date.
152                 @return The update record object
153         /
154 );
155
156 __PACKAGE__->register_method(
157         method          => 'biblio_record_replace_marc',
158         api_name                => 'open-ils.cat.biblio.record.marc.replace',
159         signature       => q/
160                 @param auth The authtoken
161                 @param recid The record whose MARC we're replacing
162                 @param newxml The new xml to use
163         /
164 );
165
166 __PACKAGE__->register_method(
167         method          => 'biblio_record_replace_marc',
168         api_name                => 'open-ils.cat.biblio.record.marc.replace.override',
169         signature       => q/@see open-ils.cat.biblio.record.marc.replace/
170 );
171
172 sub biblio_record_replace_marc  {
173         my( $self, $conn, $auth, $recid, $newxml, $source ) = @_;
174         my $e = new_editor(authtoken=>$auth, xact=>1);
175         return $e->die_event unless $e->checkauth;
176         return $e->die_event unless $e->allowed('CREATE_MARC', $e->requestor->ws_ou);
177
178     my $res = OpenILS::Application::Cat::BibCommon->biblio_record_replace_marc(
179         $e, $recid, $newxml, $source, 
180         $self->api_name =~ /replace/o,
181         $self->api_name =~ /override/o);
182
183     $e->commit unless $U->event_code($res);
184     return $res;
185 }
186
187 __PACKAGE__->register_method(
188         method  => "update_biblio_record_entry",
189         api_name        => "open-ils.cat.biblio.record_entry.update",
190     signature => q/
191         Updates a biblio.record_entry
192         @param auth The authtoken
193         @param record The record with updated values
194         @return 1 on success, Event on error.
195     /
196 );
197
198 sub update_biblio_record_entry {
199     my($self, $conn, $auth, $record) = @_;
200     my $e = new_editor(authtoken=>$auth, xact=>1);
201     return $e->die_event unless $e->checkauth;
202     return $e->die_event unless $e->allowed('UPDATE_RECORD');
203     $e->update_biblio_record_entry($record) or return $e->die_event;
204     $e->commit;
205     return 1;
206 }
207
208 __PACKAGE__->register_method(
209         method  => "undelete_biblio_record_entry",
210         api_name        => "open-ils.cat.biblio.record_entry.undelete",
211     signature => q/
212         Un-deletes a record and sets active=true
213         @param auth The authtoken
214         @param record The record_id to ressurect
215         @return 1 on success, Event on error.
216     /
217 );
218 sub undelete_biblio_record_entry {
219     my($self, $conn, $auth, $record_id) = @_;
220     my $e = new_editor(authtoken=>$auth, xact=>1);
221     return $e->die_event unless $e->checkauth;
222     return $e->die_event unless $e->allowed('UPDATE_RECORD');
223
224     my $record = $e->retrieve_biblio_record_entry($record_id)
225         or return $e->die_event;
226     $record->deleted('f');
227     $record->active('t');
228
229     # no 2 non-deleted records can have the same tcn_value
230     my $existing = $e->search_biblio_record_entry(
231         {   deleted => 'f', 
232             tcn_value => $record->tcn_value, 
233             id => {'!=' => $record_id}
234         }, {idlist => 1});
235     return OpenILS::Event->new('TCN_EXISTS') if @$existing;
236
237     $e->update_biblio_record_entry($record) or return $e->die_event;
238     $e->commit;
239     return 1;
240 }
241
242
243 __PACKAGE__->register_method(
244         method  => "biblio_record_xml_import",
245         api_name        => "open-ils.cat.biblio.record.xml.import.override",
246         signature       => q/@see open-ils.cat.biblio.record.xml.import/);
247
248 __PACKAGE__->register_method(
249         method  => "biblio_record_xml_import",
250         api_name        => "open-ils.cat.biblio.record.xml.import",
251         notes           => <<"  NOTES");
252         Takes a marcxml record and imports the record into the database.  In this
253         case, the marcxml record is assumed to be a complete record (i.e. valid
254         MARC).  The title control number is taken from (whichever comes first)
255         tags 001, 039[ab], 020a, 022a, 010, 035a and whichever does not already exist
256         in the database.
257         user_session must have IMPORT_MARC permissions
258         NOTES
259
260
261 sub biblio_record_xml_import {
262         my( $self, $client, $authtoken, $xml, $source, $auto_tcn) = @_;
263     my $e = new_editor(xact=>1, authtoken=>$authtoken);
264     return $e->die_event unless $e->checkauth;
265     return $e->die_event unless $e->allowed('IMPORT_MARC', $e->requestor->ws_ou);
266
267     my $res = OpenILS::Application::Cat::BibCommon->biblio_record_xml_import(
268         $e, $xml, $source, $auto_tcn, $self->api_name =~ /override/);
269
270     $e->commit unless $U->event_code($res);
271     return $res;
272 }
273
274 __PACKAGE__->register_method(
275         method  => "biblio_record_record_metadata",
276         api_name        => "open-ils.cat.biblio.record.metadata.retrieve",
277     authoritative => 1,
278         argc            => 1, #(session_id, biblio_tree ) 
279         notes           => "Walks the tree and commits any changed nodes " .
280                                         "adds any new nodes, and deletes any deleted nodes",
281 );
282
283 sub biblio_record_record_metadata {
284         my( $self, $client, $authtoken, $ids ) = @_;
285
286         return [] unless $ids and @$ids;
287
288         my $editor = new_editor(authtoken => $authtoken);
289         return $editor->event unless $editor->checkauth;
290         return $editor->event unless $editor->allowed('VIEW_USER');
291
292         my @results;
293
294         for(@$ids) {
295                 return $editor->event unless 
296                         my $rec = $editor->retrieve_biblio_record_entry($_);
297                 $rec->creator($editor->retrieve_actor_user($rec->creator));
298                 $rec->editor($editor->retrieve_actor_user($rec->editor));
299                 $rec->clear_marc; # slim the record down
300                 push( @results, $rec );
301         }
302
303         return \@results;
304 }
305
306
307
308 __PACKAGE__->register_method(
309         method  => "biblio_record_marc_cn",
310         api_name        => "open-ils.cat.biblio.record.marc_cn.retrieve",
311         argc            => 1, #(bib id ) 
312 );
313
314 sub biblio_record_marc_cn {
315         my( $self, $client, $id ) = @_;
316
317         my $session = OpenSRF::AppSession->create("open-ils.cstore");
318         my $marc = $session
319                 ->request("open-ils.cstore.direct.biblio.record_entry.retrieve", $id )
320                 ->gather(1)
321                 ->marc;
322
323         my $doc = XML::LibXML->new->parse_string($marc);
324         $doc->documentElement->setNamespace( "http://www.loc.gov/MARC21/slim", "marc", 1 );
325         
326         my @res;
327         for my $tag ( qw/050 055 060 070 080 082 086 088 090 092 096 098 099/ ) {
328                 my @node = $doc->findnodes("//marc:datafield[\@tag='$tag']");
329                 for my $x (@node) {
330                         my $cn = $x->findvalue("marc:subfield[\@code='a' or \@code='b']");
331                         push @res, {$tag => $cn} if ($cn);
332                 }
333         }
334
335         return \@res
336 }
337
338
339 __PACKAGE__->register_method(
340         method  => "orgs_for_title",
341     authoritative => 1,
342         api_name        => "open-ils.cat.actor.org_unit.retrieve_by_title"
343 );
344
345 sub orgs_for_title {
346         my( $self, $client, $record_id ) = @_;
347
348         my $vols = $U->simple_scalar_request(
349                 "open-ils.cstore",
350                 "open-ils.cstore.direct.asset.call_number.search.atomic",
351                 { record => $record_id, deleted => 'f' });
352
353         my $orgs = { map {$_->owning_lib => 1 } @$vols };
354         return [ keys %$orgs ];
355 }
356
357
358 __PACKAGE__->register_method(
359         method  => "retrieve_copies",
360     authoritative => 1,
361         api_name        => "open-ils.cat.asset.copy_tree.retrieve");
362
363 __PACKAGE__->register_method(
364         method  => "retrieve_copies",
365         api_name        => "open-ils.cat.asset.copy_tree.global.retrieve");
366
367 # user_session may be null/undef
368 sub retrieve_copies {
369
370         my( $self, $client, $user_session, $docid, @org_ids ) = @_;
371
372         if(ref($org_ids[0])) { @org_ids = @{$org_ids[0]}; }
373
374         $docid = "$docid";
375
376         # grabbing copy trees should be available for everyone..
377         if(!@org_ids and $user_session) {
378                 my $user_obj = 
379                         OpenILS::Application::AppUtils->check_user_session( $user_session ); #throws EX on error
380                         @org_ids = ($user_obj->home_ou);
381         }
382
383         if( $self->api_name =~ /global/ ) {
384                 return _build_volume_list( { record => $docid, deleted => 'f' } );
385
386         } else {
387
388                 my @all_vols;
389                 for my $orgid (@org_ids) {
390                         my $vols = _build_volume_list( 
391                                         { record => $docid, owning_lib => $orgid, deleted => 'f' } );
392                         push( @all_vols, @$vols );
393                 }
394                 
395                 return \@all_vols;
396         }
397
398         return undef;
399 }
400
401
402 sub _build_volume_list {
403         my $search_hash = shift;
404
405         $search_hash->{deleted} = 'f';
406         my $e = new_editor();
407
408         my $vols = $e->search_asset_call_number($search_hash);
409
410         my @volumes;
411
412         for my $volume (@$vols) {
413
414                 my $copies = $e->search_asset_copy(
415                         { call_number => $volume->id , deleted => 'f' });
416
417                 $copies = [ sort { $a->barcode cmp $b->barcode } @$copies  ];
418
419                 for my $c (@$copies) {
420                         if( $c->status == OILS_COPY_STATUS_CHECKED_OUT ) {
421                                 $c->circulations(
422                                         $e->search_action_circulation(
423                                                 [
424                                                         { target_copy => $c->id },
425                                                         {
426                                                                 order_by => { circ => 'xact_start desc' },
427                                                                 limit => 1
428                                                         }
429                                                 ]
430                                         )
431                                 )
432                         }
433                 }
434
435                 $volume->copies($copies);
436                 push( @volumes, $volume );
437         }
438
439         #$session->disconnect();
440         return \@volumes;
441
442 }
443
444
445 __PACKAGE__->register_method(
446         method  => "fleshed_copy_update",
447         api_name        => "open-ils.cat.asset.copy.fleshed.batch.update",);
448
449 __PACKAGE__->register_method(
450         method  => "fleshed_copy_update",
451         api_name        => "open-ils.cat.asset.copy.fleshed.batch.update.override",);
452
453
454 sub fleshed_copy_update {
455         my( $self, $conn, $auth, $copies, $delete_stats ) = @_;
456         return 1 unless ref $copies;
457         my( $reqr, $evt ) = $U->checkses($auth);
458         return $evt if $evt;
459         my $editor = new_editor(requestor => $reqr, xact => 1);
460         my $override = $self->api_name =~ /override/;
461         $evt = update_fleshed_copies($editor, $override, undef, $copies, $delete_stats);
462         if( $evt ) { 
463                 $logger->info("fleshed copy update failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
464                 $editor->rollback; 
465                 return $evt; 
466         }
467         $editor->commit;
468         $logger->info("fleshed copy update successfully updated ".scalar(@$copies)." copies");
469         return 1;
470 }
471
472
473 __PACKAGE__->register_method(
474         method => 'merge',
475         api_name        => 'open-ils.cat.biblio.records.merge',
476         signature       => q/
477                 Merges a group of records
478                 @param auth The login session key
479                 @param master The id of the record all other records should be merged into
480                 @param records Array of records to be merged into the master record
481                 @return 1 on success, Event on error.
482         /
483 );
484
485 sub merge {
486         my( $self, $conn, $auth, $master, $records ) = @_;
487         my( $reqr, $evt ) = $U->checkses($auth);
488         return $evt if $evt;
489         my $editor = new_editor( requestor => $reqr, xact => 1 );
490         my $v = OpenILS::Application::Cat::Merge::merge_records($editor, $master, $records);
491         return $v if $v;
492         $editor->commit;
493     # tell the client the merge is complete, then merge the holds
494     $conn->respond_complete(1);
495     merge_holds($master, $records);
496         return undef;
497 }
498
499 sub merge_holds {
500     my($master, $records) = @_;
501     return unless $master and @$records;
502     return if @$records == 1 and $master == $$records[0];
503
504     my $e = new_editor(xact=>1);
505     my $holds = $e->search_action_hold_request(
506         {   cancel_time => undef, 
507             fulfillment_time => undef,
508             hold_type => 'T',
509             target => $records
510         },
511         {idlist=>1}
512     );
513
514     for my $hold_id (@$holds) {
515
516         my $hold = $e->retrieve_action_hold_request($hold_id);
517
518         $logger->info("Changing hold ".$hold->id.
519             " target from ".$hold->target." to $master in record merge");
520
521         $hold->target($master);
522         unless($e->update_action_hold_request($hold)) {
523             my $evt = $e->event;
524             $logger->error("Error updating hold ". $evt->textcode .":". $evt->desc .":". $evt->stacktrace); 
525         }
526     }
527
528     $e->commit;
529     return undef;
530 }
531
532
533
534
535 # ---------------------------------------------------------------------------
536 # returns true if the given title (id) has no un-deleted volumes or 
537 # copies attached.  If a context volume is defined, a record
538 # is considered empty only if the context volume is the only
539 # remaining volume on the record.  
540 # ---------------------------------------------------------------------------
541 sub title_is_empty {
542         my( $editor, $rid, $vol_id ) = @_;
543
544         return 0 if $rid == OILS_PRECAT_RECORD;
545
546         my $cnlist = $editor->search_asset_call_number(
547                 { record => $rid, deleted => 'f' }, { idlist => 1 } );
548
549         return 1 unless @$cnlist; # no attached volumes
550     return 0 if @$cnlist > 1; # multiple attached volumes
551     return 0 unless $$cnlist[0] == $vol_id; # attached volume is not the context vol.
552
553     # see if the sole remaining context volume has any attached copies
554         for my $cn (@$cnlist) {
555                 my $copylist = $editor->search_asset_copy(
556                         [
557                                 { call_number => $cn, deleted => 'f' }, 
558                                 { limit => 1 },
559                         ], { idlist => 1 });
560                 return 0 if @$copylist; # false if we find any copies
561         }
562
563         return 1;
564 }
565
566
567 __PACKAGE__->register_method(
568         method  => "fleshed_volume_update",
569         api_name        => "open-ils.cat.asset.volume.fleshed.batch.update",);
570
571 __PACKAGE__->register_method(
572         method  => "fleshed_volume_update",
573         api_name        => "open-ils.cat.asset.volume.fleshed.batch.update.override",);
574
575 sub fleshed_volume_update {
576         my( $self, $conn, $auth, $volumes, $delete_stats ) = @_;
577         my( $reqr, $evt ) = $U->checkses($auth);
578         return $evt if $evt;
579
580         my $override = ($self->api_name =~ /override/);
581         my $editor = new_editor( requestor => $reqr, xact => 1 );
582
583         for my $vol (@$volumes) {
584                 $logger->info("vol-update: investigating volume ".$vol->id);
585
586                 $vol->editor($reqr->id);
587                 $vol->edit_date('now');
588
589                 my $copies = $vol->copies;
590                 $vol->clear_copies;
591
592                 $vol->editor($editor->requestor->id);
593                 $vol->edit_date('now');
594
595                 if( $vol->isdeleted ) {
596
597                         $logger->info("vol-update: deleting volume");
598                         my $cs = $editor->search_asset_copy(
599                                 { call_number => $vol->id, deleted => 'f' } );
600                         return OpenILS::Event->new(
601                                 'VOLUME_NOT_EMPTY', payload => $vol->id ) if @$cs;
602
603                         $vol->deleted('t');
604                         return $editor->event unless
605                                 $editor->update_asset_call_number($vol);
606
607                         
608                 } elsif( $vol->isnew ) {
609                         $logger->info("vol-update: creating volume");
610                         $evt = create_volume( $override, $editor, $vol );
611                         return $evt if $evt;
612
613                 } elsif( $vol->ischanged ) {
614                         $logger->info("vol-update: update volume");
615                         $evt = update_volume($vol, $editor);
616                         return $evt if $evt;
617                 }
618
619                 # now update any attached copies
620                 if( $copies and @$copies and !$vol->isdeleted ) {
621                         $_->call_number($vol->id) for @$copies;
622                         $evt = update_fleshed_copies( $editor, $override, $vol, $copies, $delete_stats );
623                         return $evt if $evt;
624                 }
625         }
626
627         $editor->finish;
628         return scalar(@$volumes);
629 }
630
631
632 sub update_volume {
633         my $vol = shift;
634         my $editor = shift;
635         my $evt;
636
637         return $evt if ( $evt = org_cannot_have_vols($editor, $vol->owning_lib) );
638
639         my $vols = $editor->search_asset_call_number( { 
640                         owning_lib      => $vol->owning_lib,
641                         record          => $vol->record,
642                         label                   => $vol->label,
643                         deleted         => 'f'
644                 }
645         );
646
647         # There exists a different volume in the DB with the same properties
648         return OpenILS::Event->new('VOLUME_LABEL_EXISTS', payload => $vol->id)
649                 if grep { $_->id ne $vol->id } @$vols;
650
651         return $editor->event unless $editor->update_asset_call_number($vol);
652         return undef;
653 }
654
655
656
657 sub copy_perm_org {
658         my( $vol, $copy ) = @_;
659         my $org = $vol->owning_lib;
660         if( $vol->id == OILS_PRECAT_CALL_NUMBER ) {
661                 $org = ref($copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
662         }
663         $logger->debug("using copy perm org $org");
664         return $org;
665 }
666
667
668 # this does the actual work
669 sub update_fleshed_copies {
670         my( $editor, $override, $vol, $copies, $delete_stats ) = @_;
671
672         my $evt;
673         my $fetchvol = ($vol) ? 0 : 1;
674
675         my %cache;
676         $cache{$vol->id} = $vol if $vol;
677
678         for my $copy (@$copies) {
679
680                 my $copyid = $copy->id;
681                 $logger->info("vol-update: inspecting copy $copyid");
682
683                 if( !($vol = $cache{$copy->call_number}) ) {
684                         $vol = $cache{$copy->call_number} = 
685                                 $editor->retrieve_asset_call_number($copy->call_number);
686                         return $editor->event unless $vol;
687                 }
688
689                 return $editor->event unless 
690                         $editor->allowed('UPDATE_COPY', copy_perm_org($vol, $copy));
691
692                 $copy->editor($editor->requestor->id);
693                 $copy->edit_date('now');
694
695                 $copy->status( $copy->status->id ) if ref($copy->status);
696                 $copy->location( $copy->location->id ) if ref($copy->location);
697                 $copy->circ_lib( $copy->circ_lib->id ) if ref($copy->circ_lib);
698                 
699                 my $sc_entries = $copy->stat_cat_entries;
700                 $copy->clear_stat_cat_entries;
701
702                 if( $copy->isdeleted ) {
703                         $evt = delete_copy($editor, $override, $vol, $copy);
704                         return $evt if $evt;
705
706                 } elsif( $copy->isnew ) {
707                         $evt = create_copy( $editor, $vol, $copy );
708                         return $evt if $evt;
709
710                 } elsif( $copy->ischanged ) {
711
712                         $evt = update_copy( $editor, $override, $vol, $copy );
713                         return $evt if $evt;
714                 }
715
716                 $copy->stat_cat_entries( $sc_entries );
717                 $evt = update_copy_stat_entries($editor, $copy, $delete_stats);
718                 return $evt if $evt;
719         }
720
721         $logger->debug("vol-update: done updating copy batch");
722
723         return undef;
724 }
725
726 sub fix_copy_price {
727         my $copy = shift;
728
729     if(defined $copy->price) {
730             my $p = $copy->price || 0;
731             $p =~ s/\$//og;
732             $copy->price($p);
733     }
734
735         my $d = $copy->deposit_amount || 0;
736         $d =~ s/\$//og;
737         $copy->deposit_amount($d);
738 }
739
740
741 sub update_copy {
742         my( $editor, $override, $vol, $copy ) = @_;
743
744         my $evt;
745         my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
746         return $evt if ( $evt = org_cannot_have_vols($editor, $org) );
747
748         $logger->info("vol-update: updating copy ".$copy->id);
749         my $orig_copy = $editor->retrieve_asset_copy($copy->id);
750         my $orig_vol  = $editor->retrieve_asset_call_number($copy->call_number);
751
752         $copy->editor($editor->requestor->id);
753         $copy->edit_date('now');
754
755         $copy->age_protect( $copy->age_protect->id )
756                 if ref $copy->age_protect;
757
758         fix_copy_price($copy);
759
760         return $editor->event unless $editor->update_asset_copy($copy);
761         return remove_empty_objects($editor, $override, $orig_vol);
762 }
763
764
765 sub remove_empty_objects {
766         my( $editor, $override, $vol ) = @_; 
767
768     my $koe = $U->ou_ancestor_setting_value(
769         $editor->requestor->ws_ou, 'cat.bib.keep_on_empty', $editor);
770     my $aoe =  $U->ou_ancestor_setting_value(
771         $editor->requestor->ws_ou, 'cat.bib.alert_on_empty', $editor);
772
773         if( title_is_empty($editor, $vol->record, $vol->id) ) {
774
775         # delete this volume if it's not already marked as deleted
776         unless( $U->is_true($vol->deleted) || $vol->isdeleted ) {
777             $vol->deleted('t');
778             $vol->editor($editor->requestor->id);
779             $vol->edit_date('now');
780             $editor->update_asset_call_number($vol) or return $editor->event;
781         }
782
783         unless($koe) {
784             # delete the bib record if the keep-on-empty setting is not set
785             my $evt = delete_rec($editor, $vol->record);
786             return $evt if $evt;
787         }
788
789         # return the empty alert if the alert-on-empty setting is set
790         return OpenILS::Event->new('TITLE_LAST_COPY', payload => $vol->record ) if $aoe;
791         }
792
793         return undef;
794 }
795
796
797 __PACKAGE__->register_method (
798         method => 'delete_bib_record',
799         api_name => 'open-ils.cat.biblio.record_entry.delete');
800
801 sub delete_bib_record {
802     my($self, $conn, $auth, $rec_id) = @_;
803     my $e = new_editor(xact=>1, authtoken=>$auth);
804     return $e->die_event unless $e->checkauth;
805     return $e->die_event unless $e->allowed('DELETE_RECORD', $e->requestor->ws_ou);
806     my $vols = $e->search_asset_call_number({record=>$rec_id, deleted=>'f'});
807     return OpenILS::Event->new('RECORD_NOT_EMPTY', payload=>$rec_id) if @$vols;
808     my $evt = delete_rec($e, $rec_id);
809     if($evt) { $e->rollback; return $evt; }   
810     $e->commit;
811     return 1;
812 }
813
814
815 # marks a record as deleted
816 sub delete_rec {
817    my( $editor, $rec_id ) = @_;
818
819    my $rec = $editor->retrieve_biblio_record_entry($rec_id)
820       or return $editor->event;
821
822    return undef if $U->is_true($rec->deleted);
823    
824    $rec->deleted('t');
825    $rec->active('f');
826    $rec->editor( $editor->requestor->id );
827    $rec->edit_date('now');
828    $editor->update_biblio_record_entry($rec) or return $editor->event;
829
830    return undef;
831 }
832
833
834 sub delete_copy {
835         my( $editor, $override, $vol, $copy ) = @_;
836
837    return $editor->event unless 
838       $editor->allowed('DELETE_COPY',copy_perm_org($vol, $copy));
839
840         my $stat = $U->copy_status($copy->status)->id;
841
842         unless($override) {
843                 return OpenILS::Event->new('COPY_DELETE_WARNING', payload => $copy->id )
844                         if $stat == OILS_COPY_STATUS_CHECKED_OUT or
845                                 $stat == OILS_COPY_STATUS_IN_TRANSIT or
846                                 $stat == OILS_COPY_STATUS_ON_HOLDS_SHELF or
847                                 $stat == OILS_COPY_STATUS_ILL;
848         }
849
850         $logger->info("vol-update: deleting copy ".$copy->id);
851         $copy->deleted('t');
852
853         $copy->editor($editor->requestor->id);
854         $copy->edit_date('now');
855         $editor->update_asset_copy($copy) or return $editor->event;
856
857         # Delete any open transits for this copy
858         my $transits = $editor->search_action_transit_copy(
859                 { target_copy=>$copy->id, dest_recv_time => undef } );
860
861         for my $t (@$transits) {
862                 $editor->delete_action_transit_copy($t)
863                         or return $editor->event;
864         }
865
866         return remove_empty_objects($editor, $override, $vol);
867 }
868
869
870 sub create_copy {
871         my( $editor, $vol, $copy ) = @_;
872
873         my $existing = $editor->search_asset_copy(
874                 { barcode => $copy->barcode, deleted => 'f' } );
875         
876         return OpenILS::Event->new('ITEM_BARCODE_EXISTS') if @$existing;
877
878    # see if the volume this copy references is marked as deleted
879    my $evol = $editor->retrieve_asset_call_number($copy->call_number)
880       or return $editor->event;
881    return OpenILS::Event->new('VOLUME_DELETED', vol => $evol->id) 
882       if $U->is_true($evol->deleted);
883
884         my $evt;
885         my $org = (ref $copy->circ_lib) ? $copy->circ_lib->id : $copy->circ_lib;
886         return $evt if ( $evt = org_cannot_have_vols($editor, $org) );
887
888         $copy->clear_id;
889         $copy->creator($editor->requestor->id);
890         $copy->create_date('now');
891         fix_copy_price($copy);
892
893         $editor->create_asset_copy($copy) or return $editor->event;
894         return undef;
895 }
896
897 # if 'delete_stats' is true, the copy->stat_cat_entries data is 
898 # treated as the authoritative list for the copy. existing entries
899 # that are not in said list will be deleted from the DB
900 sub update_copy_stat_entries {
901         my( $editor, $copy, $delete_stats ) = @_;
902
903         return undef if $copy->isdeleted;
904         return undef unless $copy->ischanged or $copy->isnew;
905
906         my $evt;
907         my $entries = $copy->stat_cat_entries;
908
909         if( $delete_stats ) {
910                 $entries = ($entries and @$entries) ? $entries : [];
911         } else {
912                 return undef unless ($entries and @$entries);
913         }
914
915         my $maps = $editor->search_asset_stat_cat_entry_copy_map({owning_copy=>$copy->id});
916
917         if(!$copy->isnew) {
918                 # if there is no stat cat entry on the copy who's id matches the
919                 # current map's id, remove the map from the database
920                 for my $map (@$maps) {
921                         if(! grep { $_->id == $map->stat_cat_entry } @$entries ) {
922
923                                 $logger->info("copy update found stale ".
924                                         "stat cat entry map ".$map->id. " on copy ".$copy->id);
925
926                                 $editor->delete_asset_stat_cat_entry_copy_map($map)
927                                         or return $editor->event;
928                         }
929                 }
930         }
931
932         # go through the stat cat update/create process
933         for my $entry (@$entries) { 
934                 next unless $entry;
935
936                 # if this link already exists in the DB, don't attempt to re-create it
937                 next if( grep{$_->stat_cat_entry == $entry->id} @$maps );
938         
939                 my $new_map = Fieldmapper::asset::stat_cat_entry_copy_map->new();
940
941                 my $sc = ref($entry->stat_cat) ? $entry->stat_cat->id : $entry->stat_cat;
942                 
943                 $new_map->stat_cat( $sc );
944                 $new_map->stat_cat_entry( $entry->id );
945                 $new_map->owning_copy( $copy->id );
946
947                 $editor->create_asset_stat_cat_entry_copy_map($new_map)
948                         or return $editor->event;
949
950                 $logger->info("copy update created new stat cat entry map ".$editor->data);
951         }
952
953         return undef;
954 }
955
956
957 sub create_volume {
958         my( $override, $editor, $vol ) = @_;
959         my $evt;
960
961         return $evt if ( $evt = org_cannot_have_vols($editor, $vol->owning_lib) );
962
963    # see if the record this volume references is marked as deleted
964    my $rec = $editor->retrieve_biblio_record_entry($vol->record)
965       or return $editor->event;
966    return OpenILS::Event->new('BIB_RECORD_DELETED', rec => $rec->id) 
967       if $U->is_true($rec->deleted);
968
969         # first lets see if there are any collisions
970         my $vols = $editor->search_asset_call_number( { 
971                         owning_lib      => $vol->owning_lib,
972                         record          => $vol->record,
973                         label                   => $vol->label,
974                         deleted         => 'f'
975                 }
976         );
977
978         my $label = undef;
979         if(@$vols) {
980       # we've found an exising volume
981                 if($override) { 
982                         $label = $vol->label;
983                 } else {
984                         return OpenILS::Event->new(
985                                 'VOLUME_LABEL_EXISTS', payload => $vol->id);
986                 }
987         }
988
989         # create a temp label so we can create the new volume, 
990    # then de-dup it with the existing volume
991         $vol->label( "__SYSTEM_TMP_$$".time) if $label;
992
993         $vol->creator($editor->requestor->id);
994         $vol->create_date('now');
995         $vol->editor($editor->requestor->id);
996         $vol->edit_date('now');
997         $vol->clear_id;
998
999         $editor->create_asset_call_number($vol) or return $editor->event;
1000
1001         if($label) {
1002                 # now restore the label and merge into the existing record
1003                 $vol->label($label);
1004                 (undef, $evt) = 
1005                         OpenILS::Application::Cat::Merge::merge_volumes($editor, [$vol], $$vols[0]);
1006                 return $evt if $evt;
1007         }
1008
1009         return undef;
1010 }
1011
1012
1013 __PACKAGE__->register_method (
1014         method => 'batch_volume_transfer',
1015         api_name => 'open-ils.cat.asset.volume.batch.transfer',
1016 );
1017
1018 __PACKAGE__->register_method (
1019         method => 'batch_volume_transfer',
1020         api_name => 'open-ils.cat.asset.volume.batch.transfer.override',
1021 );
1022
1023
1024 sub batch_volume_transfer {
1025         my( $self, $conn, $auth, $args ) = @_;
1026
1027         my $evt;
1028         my $rec         = $$args{docid};
1029         my $o_lib       = $$args{lib};
1030         my $vol_ids = $$args{volumes};
1031
1032         my $override = 1 if $self->api_name =~ /override/;
1033
1034         $logger->info("merge: transferring volumes to lib=$o_lib and record=$rec");
1035
1036         my $e = new_editor(authtoken => $auth, xact =>1);
1037         return $e->event unless $e->checkauth;
1038         return $e->event unless $e->allowed('UPDATE_VOLUME', $o_lib);
1039
1040         my $dorg = $e->retrieve_actor_org_unit($o_lib)
1041                 or return $e->event;
1042
1043         my $ou_type = $e->retrieve_actor_org_unit_type($dorg->ou_type)
1044                 or return $e->event;
1045
1046         return $evt if ( $evt = org_cannot_have_vols($e, $o_lib) );
1047
1048         my $vols = $e->batch_retrieve_asset_call_number($vol_ids);
1049         my @seen;
1050
1051    my @rec_ids;
1052
1053         for my $vol (@$vols) {
1054
1055                 # if we've already looked at this volume, go to the next
1056                 next if !$vol or grep { $vol->id == $_ } @seen;
1057
1058                 # grab all of the volumes in the list that have 
1059                 # the same label so they can be merged
1060                 my @all = grep { $_->label eq $vol->label } @$vols;
1061
1062                 # take note of the fact that we've looked at this set of volumes
1063                 push( @seen, $_->id ) for @all;
1064       push( @rec_ids, $_->record ) for @all;
1065
1066                 # for each volume, see if there are any copies that have a 
1067                 # remote circ_lib (circ_lib != vol->owning_lib and != $o_lib ).  
1068                 # if so, warn them
1069                 unless( $override ) {
1070                         for my $v (@all) {
1071
1072                                 $logger->debug("merge: searching for copies with remote circ_lib for volume ".$v->id);
1073                                 my $args = { 
1074                                         call_number     => $v->id, 
1075                                         circ_lib                => { "not in" => [ $o_lib, $v->owning_lib ] },
1076                                         deleted         => 'f'
1077                                 };
1078
1079                                 my $copies = $e->search_asset_copy($args, {idlist=>1});
1080
1081                                 # if the copy's circ_lib matches the destination lib,
1082                                 # that's ok too
1083                                 return OpenILS::Event->new('COPY_REMOTE_CIRC_LIB') if @$copies;
1084                         }
1085                 }
1086
1087                 # see if there is a volume at the destination lib that 
1088                 # already has the requested label
1089                 my $existing_vol = $e->search_asset_call_number(
1090                         {
1091                                 label                   => $vol->label, 
1092                                 record          =>$rec, 
1093                                 owning_lib      =>$o_lib,
1094                                 deleted         => 'f'
1095                         }
1096                 )->[0];
1097
1098                 if( $existing_vol ) {
1099
1100                         if( grep { $_->id == $existing_vol->id } @all ) {
1101                                 # this volume is already accounted for in our list of volumes to merge
1102                                 $existing_vol = undef;
1103
1104                         } else {
1105                                 # this volume exists on the destination record/owning_lib and must
1106                                 # be used as the destination for merging
1107                                 $logger->debug("merge: volume already exists at destination record: ".
1108                                         $existing_vol->id.' : '.$existing_vol->label) if $existing_vol;
1109                         }
1110                 } 
1111
1112                 if( @all > 1 || $existing_vol ) {
1113                         $logger->info("merge: found collisions in volume transfer");
1114                         my @args = ($e, \@all);
1115                         @args = ($e, \@all, $existing_vol) if $existing_vol;
1116                         ($vol, $evt) = OpenILS::Application::Cat::Merge::merge_volumes(@args);
1117                         return $evt if $evt;
1118                 } 
1119                 
1120                 if( !$existing_vol ) {
1121
1122                         $vol->owning_lib($o_lib);
1123                         $vol->record($rec);
1124                         $vol->editor($e->requestor->id);
1125                         $vol->edit_date('now');
1126         
1127                         $logger->info("merge: updating volume ".$vol->id);
1128                         $e->update_asset_call_number($vol) or return $e->event;
1129
1130                 } else {
1131                         $logger->info("merge: bypassing volume update because existing volume used as target");
1132                 }
1133
1134                 # regardless of what volume was used as the destination, 
1135                 # update any copies that have moved over to the new lib
1136                 my $copies = $e->search_asset_copy({call_number=>$vol->id, deleted => 'f'});
1137
1138                 # update circ lib on the copies - make this a method flag?
1139                 for my $copy (@$copies) {
1140                         next if $copy->circ_lib == $o_lib;
1141                         $logger->info("merge: transfer moving circ lib on copy ".$copy->id);
1142                         $copy->circ_lib($o_lib);
1143                         $copy->editor($e->requestor->id);
1144                         $copy->edit_date('now');
1145                         $e->update_asset_copy($copy) or return $e->event;
1146                 }
1147
1148                 # Now see if any empty records need to be deleted after all of this
1149
1150       for(@rec_ids) {
1151          $logger->debug("merge: seeing if we should delete record $_...");
1152          $evt = delete_rec($e, $_) if title_is_empty($e, $_);
1153                         return $evt if $evt;
1154       }
1155
1156                 #for(@all) {
1157                 #       $evt = remove_empty_objects($e, $override, $_);
1158                 #}
1159         }
1160
1161         $logger->info("merge: transfer succeeded");
1162         $e->commit;
1163         return 1;
1164 }
1165
1166
1167
1168 sub org_cannot_have_vols {
1169         my $e = shift;
1170         my $org_id = shift;
1171
1172         my $org = $e->retrieve_actor_org_unit($org_id)
1173                 or return $e->event;
1174
1175         my $ou_type = $e->retrieve_actor_org_unit_type($org->ou_type)
1176                 or return $e->event;
1177
1178         return OpenILS::Event->new('ORG_CANNOT_HAVE_VOLS')
1179                 unless $U->is_true($ou_type->can_have_vols);
1180
1181         return 0;
1182 }
1183
1184
1185
1186
1187 __PACKAGE__->register_method(
1188         api_name => 'open-ils.cat.call_number.find_or_create',
1189         method => 'find_or_create_volume',
1190 );
1191
1192 sub find_or_create_volume {
1193         my( $self, $conn, $auth, $label, $record_id, $org_id ) = @_;
1194         my $e = new_editor(authtoken=>$auth, xact=>1);
1195         return $e->die_event unless $e->checkauth;
1196
1197     my $vol;
1198
1199     if($record_id == OILS_PRECAT_RECORD) {
1200
1201         $vol = $e->retrieve_asset_call_number(OILS_PRECAT_CALL_NUMBER)
1202             or return $e->die_event;
1203
1204     } else {
1205         
1206             $vol = $e->search_asset_call_number(
1207                     {label => $label, record => $record_id, owning_lib => $org_id, deleted => 'f'}, 
1208                     {idlist=>1}
1209             )->[0];
1210     }
1211
1212         # If the volume exists, return the ID
1213         if( $vol ) { $e->rollback; return $vol; }
1214
1215         # -----------------------------------------------------------------
1216         # Otherwise, create a new volume with the given attributes
1217         # -----------------------------------------------------------------
1218
1219         return $e->die_event unless $e->allowed('UPDATE_VOLUME', $org_id);
1220
1221         $vol = Fieldmapper::asset::call_number->new;
1222         $vol->owning_lib($org_id);
1223         $vol->label($label);
1224         $vol->record($record_id);
1225
1226    my $evt = create_volume( 0, $e, $vol );
1227    return $evt if $evt;
1228
1229         $e->commit;
1230         return $vol->id;
1231 }
1232
1233
1234
1235 1;