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