]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Serial.pm
merge seials-integration [sic] branch into trunk
[working/Evergreen.git] / Open-ILS / src / perlmods / OpenILS / Application / Serial.pm
1 #!/usr/bin/perl
2
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
7 #
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11 # GNU General Public License for more details.
12 #
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software
15 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
16
17 =head1 NAME
18
19 OpenILS::Application::Serial - Performs serials-related tasks such as receiving issues and generating predictions
20
21 =head1 SYNOPSIS
22
23 TBD
24
25 =head1 DESCRIPTION
26
27 TBD
28
29 =head1 AUTHOR
30
31 Dan Wells, dbw2@calvin.edu
32
33 =cut
34
35 package OpenILS::Application::Serial;
36
37 use strict;
38 use warnings;
39
40 use OpenILS::Application;
41 use base qw/OpenILS::Application/;
42 use OpenILS::Application::AppUtils;
43 use OpenSRF::AppSession;
44 use OpenSRF::Utils qw/:datetime/;;
45 use OpenSRF::Utils::Logger qw($logger);
46 use OpenILS::Utils::CStoreEditor q/:funcs/;
47 use OpenILS::Utils::MFHD;
48 use MARC::File::XML (BinaryEncoding => 'utf8');
49 my $U = 'OpenILS::Application::AppUtils';
50 my @MFHD_NAMES = ('basic','supplement','index');
51 my %MFHD_NAMES_BY_TAG = (  '853' => $MFHD_NAMES[0],
52                         '863' => $MFHD_NAMES[0],
53                         '854' => $MFHD_NAMES[1],
54                         '864' => $MFHD_NAMES[1],
55                         '855' => $MFHD_NAMES[2],
56                         '865' => $MFHD_NAMES[2] );
57 my %MFHD_TAGS_BY_NAME = (  $MFHD_NAMES[0] => '853',
58                         $MFHD_NAMES[1] => '854',
59                         $MFHD_NAMES[2] => '855');
60
61
62 # helper method for conforming dates to ISO8601
63 sub _cleanse_dates {
64     my $item = shift;
65     my $fields = shift;
66
67     foreach my $field (@$fields) {
68         $item->$field(OpenSRF::Utils::clense_ISO8601($item->$field)) if $item->$field;
69     }
70     return 0;
71 }
72
73
74 ##########################################################################
75 # item methods
76 #
77 __PACKAGE__->register_method(
78     method    => 'fleshed_item_alter',
79     api_name  => 'open-ils.serial.item.fleshed.batch.update',
80     api_level => 1,
81     argc      => 2,
82     signature => {
83         desc     => 'Receives an array of one or more items and updates the database as needed',
84         'params' => [ {
85                  name => 'authtoken',
86                  desc => 'Authtoken for current user session',
87                  type => 'string'
88             },
89             {
90                  name => 'items',
91                  desc => 'Array of fleshed items',
92                  type => 'array'
93             }
94
95         ],
96         'return' => {
97             desc => 'Returns 1 if successful, event if failed',
98             type => 'mixed'
99         }
100     }
101 );
102
103 sub fleshed_item_alter {
104     my( $self, $conn, $auth, $items ) = @_;
105     return 1 unless ref $items;
106     my( $reqr, $evt ) = $U->checkses($auth);
107     return $evt if $evt;
108     my $editor = new_editor(requestor => $reqr, xact => 1);
109     my $override = $self->api_name =~ /override/;
110
111 # TODO: permission check
112 #        return $editor->event unless
113 #            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
114
115     for my $item (@$items) {
116
117         my $itemid = $item->id;
118         $item->editor($editor->requestor->id);
119         $item->edit_date('now');
120
121         if( $item->isdeleted ) {
122             $evt = _delete_sitem( $editor, $override, $item);
123         } elsif( $item->isnew ) {
124             # TODO: reconsider this
125             # if the item has a new issuance, create the issuance first
126             if (ref $item->issuance eq 'Fieldmapper::serial::issuance' and $item->issuance->isnew) {
127                 fleshed_issuance_alter($self, $conn, $auth, [$item->issuance]);
128             }
129             _cleanse_dates($item, ['date_expected','date_received']);
130             $evt = _create_sitem( $editor, $item );
131         } else {
132             _cleanse_dates($item, ['date_expected','date_received']);
133             $evt = _update_sitem( $editor, $override, $item );
134         }
135     }
136
137     if( $evt ) {
138         $logger->info("fleshed item-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
139         $editor->rollback;
140         return $evt;
141     }
142     $logger->debug("item-alter: done updating item batch");
143     $editor->commit;
144     $logger->info("fleshed item-alter successfully updated ".scalar(@$items)." items");
145     return 1;
146 }
147
148 sub _delete_sitem {
149     my ($editor, $override, $item) = @_;
150     $logger->info("item-alter: delete item ".OpenSRF::Utils::JSON->perl2JSON($item));
151     return $editor->event unless $editor->delete_serial_item($item);
152     return 0;
153 }
154
155 sub _create_sitem {
156     my ($editor, $item) = @_;
157
158     $item->creator($editor->requestor->id);
159     $item->create_date('now');
160
161     $logger->info("item-alter: new item ".OpenSRF::Utils::JSON->perl2JSON($item));
162     return $editor->event unless $editor->create_serial_item($item);
163     return 0;
164 }
165
166 sub _update_sitem {
167     my ($editor, $override, $item) = @_;
168
169     $logger->info("item-alter: retrieving item ".$item->id);
170     my $orig_item = $editor->retrieve_serial_item($item->id);
171
172     $logger->info("item-alter: original item ".OpenSRF::Utils::JSON->perl2JSON($orig_item));
173     $logger->info("item-alter: updated item ".OpenSRF::Utils::JSON->perl2JSON($item));
174     return $editor->event unless $editor->update_serial_item($item);
175     return 0;
176 }
177
178 __PACKAGE__->register_method(
179     method  => "fleshed_serial_item_retrieve_batch",
180     authoritative => 1,
181     api_name    => "open-ils.serial.item.fleshed.batch.retrieve"
182 );
183
184 sub fleshed_serial_item_retrieve_batch {
185     my( $self, $client, $ids ) = @_;
186 # FIXME: permissions?
187     $logger->info("Fetching fleshed serial items @$ids");
188     return $U->cstorereq(
189         "open-ils.cstore.direct.serial.item.search.atomic",
190         { id => $ids },
191         { flesh => 2,
192           flesh_fields => {sitem => [ qw/issuance creator editor stream unit notes/ ], sstr => ["distribution"], sunit => ["call_number"], siss => [qw/creator editor subscription/]}
193         });
194 }
195
196
197 ##########################################################################
198 # issuance methods
199 #
200 __PACKAGE__->register_method(
201     method    => 'fleshed_issuance_alter',
202     api_name  => 'open-ils.serial.issuance.fleshed.batch.update',
203     api_level => 1,
204     argc      => 2,
205     signature => {
206         desc     => 'Receives an array of one or more issuances and updates the database as needed',
207         'params' => [ {
208                  name => 'authtoken',
209                  desc => 'Authtoken for current user session',
210                  type => 'string'
211             },
212             {
213                  name => 'issuances',
214                  desc => 'Array of fleshed issuances',
215                  type => 'array'
216             }
217
218         ],
219         'return' => {
220             desc => 'Returns 1 if successful, event if failed',
221             type => 'mixed'
222         }
223     }
224 );
225
226 sub fleshed_issuance_alter {
227     my( $self, $conn, $auth, $issuances ) = @_;
228     return 1 unless ref $issuances;
229     my( $reqr, $evt ) = $U->checkses($auth);
230     return $evt if $evt;
231     my $editor = new_editor(requestor => $reqr, xact => 1);
232     my $override = $self->api_name =~ /override/;
233
234 # TODO: permission support
235 #        return $editor->event unless
236 #            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
237
238     for my $issuance (@$issuances) {
239         my $issuanceid = $issuance->id;
240         $issuance->editor($editor->requestor->id);
241         $issuance->edit_date('now');
242
243         if( $issuance->isdeleted ) {
244             $evt = _delete_siss( $editor, $override, $issuance);
245         } elsif( $issuance->isnew ) {
246             _cleanse_dates($issuance, ['date_published']);
247             $evt = _create_siss( $editor, $issuance );
248         } else {
249             _cleanse_dates($issuance, ['date_published']);
250             $evt = _update_siss( $editor, $override, $issuance );
251         }
252     }
253
254     if( $evt ) {
255         $logger->info("fleshed issuance-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
256         $editor->rollback;
257         return $evt;
258     }
259     $logger->debug("issuance-alter: done updating issuance batch");
260     $editor->commit;
261     $logger->info("fleshed issuance-alter successfully updated ".scalar(@$issuances)." issuances");
262     return 1;
263 }
264
265 sub _delete_siss {
266     my ($editor, $override, $issuance) = @_;
267     $logger->info("issuance-alter: delete issuance ".OpenSRF::Utils::JSON->perl2JSON($issuance));
268     return $editor->event unless $editor->delete_serial_issuance($issuance);
269     return 0;
270 }
271
272 sub _create_siss {
273     my ($editor, $issuance) = @_;
274
275     $issuance->creator($editor->requestor->id);
276     $issuance->create_date('now');
277
278     $logger->info("issuance-alter: new issuance ".OpenSRF::Utils::JSON->perl2JSON($issuance));
279     return $editor->event unless $editor->create_serial_issuance($issuance);
280     return 0;
281 }
282
283 sub _update_siss {
284     my ($editor, $override, $issuance) = @_;
285
286     $logger->info("issuance-alter: retrieving issuance ".$issuance->id);
287     my $orig_issuance = $editor->retrieve_serial_issuance($issuance->id);
288
289     $logger->info("issuance-alter: original issuance ".OpenSRF::Utils::JSON->perl2JSON($orig_issuance));
290     $logger->info("issuance-alter: updated issuance ".OpenSRF::Utils::JSON->perl2JSON($issuance));
291     return $editor->event unless $editor->update_serial_issuance($issuance);
292     return 0;
293 }
294
295 __PACKAGE__->register_method(
296     method  => "fleshed_serial_issuance_retrieve_batch",
297     authoritative => 1,
298     api_name    => "open-ils.serial.issuance.fleshed.batch.retrieve"
299 );
300
301 sub fleshed_serial_issuance_retrieve_batch {
302     my( $self, $client, $ids ) = @_;
303 # FIXME: permissions?
304     $logger->info("Fetching fleshed serial issuances @$ids");
305     return $U->cstorereq(
306         "open-ils.cstore.direct.serial.issuance.search.atomic",
307         { id => $ids },
308         { flesh => 1,
309           flesh_fields => {siss => [ qw/creator editor subscription/ ]}
310         });
311 }
312
313
314 ##########################################################################
315 # unit methods
316 #
317 __PACKAGE__->register_method(
318     method    => 'fleshed_sunit_alter',
319     api_name  => 'open-ils.serial.sunit.fleshed.batch.update',
320     api_level => 1,
321     argc      => 2,
322     signature => {
323         desc     => 'Receives an array of one or more Units and updates the database as needed',
324         'params' => [ {
325                  name => 'authtoken',
326                  desc => 'Authtoken for current user session',
327                  type => 'string'
328             },
329             {
330                  name => 'sunits',
331                  desc => 'Array of fleshed Units',
332                  type => 'array'
333             }
334
335         ],
336         'return' => {
337             desc => 'Returns 1 if successful, event if failed',
338             type => 'mixed'
339         }
340     }
341 );
342
343 sub fleshed_sunit_alter {
344     my( $self, $conn, $auth, $sunits ) = @_;
345     return 1 unless ref $sunits;
346     my( $reqr, $evt ) = $U->checkses($auth);
347     return $evt if $evt;
348     my $editor = new_editor(requestor => $reqr, xact => 1);
349     my $override = $self->api_name =~ /override/;
350
351 # TODO: permission support
352 #        return $editor->event unless
353 #            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
354
355     for my $sunit (@$sunits) {
356         if( $sunit->isdeleted ) {
357             $evt = _delete_sunit( $editor, $override, $sunit );
358         } else {
359             $sunit->default_location( $sunit->default_location->id ) if ref $sunit->default_location;
360
361             if( $sunit->isnew ) {
362                 $evt = _create_sunit( $editor, $sunit );
363             } else {
364                 $evt = _update_sunit( $editor, $override, $sunit );
365             }
366         }
367     }
368
369     if( $evt ) {
370         $logger->info("fleshed sunit-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
371         $editor->rollback;
372         return $evt;
373     }
374     $logger->debug("sunit-alter: done updating sunit batch");
375     $editor->commit;
376     $logger->info("fleshed sunit-alter successfully updated ".scalar(@$sunits)." Units");
377     return 1;
378 }
379
380 sub _delete_sunit {
381     my ($editor, $override, $sunit) = @_;
382     $logger->info("sunit-alter: delete sunit ".OpenSRF::Utils::JSON->perl2JSON($sunit));
383     return $editor->event unless $editor->delete_serial_unit($sunit);
384     return 0;
385 }
386
387 sub _create_sunit {
388     my ($editor, $sunit) = @_;
389
390     $logger->info("sunit-alter: new Unit ".OpenSRF::Utils::JSON->perl2JSON($sunit));
391     return $editor->event unless $editor->create_serial_unit($sunit);
392     return 0;
393 }
394
395 sub _update_sunit {
396     my ($editor, $override, $sunit) = @_;
397
398     $logger->info("sunit-alter: retrieving sunit ".$sunit->id);
399     my $orig_sunit = $editor->retrieve_serial_unit($sunit->id);
400
401     $logger->info("sunit-alter: original sunit ".OpenSRF::Utils::JSON->perl2JSON($orig_sunit));
402     $logger->info("sunit-alter: updated sunit ".OpenSRF::Utils::JSON->perl2JSON($sunit));
403     return $editor->event unless $editor->update_serial_unit($sunit);
404     return 0;
405 }
406
407 __PACKAGE__->register_method(
408         method  => "retrieve_unit_list",
409     authoritative => 1,
410         api_name        => "open-ils.serial.unit_list.retrieve"
411 );
412
413 sub retrieve_unit_list {
414
415         my( $self, $client, @sdist_ids ) = @_;
416
417         if(ref($sdist_ids[0])) { @sdist_ids = @{$sdist_ids[0]}; }
418
419         my $e = new_editor();
420
421     my $query = {
422         'select' => 
423             { 'sunit' => [ 'id', 'summary_contents', 'sort_key' ],
424               'sitem' => ['stream'],
425               'sstr' => ['distribution'],
426               'sdist' => [{'column' => 'label', 'alias' => 'sdist_label'}]
427             },
428         'from' =>
429             { 'sdist' =>
430                 { 'sstr' =>
431                     { 'join' =>
432                         { 'sitem' =>
433                             { 'join' => { 'sunit' => {} } }
434                         }
435                     }
436                 }
437             },
438         'distinct' => 'true',
439         'where' => { '+sdist' => {'id' => \@sdist_ids} },
440         'order_by' => [{'class' => 'sunit', 'field' => 'sort_key'}]
441     };
442
443     my $unit_list_entries = $e->json_query($query);
444     
445     my @entries;
446     foreach my $entry (@$unit_list_entries) {
447         my $value = {'sunit' => $entry->{id}, 'sstr' => $entry->{stream}, 'sdist' => $entry->{distribution}};
448         my $label = $entry->{summary_contents};
449         if (length($label) > 100) {
450             $label = substr($label, 0, 100) . '...'; # limited space in dropdown / menu
451         }
452         $label = "[$entry->{sdist_label}/$entry->{stream} #$entry->{id}] " . $label;
453         push (@entries, [$label, OpenSRF::Utils::JSON->perl2JSON($value)]);
454     }
455
456     return \@entries;
457 }
458
459
460
461 ##########################################################################
462 # predict and receive methods
463 #
464 __PACKAGE__->register_method(
465     method    => 'make_predictions',
466     api_name  => 'open-ils.serial.make_predictions',
467     api_level => 1,
468     argc      => 1,
469     signature => {
470         desc     => 'Receives an ssub id and populates the issuance and item tables',
471         'params' => [ {
472                  name => 'ssub_id',
473                  desc => 'Serial Subscription ID',
474                  type => 'int'
475             }
476         ]
477     }
478 );
479
480 sub make_predictions {
481     my ($self, $conn, $authtoken, $args) = @_;
482
483     my $editor = OpenILS::Utils::CStoreEditor->new();
484     my $ssub_id = $args->{ssub_id};
485     my $mfhd = MFHD->new(MARC::Record->new());
486
487     my $ssub = $editor->retrieve_serial_subscription([$ssub_id]);
488     my $scaps = $editor->search_serial_caption_and_pattern({ subscription => $ssub_id, active => 't'});
489     my $sdists = $editor->search_serial_distribution( [{ subscription => $ssub->id }, {  flesh => 1,
490               flesh_fields => {sdist => [ qw/ streams / ]}, limit => 1 }] ); #TODO: 'deleted' support?
491
492     my @predictions;
493     my $link_id = 1;
494     foreach my $scap (@$scaps) {
495         my $caption_field = _revive_caption($scap);
496         $caption_field->update('8' => $link_id);
497         $mfhd->append_fields($caption_field);
498         my $options = {
499                 'caption' => $caption_field,
500                 'scap_id' => $scap->id,
501                 'num_to_predict' => $args->{num_to_predict}
502                 };
503         if ($args->{base_issuance}) { # predict from a given issuance
504             $options->{predict_from} = _revive_holding($args->{base_issuance}->holding_code, $caption_field, 1); # fresh MFHD Record, so we simply default to 1 for seqno
505         } else { # default to predicting from last published
506             my $last_published = $editor->search_serial_issuance([
507                     {'caption_and_pattern' => $scap->id,
508                     'subscription' => $ssub_id},
509                 {limit => 1, order_by => { siss => "date_published DESC" }}]
510                 );
511             if ($last_published->[0]) {
512                 my $last_siss = $last_published->[0];
513                 $options->{predict_from} = _revive_holding($last_siss->holding_code, $caption_field, 1);
514             } else {
515                 #TODO: throw event (can't predict from nothing!)
516             }
517         }
518         push( @predictions, _generate_issuance_values($mfhd, $options) );
519         $link_id++;
520     }
521
522     my @issuances;
523     foreach my $prediction (@predictions) {
524         my $issuance = new Fieldmapper::serial::issuance;
525         $issuance->isnew(1);
526         $issuance->label($prediction->{label});
527         $issuance->date_published($prediction->{date_published}->strftime('%F'));
528         $issuance->holding_code(OpenSRF::Utils::JSON->perl2JSON($prediction->{holding_code}));
529         $issuance->holding_type($prediction->{holding_type});
530         $issuance->caption_and_pattern($prediction->{caption_and_pattern});
531         $issuance->subscription($ssub->id);
532         push (@issuances, $issuance);
533     }
534
535     fleshed_issuance_alter($self, $conn, $authtoken, \@issuances); # FIXME: catch events
536
537     my @items;
538     for (my $i = 0; $i < @issuances; $i++) {
539         my $date_expected = $predictions[$i]->{date_published}->add(seconds => interval_to_seconds($ssub->expected_date_offset))->strftime('%F');
540         my $issuance = $issuances[$i];
541         #$issuance->label(interval_to_seconds($ssub->expected_date_offset));
542         foreach my $sdist (@$sdists) {
543             my $streams = $sdist->streams;
544             foreach my $stream (@$streams) {
545                 my $item = new Fieldmapper::serial::item;
546                 $item->isnew(1);
547                 $item->stream($stream->id);
548                 $item->date_expected($date_expected);
549                 $item->issuance($issuance->id);
550                 push (@items, $item);
551             }
552         }
553     }
554     fleshed_item_alter($self, $conn, $authtoken, \@items); # FIXME: catch events
555     return \@items;
556 }
557
558 #
559 # _generate_issuance_values() is an initial attempt at a function which can be used
560 # to populate an issuance table with a list of predicted issues.  It accepts
561 # a hash ref of options initially defined as:
562 # caption : the caption field to predict on
563 # num_to_predict : the number of issues you wish to predict
564 # last_rec_date : the date of the last received issue, to be used as an offset
565 #                 for predicting future issues
566 #
567 # The basic method is to first convert to a single holding if compressed, then
568 # increment the holding and save the resulting values to @issuances.
569
570 # returns @issuance_values, an array of hashrefs containing (formatted
571 # label, formatted chronology date, formatted estimated arrival date, and an
572 # array ref of holding subfields as (key, value, key, value ...)) (not a hash
573 # to protect order and possible duplicate keys), and a holding type.
574 #
575 sub _generate_issuance_values {
576     my ($mfhd, $options) = @_;
577     my $caption = $options->{caption};
578     my $scap_id = $options->{scap_id};
579     my $num_to_predict = $options->{num_to_predict};
580     my $predict_from = $options->{predict_from};   # issuance to predict from
581     #my $last_rec_date = $options->{last_rec_date};   # expected or actual
582
583     # TODO: add support for predicting serials with no chronology by passing in
584     # a last_pub_date option?
585
586
587 # Only needed for 'real' MFHD records, not our temp records
588 #    my $link_id = $caption->link_id;
589 #    if(!$predict_from) {
590 #        my $htag = $caption->tag;
591 #        $htag =~ s/^85/86/;
592 #        my @holdings = $mfhd->holdings($htag, $link_id);
593 #        my $last_holding = $holdings[-1];
594 #
595 #        #if ($last_holding->is_compressed) {
596 #        #    $last_holding->compressed_to_last; # convert to last in range
597 #        #}
598 #        $predict_from = $last_holding;
599 #    }
600 #
601
602     $predict_from->notes('public',  []);
603 # add a note marker for system use (?)
604     $predict_from->notes('private', ['AUTOGEN']);
605
606     my $strp = new DateTime::Format::Strptime(pattern => '%F');
607     my $pub_date;
608     my @issuance_values;
609     my @predictions = $mfhd->generate_predictions({'base_holding' => $predict_from, 'num_to_predict' => $num_to_predict});
610     foreach my $prediction (@predictions) {
611         $pub_date = $strp->parse_datetime($prediction->chron_to_date);
612         push(
613                 @issuance_values,
614                 {
615                     #$link_id,
616                     label => $prediction->format,
617                     date_published => $pub_date,
618                     #date_expected => $date_expected->strftime('%F'),
619                     holding_code => [$prediction->indicator(1),$prediction->indicator(2),$prediction->subfields_list],
620                     holding_type => $MFHD_NAMES_BY_TAG{$caption->tag},
621                     caption_and_pattern => $scap_id
622                 }
623             );
624     }
625
626     return @issuance_values;
627 }
628
629 sub _revive_caption {
630     my $scap = shift;
631
632     my $pattern_code = $scap->pattern_code;
633
634     # build MARC::Field
635     my $pattern_parts = OpenSRF::Utils::JSON->JSON2perl($pattern_code);
636     unshift(@$pattern_parts, $MFHD_TAGS_BY_NAME{$scap->type});
637     my $pattern_field = new MARC::Field(@$pattern_parts);
638
639     # build MFHD::Caption
640     return new MFHD::Caption($pattern_field);
641 }
642
643 sub _revive_holding {
644     my $holding_code = shift;
645     my $caption_field = shift;
646     my $seqno = shift;
647
648     # build MARC::Field
649     my $holding_parts = OpenSRF::Utils::JSON->JSON2perl($holding_code);
650     my $captag = $caption_field->tag;
651     $captag =~ s/^85/86/;
652     unshift(@$holding_parts, $captag);
653     my $holding_field = new MARC::Field(@$holding_parts);
654
655     # build MFHD::Holding
656     return new MFHD::Holding($seqno, $holding_field, $caption_field);
657 }
658
659 __PACKAGE__->register_method(
660     method    => 'unitize_items',
661     api_name  => 'open-ils.serial.receive_items',
662     api_level => 1,
663     argc      => 1,
664     signature => {
665         desc     => 'Marks an item as received, updates the shelving unit (creating a new shelving unit if needed), and updates the summaries',
666         'params' => [ {
667                  name => 'items',
668                  desc => 'array of serial items',
669                  type => 'array'
670             }
671         ],
672         'return' => {
673             desc => 'Returns number of received items',
674             type => 'int'
675         }
676     }
677 );
678
679 sub unitize_items {
680     my ($self, $conn, $auth, $items) = @_;
681
682     my( $reqr, $evt ) = $U->checkses($auth);
683     return $evt if $evt;
684     my $editor = new_editor(requestor => $reqr, xact => 1);
685     $self->api_name =~ /serial\.(\w*)_items/;
686     my $mode = $1;
687     
688     my %found_unit_ids;
689     my %found_stream_ids;
690     my %found_types;
691
692     my %stream_ids_by_unit_id;
693
694     my %unit_map;
695     my %sdist_by_unit_id;
696     my %sdist_by_stream_id;
697
698     my $new_unit_id; # id for '-2' units to share
699     foreach my $item (@$items) {
700         # for debugging only, TODO: delete
701         if (!ref $item) { # hopefully we got an id instead
702             $item = $editor->retrieve_serial_item($item);
703         }
704         # get ids
705         my $unit_id = ref($item->unit) ? $item->unit->id : $item->unit;
706         my $stream_id = ref($item->stream) ? $item->stream->id : $item->stream;
707         my $issuance_id = ref($item->issuance) ? $item->issuance->id : $item->issuance;
708         #TODO: evt on any missing ids
709
710         if ($mode eq 'receive') {
711             $item->date_received('now');
712             $item->status('Received');
713         } else {
714             $item->status('Bindery');
715         }
716
717         # check for types to trigger summary updates
718         my $scap;
719         if (!ref $item->issuance) {
720             my $scaps = $editor->search_serial_caption_and_pattern([{"+siss" => {"id" => $issuance_id}}, { "join" => {"siss" => {}} }]);
721             $scap = $scaps->[0];
722         } elsif (!ref $item->issuance->caption_and_pattern) {
723             $scap = $editor->retrieve_serial_caption_and_pattern($item->issuance->caption_and_pattern);
724         } else {
725             $scap = $editor->issuance->caption_and_pattern;
726         }
727         if (!exists($found_types{$stream_id})) {
728             $found_types{$stream_id} = {};
729         }
730         $found_types{$stream_id}->{$scap->type} = 1;
731
732         # create unit if needed
733         if ($unit_id == -1 or (!$new_unit_id and $unit_id == -2)) { # create unit per item
734             my $unit;
735             my $sdists = $editor->search_serial_distribution([{"+sstr" => {"id" => $stream_id}}, { "join" => {"sstr" => {}} }]);
736             $unit = _build_unit($editor, $sdists->[0], $mode);
737             my $evt =  _create_sunit($editor, $unit);
738             return $evt if $evt;
739             if ($unit_id == -2) {
740                 $new_unit_id = $unit->id;
741                 $unit_id = $new_unit_id;
742             } else {
743                 $unit_id = $unit->id;
744             }
745             $item->unit($unit_id);
746             
747             # get unit with 'DEFAULT's and save unit and sdist for later use
748             $unit = $editor->retrieve_serial_unit($unit->id);
749             $unit_map{$unit_id} = $unit;
750             $sdist_by_unit_id{$unit_id} = $sdists->[0];
751             $sdist_by_stream_id{$stream_id} = $sdists->[0];
752         } elsif ($unit_id == -2) { # create one unit for all '-2' items
753             $unit_id = $new_unit_id;
754             $item->unit($unit_id);
755         }
756
757         $found_unit_ids{$unit_id} = 1;
758         $found_stream_ids{$stream_id} = 1;
759
760         # save the stream_id for this unit_id
761         # TODO: prevent items from different streams in same unit? (perhaps in interface)
762         $stream_ids_by_unit_id{$unit_id} = $stream_id;
763
764         my $evt = _update_sitem($editor, undef, $item);
765         return $evt if $evt;
766     }
767
768     # deal with unit level labels
769     foreach my $unit_id (keys %found_unit_ids) {
770
771         # get all the needed issuances for unit
772         my $issuances = $editor->search_serial_issuance([ {"+sitem" => {"unit" => $unit_id, "status" => "Received"}}, {"join" => {"sitem" => {}}, "order_by" => {"siss" => "date_published"}} ]);
773         #TODO: evt on search failure
774
775         my ($mfhd, $formatted_parts) = _summarize_contents($editor, $issuances);
776
777         # special case for single formatted_part (may have summarized version)
778         if (@$formatted_parts == 1) {
779             #TODO: MFHD.pm should have a 'format_summary' method for this
780         }
781
782         # retrieve and update unit contents
783         my $sunit;
784         my $sdist;
785
786         # if we just created the unit, we will already have it and the distribution stored
787         if (exists $unit_map{$unit_id}) {
788             $sunit = $unit_map{$unit_id};
789             $sdist = $sdist_by_unit_id{$unit_id};
790         } else {
791             $sunit = $editor->retrieve_serial_unit($unit_id);
792             $sdist = $editor->search_serial_distribution([{"+sstr" => {"id" => $stream_ids_by_unit_id{$unit_id}}}, { "join" => {"sstr" => {}} }]);
793             $sdist = $sdist->[0];
794         }
795
796         $sunit->detailed_contents($sdist->unit_label_prefix . ' '
797                     . join(', ', @$formatted_parts) . ' '
798                     . $sdist->unit_label_suffix);
799
800         $sunit->summary_contents($sunit->detailed_contents); #TODO: change this when real summary contents are available
801
802         # create sort_key by left padding numbers to 6 digits
803         my $sort_key = $sunit->detailed_contents;
804         $sort_key =~ s/(\d+)/sprintf '%06d', $1/eg; # this may need improvement
805         $sunit->sort_key($sort_key);
806         
807         if ($mode eq 'bind') {
808             $sunit->status(2); # set to 'Bindery' status
809         }
810
811         my $evt = _update_sunit($editor, undef, $sunit);
812         return $evt if $evt;
813     }
814
815     # TODO: cleanup 'dead' units (units which are now emptied of their items)
816
817     if ($mode eq 'receive') { # the summary holdings do not change when binding
818         # deal with stream level summaries
819         # summaries will be built from the "primary" stream only, that is, the stream with the lowest ID per distribution
820         # (TODO: consider direct designation)
821         my %primary_streams_by_sdist;
822         my %streams_by_sdist;
823
824         # see if we have primary streams, and if so, associate them with their distributions
825         foreach my $stream_id (keys %found_stream_ids) {
826             my $sdist;
827             if (exists $sdist_by_stream_id{$stream_id}) {
828                 $sdist = $sdist_by_stream_id{$stream_id};
829             } else {
830                 $sdist = $editor->search_serial_distribution([{"+sstr" => {"id" => $stream_id}}, { "join" => {"sstr" => {}} }]);
831                 $sdist = $sdist->[0];
832             }
833             my $streams;
834             if (!exists($streams_by_sdist{$sdist->id})) {
835                 $streams = $editor->search_serial_stream([{"distribution" => $sdist->id}, {"order_by" => {"sstr" => "id"}}]);
836                 $streams_by_sdist{$sdist->id} = $streams;
837             } else {
838                 $streams = $streams_by_sdist{$sdist->id};
839             }
840             $primary_streams_by_sdist{$sdist->id} = $streams->[0] if ($stream_id == $streams->[0]->id);
841         }
842
843         # retrieve and update summaries for each affected primary stream's distribution
844         foreach my $sdist_id (keys %primary_streams_by_sdist) {
845             my $stream = $primary_streams_by_sdist{$sdist_id};
846             my $stream_id = $stream->id;
847             # get all the needed issuances for stream
848             # FIXME: search in Bindery/Bound/Not Published? as well as Received
849             foreach my $type (keys %{$found_types{$stream_id}}) {
850                 my $issuances = $editor->search_serial_issuance([ {"+sitem" => {"stream" => $stream_id, "status" => "Received"}, "+scap" => {"type" => $type}}, {"join" => {"sitem" => {}, "scap" => {}}, "order_by" => {"siss" => "date_published"}} ]);
851                 #TODO: evt on search failure
852
853                 my ($mfhd, $formatted_parts) = _summarize_contents($editor, $issuances);
854
855                 # retrieve and update the generated_coverage of the summary
856                 my $search_method = "search_serial_${type}_summary";
857                 my $summary = $editor->$search_method([{"distribution" => $sdist_id}]);
858                 $summary = $summary->[0];
859                 $summary->generated_coverage(join(', ', @$formatted_parts));
860                 my $update_method = "update_serial_${type}_summary";
861                 return $editor->event unless $editor->$update_method($summary);
862             }
863         }
864     }
865
866     $editor->commit;
867     return {'num_items_received' => scalar @$items, 'new_unit_id' => $new_unit_id};
868 }
869
870 sub _build_unit {
871     my $editor = shift;
872     my $sdist = shift;
873     my $mode = shift;
874
875     my $attr = $mode . '_unit_template';
876     my $template = $editor->retrieve_asset_copy_template($sdist->$attr);
877
878     my @parts = qw( status location loan_duration fine_level age_protect circulate deposit ref holdable deposit_amount price circ_modifier circ_as_type alert_message opac_visible floating mint_condition );
879
880     my $unit = new Fieldmapper::serial::unit;
881     foreach my $part (@parts) {
882         my $value = $template->$part;
883         next if !defined($value);
884         $unit->$part($value);
885     }
886
887     # ignore circ_lib in template, set to distribution holding_lib
888     $unit->circ_lib($sdist->holding_lib);
889     $unit->creator($editor->requestor->id);
890     $unit->editor($editor->requestor->id);
891     $attr = $mode . '_call_number';
892     $unit->call_number($sdist->$attr);
893     $unit->barcode('AUTO');
894     $unit->sort_key('');
895     $unit->summary_contents('');
896     $unit->detailed_contents('');
897
898     return $unit;
899 }
900
901
902 sub _summarize_contents {
903     my $editor = shift;
904     my $issuances = shift;
905
906     # create MFHD record
907     my $mfhd = MFHD->new(MARC::Record->new());
908     my %scaps;
909     my %scap_fields;
910     my @scap_fields_ordered;
911     my $seqno = 1;
912     my $link_id = 1;
913     foreach my $issuance (@$issuances) {
914         my $scap_id = $issuance->caption_and_pattern;
915         next if (!$scap_id); # skip issuances with no caption/pattern
916
917         my $scap;
918         my $scap_field;
919         # if this is the first appearance of this scap, retrieve it and add it to the temporary record
920         if (!exists $scaps{$issuance->caption_and_pattern}) {
921             $scaps{$scap_id} = $editor->retrieve_serial_caption_and_pattern($scap_id);
922             $scap = $scaps{$scap_id};
923             $scap_field = _revive_caption($scap);
924             $scap_fields{$scap_id} = $scap_field;
925             push(@scap_fields_ordered, $scap_field);
926             $scap_field->update('8' => $link_id);
927             $mfhd->append_fields($scap_field);
928             $link_id++;
929         } else {
930             $scap = $scaps{$scap_id};
931             $scap_field = $scap_fields{$scap_id};
932         }
933
934         $mfhd->append_fields(_revive_holding($issuance->holding_code, $scap_field, $seqno));
935         $seqno++;
936     }
937
938     my @formatted_parts;
939     foreach my $scap_field (@scap_fields_ordered) { #TODO: use generic MFHD "summarize" method, once available
940        my @updated_holdings = $mfhd->get_compressed_holdings($scap_field);
941        foreach my $holding (@updated_holdings) {
942            push(@formatted_parts, $holding->format);
943        }
944     }
945
946     return ($mfhd, \@formatted_parts);
947 }
948
949 ##########################################################################
950 # note methods
951 #
952 __PACKAGE__->register_method(
953     method      => 'fetch_notes',
954     api_name        => 'open-ils.serial.item_note.retrieve.all',
955     signature   => q/
956         Returns an array of copy note objects.  
957         @param args A named hash of parameters including:
958             authtoken   : Required if viewing non-public notes
959             item_id      : The id of the item whose notes we want to retrieve
960             pub         : True if all the caller wants are public notes
961         @return An array of note objects
962     /
963 );
964
965 __PACKAGE__->register_method(
966     method      => 'fetch_notes',
967     api_name        => 'open-ils.serial.subscription_note.retrieve.all',
968     signature   => q/
969         Returns an array of copy note objects.  
970         @param args A named hash of parameters including:
971             authtoken       : Required if viewing non-public notes
972             subscription_id : The id of the item whose notes we want to retrieve
973             pub             : True if all the caller wants are public notes
974         @return An array of note objects
975     /
976 );
977
978 __PACKAGE__->register_method(
979     method      => 'fetch_notes',
980     api_name        => 'open-ils.serial.distribution_note.retrieve.all',
981     signature   => q/
982         Returns an array of copy note objects.  
983         @param args A named hash of parameters including:
984             authtoken       : Required if viewing non-public notes
985             distribution_id : The id of the item whose notes we want to retrieve
986             pub             : True if all the caller wants are public notes
987         @return An array of note objects
988     /
989 );
990
991 # TODO: revisit this method to consider replacing cstore direct calls
992 sub fetch_notes {
993     my( $self, $connection, $args ) = @_;
994     
995     $self->api_name =~ /serial\.(\w*)_note/;
996     my $type = $1;
997
998     my $id = $$args{object_id};
999     my $authtoken = $$args{authtoken};
1000     my( $r, $evt);
1001
1002     if( $$args{pub} ) {
1003         return $U->cstorereq(
1004             'open-ils.cstore.direct.serial.'.$type.'_note.search.atomic',
1005             { $type => $id, pub => 't' } );
1006     } else {
1007         # FIXME: restore perm check
1008         # ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_COPY_NOTES');
1009         # return $evt if $evt;
1010         return $U->cstorereq(
1011             'open-ils.cstore.direct.serial.'.$type.'_note.search.atomic', {$type => $id} );
1012     }
1013
1014     return undef;
1015 }
1016
1017 __PACKAGE__->register_method(
1018     method      => 'create_note',
1019     api_name        => 'open-ils.serial.item_note.create',
1020     signature   => q/
1021         Creates a new item note
1022         @param authtoken The login session key
1023         @param note The note object to create
1024         @return The id of the new note object
1025     /
1026 );
1027
1028 __PACKAGE__->register_method(
1029     method      => 'create_note',
1030     api_name        => 'open-ils.serial.subscription_note.create',
1031     signature   => q/
1032         Creates a new subscription note
1033         @param authtoken The login session key
1034         @param note The note object to create
1035         @return The id of the new note object
1036     /
1037 );
1038
1039 __PACKAGE__->register_method(
1040     method      => 'create_note',
1041     api_name        => 'open-ils.serial.distribution_note.create',
1042     signature   => q/
1043         Creates a new distribution note
1044         @param authtoken The login session key
1045         @param note The note object to create
1046         @return The id of the new note object
1047     /
1048 );
1049
1050 sub create_note {
1051     my( $self, $connection, $authtoken, $note ) = @_;
1052
1053     $self->api_name =~ /serial\.(\w*)_note/;
1054     my $type = $1;
1055
1056     my $e = new_editor(xact=>1, authtoken=>$authtoken);
1057     return $e->event unless $e->checkauth;
1058
1059     # FIXME: restore permission support
1060 #    my $item = $e->retrieve_serial_item(
1061 #        [
1062 #            $note->item
1063 #        ]
1064 #    );
1065 #
1066 #    return $e->event unless
1067 #        $e->allowed('CREATE_COPY_NOTE', $item->call_number->owning_lib);
1068
1069     $note->create_date('now');
1070     $note->creator($e->requestor->id);
1071     $note->pub( ($U->is_true($note->pub)) ? 't' : 'f' );
1072     $note->clear_id;
1073
1074     my $method = "create_serial_${type}_note";
1075     $e->$method($note) or return $e->event;
1076     $e->commit;
1077     return $note->id;
1078 }
1079
1080 __PACKAGE__->register_method(
1081     method      => 'delete_note',
1082     api_name        =>  'open-ils.serial.item_note.delete',
1083     signature   => q/
1084         Deletes an existing item note
1085         @param authtoken The login session key
1086         @param noteid The id of the note to delete
1087         @return 1 on success - Event otherwise.
1088         /
1089 );
1090
1091 __PACKAGE__->register_method(
1092     method      => 'delete_note',
1093     api_name        =>  'open-ils.serial.subscription_note.delete',
1094     signature   => q/
1095         Deletes an existing subscription note
1096         @param authtoken The login session key
1097         @param noteid The id of the note to delete
1098         @return 1 on success - Event otherwise.
1099         /
1100 );
1101
1102 __PACKAGE__->register_method(
1103     method      => 'delete_note',
1104     api_name        =>  'open-ils.serial.distribution_note.delete',
1105     signature   => q/
1106         Deletes an existing distribution note
1107         @param authtoken The login session key
1108         @param noteid The id of the note to delete
1109         @return 1 on success - Event otherwise.
1110         /
1111 );
1112
1113 sub delete_note {
1114     my( $self, $conn, $authtoken, $noteid ) = @_;
1115
1116     $self->api_name =~ /serial\.(\w*)_note/;
1117     my $type = $1;
1118
1119     my $e = new_editor(xact=>1, authtoken=>$authtoken);
1120     return $e->die_event unless $e->checkauth;
1121
1122     my $method = "retrieve_serial_${type}_note";
1123     my $note = $e->$method([
1124         $noteid,
1125     ]) or return $e->die_event;
1126
1127 # FIXME: restore permissions check
1128 #    if( $note->creator ne $e->requestor->id ) {
1129 #        return $e->die_event unless
1130 #            $e->allowed('DELETE_COPY_NOTE', $note->item->call_number->owning_lib);
1131 #    }
1132
1133     $method = "delete_serial_${type}_note";
1134     $e->$method($note) or return $e->die_event;
1135     $e->commit;
1136     return 1;
1137 }
1138
1139
1140 ##########################################################################
1141 # subscription methods
1142 #
1143 __PACKAGE__->register_method(
1144     method    => 'fleshed_ssub_alter',
1145     api_name  => 'open-ils.serial.subscription.fleshed.batch.update',
1146     api_level => 1,
1147     argc      => 2,
1148     signature => {
1149         desc     => 'Receives an array of one or more subscriptions and updates the database as needed',
1150         'params' => [ {
1151                  name => 'authtoken',
1152                  desc => 'Authtoken for current user session',
1153                  type => 'string'
1154             },
1155             {
1156                  name => 'subscriptions',
1157                  desc => 'Array of fleshed subscriptions',
1158                  type => 'array'
1159             }
1160
1161         ],
1162         'return' => {
1163             desc => 'Returns 1 if successful, event if failed',
1164             type => 'mixed'
1165         }
1166     }
1167 );
1168
1169 sub fleshed_ssub_alter {
1170     my( $self, $conn, $auth, $ssubs ) = @_;
1171     return 1 unless ref $ssubs;
1172     my( $reqr, $evt ) = $U->checkses($auth);
1173     return $evt if $evt;
1174     my $editor = new_editor(requestor => $reqr, xact => 1);
1175     my $override = $self->api_name =~ /override/;
1176
1177 # TODO: permission check
1178 #        return $editor->event unless
1179 #            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
1180
1181     for my $ssub (@$ssubs) {
1182
1183         my $ssubid = $ssub->id;
1184
1185         if( $ssub->isdeleted ) {
1186             $evt = _delete_ssub( $editor, $override, $ssub);
1187         } elsif( $ssub->isnew ) {
1188             _cleanse_dates($ssub, ['start_date','end_date']);
1189             $evt = _create_ssub( $editor, $ssub );
1190         } else {
1191             _cleanse_dates($ssub, ['start_date','end_date']);
1192             $evt = _update_ssub( $editor, $override, $ssub );
1193         }
1194     }
1195
1196     if( $evt ) {
1197         $logger->info("fleshed subscription-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
1198         $editor->rollback;
1199         return $evt;
1200     }
1201     $logger->debug("subscription-alter: done updating subscription batch");
1202     $editor->commit;
1203     $logger->info("fleshed subscription-alter successfully updated ".scalar(@$ssubs)." subscriptions");
1204     return 1;
1205 }
1206
1207 sub _delete_ssub {
1208     my ($editor, $override, $ssub) = @_;
1209     $logger->info("subscription-alter: delete subscription ".OpenSRF::Utils::JSON->perl2JSON($ssub));
1210     my $sdists = $editor->search_serial_distribution(
1211             { subscription => $ssub->id }, { limit => 1 } ); #TODO: 'deleted' support?
1212     my $cps = $editor->search_serial_caption_and_pattern(
1213             { subscription => $ssub->id }, { limit => 1 } ); #TODO: 'deleted' support?
1214     my $sisses = $editor->search_serial_issuance(
1215             { subscription => $ssub->id }, { limit => 1 } ); #TODO: 'deleted' support?
1216     return OpenILS::Event->new(
1217             'SERIAL_SUBSCRIPTION_NOT_EMPTY', payload => $ssub->id ) if (@$sdists or @$cps or @$sisses);
1218
1219     return $editor->event unless $editor->delete_serial_subscription($ssub);
1220     return 0;
1221 }
1222
1223 sub _create_ssub {
1224     my ($editor, $ssub) = @_;
1225
1226     $logger->info("subscription-alter: new subscription ".OpenSRF::Utils::JSON->perl2JSON($ssub));
1227     return $editor->event unless $editor->create_serial_subscription($ssub);
1228     return 0;
1229 }
1230
1231 sub _update_ssub {
1232     my ($editor, $override, $ssub) = @_;
1233
1234     $logger->info("subscription-alter: retrieving subscription ".$ssub->id);
1235     my $orig_ssub = $editor->retrieve_serial_subscription($ssub->id);
1236
1237     $logger->info("subscription-alter: original subscription ".OpenSRF::Utils::JSON->perl2JSON($orig_ssub));
1238     $logger->info("subscription-alter: updated subscription ".OpenSRF::Utils::JSON->perl2JSON($ssub));
1239     return $editor->event unless $editor->update_serial_subscription($ssub);
1240     return 0;
1241 }
1242
1243 __PACKAGE__->register_method(
1244     method  => "fleshed_serial_subscription_retrieve_batch",
1245     authoritative => 1,
1246     api_name    => "open-ils.serial.subscription.fleshed.batch.retrieve"
1247 );
1248
1249 sub fleshed_serial_subscription_retrieve_batch {
1250     my( $self, $client, $ids ) = @_;
1251 # FIXME: permissions?
1252     $logger->info("Fetching fleshed subscriptions @$ids");
1253     return $U->cstorereq(
1254         "open-ils.cstore.direct.serial.subscription.search.atomic",
1255         { id => $ids },
1256         { flesh => 1,
1257           flesh_fields => {ssub => [ qw/owning_lib notes/ ]}
1258         });
1259 }
1260
1261 __PACKAGE__->register_method(
1262         method  => "retrieve_sub_tree",
1263     authoritative => 1,
1264         api_name        => "open-ils.serial.subscription_tree.retrieve"
1265 );
1266
1267 __PACKAGE__->register_method(
1268         method  => "retrieve_sub_tree",
1269         api_name        => "open-ils.serial.subscription_tree.global.retrieve"
1270 );
1271
1272 sub retrieve_sub_tree {
1273
1274         my( $self, $client, $user_session, $docid, @org_ids ) = @_;
1275
1276         if(ref($org_ids[0])) { @org_ids = @{$org_ids[0]}; }
1277
1278         $docid = "$docid";
1279
1280         # TODO: permission support
1281         if(!@org_ids and $user_session) {
1282                 my $user_obj = 
1283                         OpenILS::Application::AppUtils->check_user_session( $user_session ); #throws EX on error
1284                         @org_ids = ($user_obj->home_ou);
1285         }
1286
1287         if( $self->api_name =~ /global/ ) {
1288                 return _build_subs_list( { record_entry => $docid } ); # TODO: filter for !deleted, or active?
1289
1290         } else {
1291
1292                 my @all_subs;
1293                 for my $orgid (@org_ids) {
1294                         my $subs = _build_subs_list( 
1295                                         { record_entry => $docid, owning_lib => $orgid } );# TODO: filter for !deleted, or active?
1296                         push( @all_subs, @$subs );
1297                 }
1298                 
1299                 return \@all_subs;
1300         }
1301
1302         return undef;
1303 }
1304
1305 sub _build_subs_list {
1306         my $search_hash = shift;
1307
1308         #$search_hash->{deleted} = 'f';
1309         my $e = new_editor();
1310
1311         my $subs = $e->search_serial_subscription([$search_hash, { 'order_by' => {'ssub' => 'id'} }]);
1312
1313         my @built_subs;
1314
1315         for my $sub (@$subs) {
1316
1317         # TODO: filter on !deleted?
1318                 my $dists = $e->search_serial_distribution(
1319             [{ subscription => $sub->id }, { 'order_by' => {'sdist' => 'label'} }]
1320             );
1321
1322                 #$dists = [ sort { $a->label cmp $b->label } @$dists  ];
1323
1324                 $sub->distributions($dists);
1325         
1326         # TODO: filter on !deleted?
1327                 my $issuances = $e->search_serial_issuance(
1328                         [{ subscription => $sub->id }, { 'order_by' => {'siss' => 'label'} }]
1329             );
1330
1331                 #$issuances = [ sort { $a->label cmp $b->label } @$issuances  ];
1332                 $sub->issuances($issuances);
1333
1334         # TODO: filter on !deleted?
1335                 my $scaps = $e->search_serial_caption_and_pattern(
1336                         [{ subscription => $sub->id }, { 'order_by' => {'scap' => 'id'} }]
1337             );
1338
1339                 #$scaps = [ sort { $a->id cmp $b->id } @$scaps  ];
1340                 $sub->scaps($scaps);
1341                 push( @built_subs, $sub );
1342         }
1343
1344         return \@built_subs;
1345
1346 }
1347
1348 __PACKAGE__->register_method(
1349     method  => "subscription_orgs_for_title",
1350     authoritative => 1,
1351     api_name    => "open-ils.serial.subscription.retrieve_orgs_by_title"
1352 );
1353
1354 sub subscription_orgs_for_title {
1355     my( $self, $client, $record_id ) = @_;
1356
1357     my $subs = $U->simple_scalar_request(
1358         "open-ils.cstore",
1359         "open-ils.cstore.direct.serial.subscription.search.atomic",
1360         { record_entry => $record_id }); # TODO: filter on !deleted?
1361
1362     my $orgs = { map {$_->owning_lib => 1 } @$subs };
1363     return [ keys %$orgs ];
1364 }
1365
1366
1367 ##########################################################################
1368 # distribution methods
1369 #
1370 __PACKAGE__->register_method(
1371     method    => 'fleshed_sdist_alter',
1372     api_name  => 'open-ils.serial.distribution.fleshed.batch.update',
1373     api_level => 1,
1374     argc      => 2,
1375     signature => {
1376         desc     => 'Receives an array of one or more distributions and updates the database as needed',
1377         'params' => [ {
1378                  name => 'authtoken',
1379                  desc => 'Authtoken for current user session',
1380                  type => 'string'
1381             },
1382             {
1383                  name => 'distributions',
1384                  desc => 'Array of fleshed distributions',
1385                  type => 'array'
1386             }
1387
1388         ],
1389         'return' => {
1390             desc => 'Returns 1 if successful, event if failed',
1391             type => 'mixed'
1392         }
1393     }
1394 );
1395
1396 sub fleshed_sdist_alter {
1397     my( $self, $conn, $auth, $sdists ) = @_;
1398     return 1 unless ref $sdists;
1399     my( $reqr, $evt ) = $U->checkses($auth);
1400     return $evt if $evt;
1401     my $editor = new_editor(requestor => $reqr, xact => 1);
1402     my $override = $self->api_name =~ /override/;
1403
1404 # TODO: permission check
1405 #        return $editor->event unless
1406 #            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
1407
1408     for my $sdist (@$sdists) {
1409         my $sdistid = $sdist->id;
1410
1411         if( $sdist->isdeleted ) {
1412             $evt = _delete_sdist( $editor, $override, $sdist);
1413         } elsif( $sdist->isnew ) {
1414             $evt = _create_sdist( $editor, $sdist );
1415         } else {
1416             $evt = _update_sdist( $editor, $override, $sdist );
1417         }
1418     }
1419
1420     if( $evt ) {
1421         $logger->info("fleshed distribution-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
1422         $editor->rollback;
1423         return $evt;
1424     }
1425     $logger->debug("distribution-alter: done updating distribution batch");
1426     $editor->commit;
1427     $logger->info("fleshed distribution-alter successfully updated ".scalar(@$sdists)." distributions");
1428     return 1;
1429 }
1430
1431 sub _delete_sdist {
1432     my ($editor, $override, $sdist) = @_;
1433     $logger->info("distribution-alter: delete distribution ".OpenSRF::Utils::JSON->perl2JSON($sdist));
1434     return $editor->event unless $editor->delete_serial_distribution($sdist);
1435     return 0;
1436 }
1437
1438 sub _create_sdist {
1439     my ($editor, $sdist) = @_;
1440
1441     $logger->info("distribution-alter: new distribution ".OpenSRF::Utils::JSON->perl2JSON($sdist));
1442     return $editor->event unless $editor->create_serial_distribution($sdist);
1443
1444     # create summaries too
1445     my $summary = new Fieldmapper::serial::basic_summary;
1446     $summary->distribution($sdist->id);
1447     $summary->generated_coverage('');
1448     return $editor->event unless $editor->create_serial_basic_summary($summary);
1449     $summary = new Fieldmapper::serial::supplement_summary;
1450     $summary->distribution($sdist->id);
1451     $summary->generated_coverage('');
1452     return $editor->event unless $editor->create_serial_supplement_summary($summary);
1453     $summary = new Fieldmapper::serial::index_summary;
1454     $summary->distribution($sdist->id);
1455     $summary->generated_coverage('');
1456     return $editor->event unless $editor->create_serial_index_summary($summary);
1457
1458     # create a starter stream (TODO: reconsider this)
1459     my $stream = new Fieldmapper::serial::stream;
1460     $stream->distribution($sdist->id);
1461     return $editor->event unless $editor->create_serial_stream($stream);
1462
1463     return 0;
1464 }
1465
1466 sub _update_sdist {
1467     my ($editor, $override, $sdist) = @_;
1468
1469     $logger->info("distribution-alter: retrieving distribution ".$sdist->id);
1470     my $orig_sdist = $editor->retrieve_serial_distribution($sdist->id);
1471
1472     $logger->info("distribution-alter: original distribution ".OpenSRF::Utils::JSON->perl2JSON($orig_sdist));
1473     $logger->info("distribution-alter: updated distribution ".OpenSRF::Utils::JSON->perl2JSON($sdist));
1474     return $editor->event unless $editor->update_serial_distribution($sdist);
1475     return 0;
1476 }
1477
1478 __PACKAGE__->register_method(
1479     method  => "fleshed_serial_distribution_retrieve_batch",
1480     authoritative => 1,
1481     api_name    => "open-ils.serial.distribution.fleshed.batch.retrieve"
1482 );
1483
1484 sub fleshed_serial_distribution_retrieve_batch {
1485     my( $self, $client, $ids ) = @_;
1486 # FIXME: permissions?
1487     $logger->info("Fetching fleshed distributions @$ids");
1488     return $U->cstorereq(
1489         "open-ils.cstore.direct.serial.distribution.search.atomic",
1490         { id => $ids },
1491         { flesh => 1,
1492           flesh_fields => {sdist => [ qw/ holding_lib receive_call_number receive_unit_template bind_call_number bind_unit_template streams / ]}
1493         });
1494 }
1495
1496 ##########################################################################
1497 # caption and pattern methods
1498 #
1499 __PACKAGE__->register_method(
1500     method    => 'scap_alter',
1501     api_name  => 'open-ils.serial.caption_and_pattern.batch.update',
1502     api_level => 1,
1503     argc      => 2,
1504     signature => {
1505         desc     => 'Receives an array of one or more caption and patterns and updates the database as needed',
1506         'params' => [ {
1507                  name => 'authtoken',
1508                  desc => 'Authtoken for current user session',
1509                  type => 'string'
1510             },
1511             {
1512                  name => 'scaps',
1513                  desc => 'Array of caption and patterns',
1514                  type => 'array'
1515             }
1516
1517         ],
1518         'return' => {
1519             desc => 'Returns 1 if successful, event if failed',
1520             type => 'mixed'
1521         }
1522     }
1523 );
1524
1525 sub scap_alter {
1526     my( $self, $conn, $auth, $scaps ) = @_;
1527     return 1 unless ref $scaps;
1528     my( $reqr, $evt ) = $U->checkses($auth);
1529     return $evt if $evt;
1530     my $editor = new_editor(requestor => $reqr, xact => 1);
1531     my $override = $self->api_name =~ /override/;
1532
1533 # TODO: permission check
1534 #        return $editor->event unless
1535 #            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
1536
1537     for my $scap (@$scaps) {
1538         my $scapid = $scap->id;
1539
1540         if( $scap->isdeleted ) {
1541             $evt = _delete_scap( $editor, $override, $scap);
1542         } elsif( $scap->isnew ) {
1543             $evt = _create_scap( $editor, $scap );
1544         } else {
1545             $evt = _update_scap( $editor, $override, $scap );
1546         }
1547     }
1548
1549     if( $evt ) {
1550         $logger->info("caption_and_pattern-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
1551         $editor->rollback;
1552         return $evt;
1553     }
1554     $logger->debug("caption_and_pattern-alter: done updating caption_and_pattern batch");
1555     $editor->commit;
1556     $logger->info("caption_and_pattern-alter successfully updated ".scalar(@$scaps)." caption_and_patterns");
1557     return 1;
1558 }
1559
1560 sub _delete_scap {
1561     my ($editor, $override, $scap) = @_;
1562     $logger->info("caption_and_pattern-alter: delete caption_and_pattern ".OpenSRF::Utils::JSON->perl2JSON($scap));
1563     my $sisses = $editor->search_serial_issuance(
1564             { caption_and_pattern => $scap->id }, { limit => 1 } ); #TODO: 'deleted' support?
1565     return OpenILS::Event->new(
1566             'SERIAL_CAPTION_AND_PATTERN_HAS_ISSUANCES', payload => $scap->id ) if (@$sisses);
1567
1568     return $editor->event unless $editor->delete_serial_caption_and_pattern($scap);
1569     return 0;
1570 }
1571
1572 sub _create_scap {
1573     my ($editor, $scap) = @_;
1574
1575     $logger->info("caption_and_pattern-alter: new caption_and_pattern ".OpenSRF::Utils::JSON->perl2JSON($scap));
1576     return $editor->event unless $editor->create_serial_caption_and_pattern($scap);
1577     return 0;
1578 }
1579
1580 sub _update_scap {
1581     my ($editor, $override, $scap) = @_;
1582
1583     $logger->info("caption_and_pattern-alter: retrieving caption_and_pattern ".$scap->id);
1584     my $orig_scap = $editor->retrieve_serial_caption_and_pattern($scap->id);
1585
1586     $logger->info("caption_and_pattern-alter: original caption_and_pattern ".OpenSRF::Utils::JSON->perl2JSON($orig_scap));
1587     $logger->info("caption_and_pattern-alter: updated caption_and_pattern ".OpenSRF::Utils::JSON->perl2JSON($scap));
1588     return $editor->event unless $editor->update_serial_caption_and_pattern($scap);
1589     return 0;
1590 }
1591
1592 __PACKAGE__->register_method(
1593     method  => "serial_caption_and_pattern_retrieve_batch",
1594     authoritative => 1,
1595     api_name    => "open-ils.serial.caption_and_pattern.batch.retrieve"
1596 );
1597
1598 sub serial_caption_and_pattern_retrieve_batch {
1599     my( $self, $client, $ids ) = @_;
1600     $logger->info("Fetching caption_and_patterns @$ids");
1601     return $U->cstorereq(
1602         "open-ils.cstore.direct.serial.caption_and_pattern.search.atomic",
1603         { id => $ids }
1604     );
1605 }
1606
1607 1;