]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/OpenILS/Application/Serial.pm
add "type" (siss.holding_type) and "status" (sitem.status) filters; renaming the...
[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
41 use OpenILS::Application;
42 use base qw/OpenILS::Application/;
43 use OpenILS::Application::AppUtils;
44 use OpenILS::Event;
45 use OpenSRF::AppSession;
46 use OpenSRF::Utils qw/:datetime/;
47 use OpenSRF::Utils::Logger qw/:logger/;
48 use OpenILS::Utils::CStoreEditor q/:funcs/;
49 use OpenILS::Utils::Fieldmapper;
50 use OpenILS::Utils::MFHD;
51 use MARC::File::XML (BinaryEncoding => 'utf8');
52 my $U = 'OpenILS::Application::AppUtils';
53 my @MFHD_NAMES = ('basic','supplement','index');
54 my %MFHD_NAMES_BY_TAG = (  '853' => $MFHD_NAMES[0],
55                         '863' => $MFHD_NAMES[0],
56                         '854' => $MFHD_NAMES[1],
57                         '864' => $MFHD_NAMES[1],
58                         '855' => $MFHD_NAMES[2],
59                         '865' => $MFHD_NAMES[2] );
60 my %MFHD_TAGS_BY_NAME = (  $MFHD_NAMES[0] => '853',
61                         $MFHD_NAMES[1] => '854',
62                         $MFHD_NAMES[2] => '855');
63
64 # helper method for conforming dates to ISO8601
65 sub _cleanse_dates {
66     my $item = shift;
67     my $fields = shift;
68
69     foreach my $field (@$fields) {
70         $item->$field(OpenSRF::Utils::clense_ISO8601($item->$field)) if $item->$field;
71     }
72     return 0;
73 }
74
75 sub _get_mvr {
76     $U->simplereq(
77         "open-ils.search",
78         "open-ils.search.biblio.record.mods_slim.retrieve",
79         @_
80     );
81 }
82
83
84 ##########################################################################
85 # item methods
86 #
87 __PACKAGE__->register_method(
88     method    => 'fleshed_item_alter',
89     api_name  => 'open-ils.serial.item.fleshed.batch.update',
90     api_level => 1,
91     argc      => 2,
92     signature => {
93         desc     => 'Receives an array of one or more items and updates the database as needed',
94         'params' => [ {
95                  name => 'authtoken',
96                  desc => 'Authtoken for current user session',
97                  type => 'string'
98             },
99             {
100                  name => 'items',
101                  desc => 'Array of fleshed items',
102                  type => 'array'
103             }
104
105         ],
106         'return' => {
107             desc => 'Returns 1 if successful, event if failed',
108             type => 'mixed'
109         }
110     }
111 );
112
113 sub fleshed_item_alter {
114     my( $self, $conn, $auth, $items ) = @_;
115     return 1 unless ref $items;
116     my( $reqr, $evt ) = $U->checkses($auth);
117     return $evt if $evt;
118     my $editor = new_editor(requestor => $reqr, xact => 1);
119     my $override = $self->api_name =~ /override/;
120
121 # TODO: permission check
122 #        return $editor->event unless
123 #            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
124
125     for my $item (@$items) {
126
127         my $itemid = $item->id;
128         $item->editor($editor->requestor->id);
129         $item->edit_date('now');
130
131         if( $item->isdeleted ) {
132             $evt = _delete_sitem( $editor, $override, $item);
133         } elsif( $item->isnew ) {
134             # TODO: reconsider this
135             # if the item has a new issuance, create the issuance first
136             if (ref $item->issuance eq 'Fieldmapper::serial::issuance' and $item->issuance->isnew) {
137                 fleshed_issuance_alter($self, $conn, $auth, [$item->issuance]);
138             }
139             _cleanse_dates($item, ['date_expected','date_received']);
140             $evt = _create_sitem( $editor, $item );
141         } else {
142             _cleanse_dates($item, ['date_expected','date_received']);
143             $evt = _update_sitem( $editor, $override, $item );
144         }
145     }
146
147     if( $evt ) {
148         $logger->info("fleshed item-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
149         $editor->rollback;
150         return $evt;
151     }
152     $logger->debug("item-alter: done updating item batch");
153     $editor->commit;
154     $logger->info("fleshed item-alter successfully updated ".scalar(@$items)." items");
155     return 1;
156 }
157
158 sub _delete_sitem {
159     my ($editor, $override, $item) = @_;
160     $logger->info("item-alter: delete item ".OpenSRF::Utils::JSON->perl2JSON($item));
161     return $editor->event unless $editor->delete_serial_item($item);
162     return 0;
163 }
164
165 sub _create_sitem {
166     my ($editor, $item) = @_;
167
168     $item->creator($editor->requestor->id);
169     $item->create_date('now');
170
171     $logger->info("item-alter: new item ".OpenSRF::Utils::JSON->perl2JSON($item));
172     return $editor->event unless $editor->create_serial_item($item);
173     return 0;
174 }
175
176 sub _update_sitem {
177     my ($editor, $override, $item) = @_;
178
179     $logger->info("item-alter: retrieving item ".$item->id);
180     my $orig_item = $editor->retrieve_serial_item($item->id);
181
182     $logger->info("item-alter: original item ".OpenSRF::Utils::JSON->perl2JSON($orig_item));
183     $logger->info("item-alter: updated item ".OpenSRF::Utils::JSON->perl2JSON($item));
184     return $editor->event unless $editor->update_serial_item($item);
185     return 0;
186 }
187
188 __PACKAGE__->register_method(
189     method  => "fleshed_serial_item_retrieve_batch",
190     authoritative => 1,
191     api_name    => "open-ils.serial.item.fleshed.batch.retrieve"
192 );
193
194 sub fleshed_serial_item_retrieve_batch {
195     my( $self, $client, $ids ) = @_;
196 # FIXME: permissions?
197     $logger->info("Fetching fleshed serial items @$ids");
198     return $U->cstorereq(
199         "open-ils.cstore.direct.serial.item.search.atomic",
200         { id => $ids },
201         { flesh => 2,
202           flesh_fields => {sitem => [ qw/issuance creator editor stream unit notes/ ], sstr => ["distribution"], sunit => ["call_number"], siss => [qw/creator editor subscription/]}
203         });
204 }
205
206
207 ##########################################################################
208 # issuance methods
209 #
210 __PACKAGE__->register_method(
211     method    => 'fleshed_issuance_alter',
212     api_name  => 'open-ils.serial.issuance.fleshed.batch.update',
213     api_level => 1,
214     argc      => 2,
215     signature => {
216         desc     => 'Receives an array of one or more issuances and updates the database as needed',
217         'params' => [ {
218                  name => 'authtoken',
219                  desc => 'Authtoken for current user session',
220                  type => 'string'
221             },
222             {
223                  name => 'issuances',
224                  desc => 'Array of fleshed issuances',
225                  type => 'array'
226             }
227
228         ],
229         'return' => {
230             desc => 'Returns 1 if successful, event if failed',
231             type => 'mixed'
232         }
233     }
234 );
235
236 sub fleshed_issuance_alter {
237     my( $self, $conn, $auth, $issuances ) = @_;
238     return 1 unless ref $issuances;
239     my( $reqr, $evt ) = $U->checkses($auth);
240     return $evt if $evt;
241     my $editor = new_editor(requestor => $reqr, xact => 1);
242     my $override = $self->api_name =~ /override/;
243
244 # TODO: permission support
245 #        return $editor->event unless
246 #            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
247
248     for my $issuance (@$issuances) {
249         my $issuanceid = $issuance->id;
250         $issuance->editor($editor->requestor->id);
251         $issuance->edit_date('now');
252
253         if( $issuance->isdeleted ) {
254             $evt = _delete_siss( $editor, $override, $issuance);
255         } elsif( $issuance->isnew ) {
256             _cleanse_dates($issuance, ['date_published']);
257             $evt = _create_siss( $editor, $issuance );
258         } else {
259             _cleanse_dates($issuance, ['date_published']);
260             $evt = _update_siss( $editor, $override, $issuance );
261         }
262     }
263
264     if( $evt ) {
265         $logger->info("fleshed issuance-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
266         $editor->rollback;
267         return $evt;
268     }
269     $logger->debug("issuance-alter: done updating issuance batch");
270     $editor->commit;
271     $logger->info("fleshed issuance-alter successfully updated ".scalar(@$issuances)." issuances");
272     return 1;
273 }
274
275 sub _delete_siss {
276     my ($editor, $override, $issuance) = @_;
277     $logger->info("issuance-alter: delete issuance ".OpenSRF::Utils::JSON->perl2JSON($issuance));
278     return $editor->event unless $editor->delete_serial_issuance($issuance);
279     return 0;
280 }
281
282 sub _create_siss {
283     my ($editor, $issuance) = @_;
284
285     $issuance->creator($editor->requestor->id);
286     $issuance->create_date('now');
287
288     $logger->info("issuance-alter: new issuance ".OpenSRF::Utils::JSON->perl2JSON($issuance));
289     return $editor->event unless $editor->create_serial_issuance($issuance);
290     return 0;
291 }
292
293 sub _update_siss {
294     my ($editor, $override, $issuance) = @_;
295
296     $logger->info("issuance-alter: retrieving issuance ".$issuance->id);
297     my $orig_issuance = $editor->retrieve_serial_issuance($issuance->id);
298
299     $logger->info("issuance-alter: original issuance ".OpenSRF::Utils::JSON->perl2JSON($orig_issuance));
300     $logger->info("issuance-alter: updated issuance ".OpenSRF::Utils::JSON->perl2JSON($issuance));
301     return $editor->event unless $editor->update_serial_issuance($issuance);
302     return 0;
303 }
304
305 __PACKAGE__->register_method(
306     method  => "fleshed_serial_issuance_retrieve_batch",
307     authoritative => 1,
308     api_name    => "open-ils.serial.issuance.fleshed.batch.retrieve"
309 );
310
311 sub fleshed_serial_issuance_retrieve_batch {
312     my( $self, $client, $ids ) = @_;
313 # FIXME: permissions?
314     $logger->info("Fetching fleshed serial issuances @$ids");
315     return $U->cstorereq(
316         "open-ils.cstore.direct.serial.issuance.search.atomic",
317         { id => $ids },
318         { flesh => 1,
319           flesh_fields => {siss => [ qw/creator editor subscription/ ]}
320         });
321 }
322
323 __PACKAGE__->register_method(
324     method  => "pub_fleshed_serial_issuance_retrieve_batch",
325     api_name    => "open-ils.serial.issuance.pub_fleshed.batch.retrieve",
326     signature => {
327         desc => q/
328             Public (i.e. OPAC) call for getting at the sub and 
329             ultimately the record entry from an issuance
330         /,
331         params => [{name => 'ids', desc => 'Array of IDs', type => 'array'}],
332         return => {
333             desc => q/
334                 issuance objects, fleshed with subscriptions
335             /,
336             class => 'siss'
337         }
338     }
339 );
340 sub pub_fleshed_serial_issuance_retrieve_batch {
341     my( $self, $client, $ids ) = @_;
342     return [] unless $ids and @$ids;
343     return new_editor()->search_serial_issuance([
344         { id => $ids },
345         { 
346             flesh => 1,
347             flesh_fields => {siss => [ qw/subscription/ ]}
348         }
349     ]);
350 }
351
352 sub received_siss_by_bib {
353     my $self = shift;
354     my $client = shift;
355     my $bib = shift;
356
357     my $args = shift || {};
358     $$args{order} ||= 'asc';
359
360     my $e = new_editor();
361     my $issuances = $e->json_query({
362         select  => { 'siss' => [ 'id' ] },
363         from    => {
364             siss => {
365                 ssub => {
366                     field  => 'id',
367                     fkey   => 'subscription'
368                 },
369                 sitem => {
370                     field  => 'issuance',
371                     fkey   => 'id',
372                     $$args{ou} ? ( join  => {
373                         sstr => {
374                             field => 'id',
375                             fkey  => 'stream',
376                             join  => {
377                                 sdist => {
378                                     field  => 'id',
379                                     fkey   => 'distribution'
380                                 }
381                             }
382                         }
383                     }) : ()
384                 }
385             }
386         },
387         where => {
388             $$args{type} ? ( 'holding_type' => $$args{type} ) : (),
389             '+ssub'  => { record_entry => $bib },
390             '+sitem' => {
391                 # XXX should we also take specific item statuses into account?
392                 date_received => { '!=' => undef },
393                 $$args{status} ? ( 'status' => $$args{status} ) : ()
394             },
395             $$args{ou} ? ( '+sdist' => {
396                 holding_lib => {
397                     'in' => {
398                         from => [
399                             'actor.org_unit_descendants',
400                             defined($$args{depth}) ? ( $$args{ou}, $$args{depth} ) :  ( $$args{ou} )
401                         ]
402                     }
403                 }
404             }) : ()
405         },
406         $$args{limit}  ? ( limit  => $$args{limit}  ) : (),
407         $$args{offset} ? ( offset => $$args{offset} ) : (),
408         order_by => [{ class => 'siss', field => 'date_published', direction => $$args{order} }],
409         distinct => 1
410     });
411
412     $client->respond($e->retrieve_serial_issuance($_->{id})) for @$issuances;
413     return undef;
414 }
415 __PACKAGE__->register_method(
416     method    => 'received_siss_by_bib',
417     api_name  => 'open-ils.serial.received_siss.retrieve.by_bib',
418     api_level => 1,
419     argc      => 1,
420     stream    => 1,
421     signature => {
422         desc   => 'Receives a Bib ID and other optional params and returns "siss" (issuance) objects',
423         params => [
424             {   name => 'bibid',
425                 desc => 'id of the bre to which the issuances belong',
426                 type => 'number'
427             },
428             {   name => 'args',
429                 desc =>
430 q/A hash of optional arguments.  Valid keys and their meanings:
431     order  := date_published sort direction, either "asc" (chronological, default) or "desc" (reverse chronological)
432     limit  := Number of issuances to return.  Useful for paging results, or finding the oldest or newest
433     offest := Number of issuance to skip before returning results.  Useful for paging.
434     orgid  := OU id used to scope retrieval, based on distribution.holding_lib
435     depth  := OU depth used to range the scope of orgid
436     type   := Holding type filter. Valid values are "basic", "supplement" and "index". Can be a scalar (one) or arrayref (one or more).
437     status := Item status filter. Valid values are "Bindery", "Bound", "Claimed", "Discarded", "Expected", "Not Held", "Not Published" and "Received". Can be a scalar (one) or arrayref (one or more).
438 /
439             }
440         ]
441     }
442 );
443
444
445 ##########################################################################
446 # unit methods
447 #
448 __PACKAGE__->register_method(
449     method    => 'fleshed_sunit_alter',
450     api_name  => 'open-ils.serial.sunit.fleshed.batch.update',
451     api_level => 1,
452     argc      => 2,
453     signature => {
454         desc     => 'Receives an array of one or more Units and updates the database as needed',
455         'params' => [ {
456                  name => 'authtoken',
457                  desc => 'Authtoken for current user session',
458                  type => 'string'
459             },
460             {
461                  name => 'sunits',
462                  desc => 'Array of fleshed Units',
463                  type => 'array'
464             }
465
466         ],
467         'return' => {
468             desc => 'Returns 1 if successful, event if failed',
469             type => 'mixed'
470         }
471     }
472 );
473
474 sub fleshed_sunit_alter {
475     my( $self, $conn, $auth, $sunits ) = @_;
476     return 1 unless ref $sunits;
477     my( $reqr, $evt ) = $U->checkses($auth);
478     return $evt if $evt;
479     my $editor = new_editor(requestor => $reqr, xact => 1);
480     my $override = $self->api_name =~ /override/;
481
482 # TODO: permission support
483 #        return $editor->event unless
484 #            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
485
486     for my $sunit (@$sunits) {
487         if( $sunit->isdeleted ) {
488             $evt = _delete_sunit( $editor, $override, $sunit );
489         } else {
490             $sunit->default_location( $sunit->default_location->id ) if ref $sunit->default_location;
491
492             if( $sunit->isnew ) {
493                 $evt = _create_sunit( $editor, $sunit );
494             } else {
495                 $evt = _update_sunit( $editor, $override, $sunit );
496             }
497         }
498     }
499
500     if( $evt ) {
501         $logger->info("fleshed sunit-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
502         $editor->rollback;
503         return $evt;
504     }
505     $logger->debug("sunit-alter: done updating sunit batch");
506     $editor->commit;
507     $logger->info("fleshed sunit-alter successfully updated ".scalar(@$sunits)." Units");
508     return 1;
509 }
510
511 sub _delete_sunit {
512     my ($editor, $override, $sunit) = @_;
513     $logger->info("sunit-alter: delete sunit ".OpenSRF::Utils::JSON->perl2JSON($sunit));
514     return $editor->event unless $editor->delete_serial_unit($sunit);
515     return 0;
516 }
517
518 sub _create_sunit {
519     my ($editor, $sunit) = @_;
520
521     $logger->info("sunit-alter: new Unit ".OpenSRF::Utils::JSON->perl2JSON($sunit));
522     return $editor->event unless $editor->create_serial_unit($sunit);
523     return 0;
524 }
525
526 sub _update_sunit {
527     my ($editor, $override, $sunit) = @_;
528
529     $logger->info("sunit-alter: retrieving sunit ".$sunit->id);
530     my $orig_sunit = $editor->retrieve_serial_unit($sunit->id);
531
532     $logger->info("sunit-alter: original sunit ".OpenSRF::Utils::JSON->perl2JSON($orig_sunit));
533     $logger->info("sunit-alter: updated sunit ".OpenSRF::Utils::JSON->perl2JSON($sunit));
534     return $editor->event unless $editor->update_serial_unit($sunit);
535     return 0;
536 }
537
538 __PACKAGE__->register_method(
539         method  => "retrieve_unit_list",
540     authoritative => 1,
541         api_name        => "open-ils.serial.unit_list.retrieve"
542 );
543
544 sub retrieve_unit_list {
545
546         my( $self, $client, @sdist_ids ) = @_;
547
548         if(ref($sdist_ids[0])) { @sdist_ids = @{$sdist_ids[0]}; }
549
550         my $e = new_editor();
551
552     my $query = {
553         'select' => 
554             { 'sunit' => [ 'id', 'summary_contents', 'sort_key' ],
555               'sitem' => ['stream'],
556               'sstr' => ['distribution'],
557               'sdist' => [{'column' => 'label', 'alias' => 'sdist_label'}]
558             },
559         'from' =>
560             { 'sdist' =>
561                 { 'sstr' =>
562                     { 'join' =>
563                         { 'sitem' =>
564                             { 'join' => { 'sunit' => {} } }
565                         }
566                     }
567                 }
568             },
569         'distinct' => 'true',
570         'where' => { '+sdist' => {'id' => \@sdist_ids} },
571         'order_by' => [{'class' => 'sunit', 'field' => 'sort_key'}]
572     };
573
574     my $unit_list_entries = $e->json_query($query);
575     
576     my @entries;
577     foreach my $entry (@$unit_list_entries) {
578         my $value = {'sunit' => $entry->{id}, 'sstr' => $entry->{stream}, 'sdist' => $entry->{distribution}};
579         my $label = $entry->{summary_contents};
580         if (length($label) > 100) {
581             $label = substr($label, 0, 100) . '...'; # limited space in dropdown / menu
582         }
583         $label = "[$entry->{sdist_label}/$entry->{stream} #$entry->{id}] " . $label;
584         push (@entries, [$label, OpenSRF::Utils::JSON->perl2JSON($value)]);
585     }
586
587     return \@entries;
588 }
589
590
591
592 ##########################################################################
593 # predict and receive methods
594 #
595 __PACKAGE__->register_method(
596     method    => 'make_predictions',
597     api_name  => 'open-ils.serial.make_predictions',
598     api_level => 1,
599     argc      => 1,
600     signature => {
601         desc     => 'Receives an ssub id and populates the issuance and item tables',
602         'params' => [ {
603                  name => 'ssub_id',
604                  desc => 'Serial Subscription ID',
605                  type => 'int'
606             }
607         ]
608     }
609 );
610
611 sub make_predictions {
612     my ($self, $conn, $authtoken, $args) = @_;
613
614     my $editor = OpenILS::Utils::CStoreEditor->new();
615     my $ssub_id = $args->{ssub_id};
616     my $mfhd = MFHD->new(MARC::Record->new());
617
618     my $ssub = $editor->retrieve_serial_subscription([$ssub_id]);
619     my $scaps = $editor->search_serial_caption_and_pattern({ subscription => $ssub_id, active => 't'});
620     my $sdists = $editor->search_serial_distribution( [{ subscription => $ssub->id }, {  flesh => 1,
621               flesh_fields => {sdist => [ qw/ streams / ]}, limit => 1 }] ); #TODO: 'deleted' support?
622
623     my @predictions;
624     my $link_id = 1;
625     foreach my $scap (@$scaps) {
626         my $caption_field = _revive_caption($scap);
627         $caption_field->update('8' => $link_id);
628         $mfhd->append_fields($caption_field);
629         my $options = {
630                 'caption' => $caption_field,
631                 'scap_id' => $scap->id,
632                 'num_to_predict' => $args->{num_to_predict}
633                 };
634         if ($args->{base_issuance}) { # predict from a given issuance
635             $options->{predict_from} = _revive_holding($args->{base_issuance}->holding_code, $caption_field, 1); # fresh MFHD Record, so we simply default to 1 for seqno
636         } else { # default to predicting from last published
637             my $last_published = $editor->search_serial_issuance([
638                     {'caption_and_pattern' => $scap->id,
639                     'subscription' => $ssub_id},
640                 {limit => 1, order_by => { siss => "date_published DESC" }}]
641                 );
642             if ($last_published->[0]) {
643                 my $last_siss = $last_published->[0];
644                 $options->{predict_from} = _revive_holding($last_siss->holding_code, $caption_field, 1);
645             } else {
646                 #TODO: throw event (can't predict from nothing!)
647             }
648         }
649         push( @predictions, _generate_issuance_values($mfhd, $options) );
650         $link_id++;
651     }
652
653     my @issuances;
654     foreach my $prediction (@predictions) {
655         my $issuance = new Fieldmapper::serial::issuance;
656         $issuance->isnew(1);
657         $issuance->label($prediction->{label});
658         $issuance->date_published($prediction->{date_published}->strftime('%F'));
659         $issuance->holding_code(OpenSRF::Utils::JSON->perl2JSON($prediction->{holding_code}));
660         $issuance->holding_type($prediction->{holding_type});
661         $issuance->caption_and_pattern($prediction->{caption_and_pattern});
662         $issuance->subscription($ssub->id);
663         push (@issuances, $issuance);
664     }
665
666     fleshed_issuance_alter($self, $conn, $authtoken, \@issuances); # FIXME: catch events
667
668     my @items;
669     for (my $i = 0; $i < @issuances; $i++) {
670         my $date_expected = $predictions[$i]->{date_published}->add(seconds => interval_to_seconds($ssub->expected_date_offset))->strftime('%F');
671         my $issuance = $issuances[$i];
672         #$issuance->label(interval_to_seconds($ssub->expected_date_offset));
673         foreach my $sdist (@$sdists) {
674             my $streams = $sdist->streams;
675             foreach my $stream (@$streams) {
676                 my $item = new Fieldmapper::serial::item;
677                 $item->isnew(1);
678                 $item->stream($stream->id);
679                 $item->date_expected($date_expected);
680                 $item->issuance($issuance->id);
681                 push (@items, $item);
682             }
683         }
684     }
685     fleshed_item_alter($self, $conn, $authtoken, \@items); # FIXME: catch events
686     return \@items;
687 }
688
689 #
690 # _generate_issuance_values() is an initial attempt at a function which can be used
691 # to populate an issuance table with a list of predicted issues.  It accepts
692 # a hash ref of options initially defined as:
693 # caption : the caption field to predict on
694 # num_to_predict : the number of issues you wish to predict
695 # last_rec_date : the date of the last received issue, to be used as an offset
696 #                 for predicting future issues
697 #
698 # The basic method is to first convert to a single holding if compressed, then
699 # increment the holding and save the resulting values to @issuances.
700
701 # returns @issuance_values, an array of hashrefs containing (formatted
702 # label, formatted chronology date, formatted estimated arrival date, and an
703 # array ref of holding subfields as (key, value, key, value ...)) (not a hash
704 # to protect order and possible duplicate keys), and a holding type.
705 #
706 sub _generate_issuance_values {
707     my ($mfhd, $options) = @_;
708     my $caption = $options->{caption};
709     my $scap_id = $options->{scap_id};
710     my $num_to_predict = $options->{num_to_predict};
711     my $predict_from = $options->{predict_from};   # issuance to predict from
712     #my $last_rec_date = $options->{last_rec_date};   # expected or actual
713
714     # TODO: add support for predicting serials with no chronology by passing in
715     # a last_pub_date option?
716
717
718 # Only needed for 'real' MFHD records, not our temp records
719 #    my $link_id = $caption->link_id;
720 #    if(!$predict_from) {
721 #        my $htag = $caption->tag;
722 #        $htag =~ s/^85/86/;
723 #        my @holdings = $mfhd->holdings($htag, $link_id);
724 #        my $last_holding = $holdings[-1];
725 #
726 #        #if ($last_holding->is_compressed) {
727 #        #    $last_holding->compressed_to_last; # convert to last in range
728 #        #}
729 #        $predict_from = $last_holding;
730 #    }
731 #
732
733     $predict_from->notes('public',  []);
734 # add a note marker for system use (?)
735     $predict_from->notes('private', ['AUTOGEN']);
736
737     my $strp = new DateTime::Format::Strptime(pattern => '%F');
738     my $pub_date;
739     my @issuance_values;
740     my @predictions = $mfhd->generate_predictions({'base_holding' => $predict_from, 'num_to_predict' => $num_to_predict});
741     foreach my $prediction (@predictions) {
742         $pub_date = $strp->parse_datetime($prediction->chron_to_date);
743         push(
744                 @issuance_values,
745                 {
746                     #$link_id,
747                     label => $prediction->format,
748                     date_published => $pub_date,
749                     #date_expected => $date_expected->strftime('%F'),
750                     holding_code => [$prediction->indicator(1),$prediction->indicator(2),$prediction->subfields_list],
751                     holding_type => $MFHD_NAMES_BY_TAG{$caption->tag},
752                     caption_and_pattern => $scap_id
753                 }
754             );
755     }
756
757     return @issuance_values;
758 }
759
760 sub _revive_caption {
761     my $scap = shift;
762
763     my $pattern_code = $scap->pattern_code;
764
765     # build MARC::Field
766     my $pattern_parts = OpenSRF::Utils::JSON->JSON2perl($pattern_code);
767     unshift(@$pattern_parts, $MFHD_TAGS_BY_NAME{$scap->type});
768     my $pattern_field = new MARC::Field(@$pattern_parts);
769
770     # build MFHD::Caption
771     return new MFHD::Caption($pattern_field);
772 }
773
774 sub _revive_holding {
775     my $holding_code = shift;
776     my $caption_field = shift;
777     my $seqno = shift;
778
779     # build MARC::Field
780     my $holding_parts = OpenSRF::Utils::JSON->JSON2perl($holding_code);
781     my $captag = $caption_field->tag;
782     $captag =~ s/^85/86/;
783     unshift(@$holding_parts, $captag);
784     my $holding_field = new MARC::Field(@$holding_parts);
785
786     # build MFHD::Holding
787     return new MFHD::Holding($seqno, $holding_field, $caption_field);
788 }
789
790 __PACKAGE__->register_method(
791     method    => 'unitize_items',
792     api_name  => 'open-ils.serial.receive_items',
793     api_level => 1,
794     argc      => 1,
795     signature => {
796         desc     => 'Marks an item as received, updates the shelving unit (creating a new shelving unit if needed), and updates the summaries',
797         'params' => [ {
798                  name => 'items',
799                  desc => 'array of serial items',
800                  type => 'array'
801             }
802         ],
803         'return' => {
804             desc => 'Returns number of received items',
805             type => 'int'
806         }
807     }
808 );
809
810 sub unitize_items {
811     my ($self, $conn, $auth, $items) = @_;
812
813     my( $reqr, $evt ) = $U->checkses($auth);
814     return $evt if $evt;
815     my $editor = new_editor(requestor => $reqr, xact => 1);
816     $self->api_name =~ /serial\.(\w*)_items/;
817     my $mode = $1;
818     
819     my %found_unit_ids;
820     my %found_stream_ids;
821     my %found_types;
822
823     my %stream_ids_by_unit_id;
824
825     my %unit_map;
826     my %sdist_by_unit_id;
827     my %sdist_by_stream_id;
828
829     my $new_unit_id; # id for '-2' units to share
830     foreach my $item (@$items) {
831         # for debugging only, TODO: delete
832         if (!ref $item) { # hopefully we got an id instead
833             $item = $editor->retrieve_serial_item($item);
834         }
835         # get ids
836         my $unit_id = ref($item->unit) ? $item->unit->id : $item->unit;
837         my $stream_id = ref($item->stream) ? $item->stream->id : $item->stream;
838         my $issuance_id = ref($item->issuance) ? $item->issuance->id : $item->issuance;
839         #TODO: evt on any missing ids
840
841         if ($mode eq 'receive') {
842             $item->date_received('now');
843             $item->status('Received');
844         } else {
845             $item->status('Bindery');
846         }
847
848         # check for types to trigger summary updates
849         my $scap;
850         if (!ref $item->issuance) {
851             my $scaps = $editor->search_serial_caption_and_pattern([{"+siss" => {"id" => $issuance_id}}, { "join" => {"siss" => {}} }]);
852             $scap = $scaps->[0];
853         } elsif (!ref $item->issuance->caption_and_pattern) {
854             $scap = $editor->retrieve_serial_caption_and_pattern($item->issuance->caption_and_pattern);
855         } else {
856             $scap = $editor->issuance->caption_and_pattern;
857         }
858         if (!exists($found_types{$stream_id})) {
859             $found_types{$stream_id} = {};
860         }
861         $found_types{$stream_id}->{$scap->type} = 1;
862
863         # create unit if needed
864         if ($unit_id == -1 or (!$new_unit_id and $unit_id == -2)) { # create unit per item
865             my $unit;
866             my $sdists = $editor->search_serial_distribution([{"+sstr" => {"id" => $stream_id}}, { "join" => {"sstr" => {}} }]);
867             $unit = _build_unit($editor, $sdists->[0], $mode);
868             my $evt =  _create_sunit($editor, $unit);
869             return $evt if $evt;
870             if ($unit_id == -2) {
871                 $new_unit_id = $unit->id;
872                 $unit_id = $new_unit_id;
873             } else {
874                 $unit_id = $unit->id;
875             }
876             $item->unit($unit_id);
877             
878             # get unit with 'DEFAULT's and save unit and sdist for later use
879             $unit = $editor->retrieve_serial_unit($unit->id);
880             $unit_map{$unit_id} = $unit;
881             $sdist_by_unit_id{$unit_id} = $sdists->[0];
882             $sdist_by_stream_id{$stream_id} = $sdists->[0];
883         } elsif ($unit_id == -2) { # create one unit for all '-2' items
884             $unit_id = $new_unit_id;
885             $item->unit($unit_id);
886         }
887
888         $found_unit_ids{$unit_id} = 1;
889         $found_stream_ids{$stream_id} = 1;
890
891         # save the stream_id for this unit_id
892         # TODO: prevent items from different streams in same unit? (perhaps in interface)
893         $stream_ids_by_unit_id{$unit_id} = $stream_id;
894
895         my $evt = _update_sitem($editor, undef, $item);
896         return $evt if $evt;
897     }
898
899     # deal with unit level labels
900     foreach my $unit_id (keys %found_unit_ids) {
901
902         # get all the needed issuances for unit
903         my $issuances = $editor->search_serial_issuance([ {"+sitem" => {"unit" => $unit_id, "status" => "Received"}}, {"join" => {"sitem" => {}}, "order_by" => {"siss" => "date_published"}} ]);
904         #TODO: evt on search failure
905
906         my ($mfhd, $formatted_parts) = _summarize_contents($editor, $issuances);
907
908         # special case for single formatted_part (may have summarized version)
909         if (@$formatted_parts == 1) {
910             #TODO: MFHD.pm should have a 'format_summary' method for this
911         }
912
913         # retrieve and update unit contents
914         my $sunit;
915         my $sdist;
916
917         # if we just created the unit, we will already have it and the distribution stored
918         if (exists $unit_map{$unit_id}) {
919             $sunit = $unit_map{$unit_id};
920             $sdist = $sdist_by_unit_id{$unit_id};
921         } else {
922             $sunit = $editor->retrieve_serial_unit($unit_id);
923             $sdist = $editor->search_serial_distribution([{"+sstr" => {"id" => $stream_ids_by_unit_id{$unit_id}}}, { "join" => {"sstr" => {}} }]);
924             $sdist = $sdist->[0];
925         }
926
927         $sunit->detailed_contents($sdist->unit_label_prefix . ' '
928                     . join(', ', @$formatted_parts) . ' '
929                     . $sdist->unit_label_suffix);
930
931         $sunit->summary_contents($sunit->detailed_contents); #TODO: change this when real summary contents are available
932
933         # create sort_key by left padding numbers to 6 digits
934         my $sort_key = $sunit->detailed_contents;
935         $sort_key =~ s/(\d+)/sprintf '%06d', $1/eg; # this may need improvement
936         $sunit->sort_key($sort_key);
937         
938         if ($mode eq 'bind') {
939             $sunit->status(2); # set to 'Bindery' status
940         }
941
942         my $evt = _update_sunit($editor, undef, $sunit);
943         return $evt if $evt;
944     }
945
946     # TODO: cleanup 'dead' units (units which are now emptied of their items)
947
948     if ($mode eq 'receive') { # the summary holdings do not change when binding
949         # deal with stream level summaries
950         # summaries will be built from the "primary" stream only, that is, the stream with the lowest ID per distribution
951         # (TODO: consider direct designation)
952         my %primary_streams_by_sdist;
953         my %streams_by_sdist;
954
955         # see if we have primary streams, and if so, associate them with their distributions
956         foreach my $stream_id (keys %found_stream_ids) {
957             my $sdist;
958             if (exists $sdist_by_stream_id{$stream_id}) {
959                 $sdist = $sdist_by_stream_id{$stream_id};
960             } else {
961                 $sdist = $editor->search_serial_distribution([{"+sstr" => {"id" => $stream_id}}, { "join" => {"sstr" => {}} }]);
962                 $sdist = $sdist->[0];
963             }
964             my $streams;
965             if (!exists($streams_by_sdist{$sdist->id})) {
966                 $streams = $editor->search_serial_stream([{"distribution" => $sdist->id}, {"order_by" => {"sstr" => "id"}}]);
967                 $streams_by_sdist{$sdist->id} = $streams;
968             } else {
969                 $streams = $streams_by_sdist{$sdist->id};
970             }
971             $primary_streams_by_sdist{$sdist->id} = $streams->[0] if ($stream_id == $streams->[0]->id);
972         }
973
974         # retrieve and update summaries for each affected primary stream's distribution
975         foreach my $sdist_id (keys %primary_streams_by_sdist) {
976             my $stream = $primary_streams_by_sdist{$sdist_id};
977             my $stream_id = $stream->id;
978             # get all the needed issuances for stream
979             # FIXME: search in Bindery/Bound/Not Published? as well as Received
980             foreach my $type (keys %{$found_types{$stream_id}}) {
981                 my $issuances = $editor->search_serial_issuance([ {"+sitem" => {"stream" => $stream_id, "status" => "Received"}, "+scap" => {"type" => $type}}, {"join" => {"sitem" => {}, "scap" => {}}, "order_by" => {"siss" => "date_published"}} ]);
982                 #TODO: evt on search failure
983
984                 my ($mfhd, $formatted_parts) = _summarize_contents($editor, $issuances);
985
986                 # retrieve and update the generated_coverage of the summary
987                 my $search_method = "search_serial_${type}_summary";
988                 my $summary = $editor->$search_method([{"distribution" => $sdist_id}]);
989                 $summary = $summary->[0];
990                 $summary->generated_coverage(join(', ', @$formatted_parts));
991                 my $update_method = "update_serial_${type}_summary";
992                 return $editor->event unless $editor->$update_method($summary);
993             }
994         }
995     }
996
997     $editor->commit;
998     return {'num_items_received' => scalar @$items, 'new_unit_id' => $new_unit_id};
999 }
1000
1001 sub _find_or_create_call_number {
1002     my ($e, $lib, $cn_string, $record) = @_;
1003
1004     my $existing = $e->search_asset_call_number({
1005         "owning_lib" => $lib,
1006         "label" => $cn_string,
1007         "record" => $record,
1008         "deleted" => "f"
1009     }) or return $e->die_event;
1010
1011     if (@$existing) {
1012         return $existing->[0]->id;
1013     } else {
1014         return $e->die_event unless
1015             $e->allowed("CREATE_VOLUME", $lib);
1016
1017         my $acn = new Fieldmapper::asset::call_number;
1018
1019         $acn->creator($e->requestor->id);
1020         $acn->editor($e->requestor->id);
1021         $acn->record($record);
1022         $acn->label($cn_string);
1023         $acn->owning_lib($lib);
1024
1025         $e->create_asset_call_number($acn) or return $e->die_event;
1026         return $e->data->id;
1027     }
1028 }
1029
1030 sub _issuances_received {
1031     my ($e, $sitem) = @_;
1032
1033     my $results = $e->json_query({
1034         "select" => {
1035             "sitem" => [
1036                 {"transform" => "distinct", "column" => "issuance"}
1037             ]
1038         },
1039         "from" => {"sitem" => {"sstr" => {}, "siss" => {}}},
1040         "where" => {
1041             "+sstr" => {"distribution" => $sitem->stream->distribution->id},
1042             "+siss" => {"holding_type" => $sitem->issuance->holding_type},
1043             "+sitem" => {"date_received" => {"!=" => undef}}
1044         }
1045     }) or return $e->die_event;
1046
1047     return [ map { $e->retrieve_serial_issuance($_->{"issuance"}) } @$results ];
1048 }
1049
1050 # XXX _prepare_unit_label() duplicates some code from unitize_items().
1051 # Hopefully we can unify code paths down the road.
1052 sub _prepare_unit_label {
1053     my ($e, $sunit, $sdist, $issuance) = @_;
1054
1055     my ($mfhd, $formatted_parts) = _summarize_contents($e, [$issuance]);
1056
1057     # special case for single formatted_part (may have summarized version)
1058     if (@$formatted_parts == 1) {
1059         #TODO: MFHD.pm should have a 'format_summary' method for this
1060     }
1061
1062     $sunit->detailed_contents(
1063         join(
1064             " ",
1065             $sdist->unit_label_prefix,
1066             join(", ", @$formatted_parts),
1067             $sdist->unit_label_suffix
1068         )
1069     );
1070
1071     # TODO: change this when real summary contents are available
1072     $sunit->summary_contents($sunit->detailed_contents);
1073
1074     # Create sort_key by left padding numbers to 6 digits.
1075     (my $sort_key = $sunit->detailed_contents) =~
1076         s/(\d+)/sprintf '%06d', $1/eg;
1077     $sunit->sort_key($sort_key);
1078 }
1079
1080 # XXX duplicates a block of code from unitize_items().  Once I fully understand
1081 # what's going on and I'm sure it's working right, I'd like to have
1082 # unitize_items() just use this, keeping the logic in one place.
1083 sub _prepare_summaries {
1084     my ($e, $sitem, $issuances) = @_;
1085
1086     my $dist_id = $sitem->stream->distribution->id;
1087     my $type = $sitem->issuance->holding_type;
1088
1089     # Make sure @$issuances contains the new issuance from sitem.
1090     unless (grep { $_->id == $sitem->issuance->id } @$issuances) {
1091         push @$issuances, $sitem->issuance;
1092     }
1093
1094     my ($mfhd, $formatted_parts) = _summarize_contents($e, $issuances);
1095
1096     my $search_method = "search_serial_${type}_summary";
1097     my $summary = $e->$search_method([{"distribution" => $dist_id}]);
1098
1099     my $cu_method = "update";
1100
1101     if (@$summary) {
1102         $summary = $summary->[0];
1103     } else {
1104         my $class = "Fieldmapper::serial::${type}_summary";
1105         $summary = $class->new;
1106         $summary->distribution($dist_id);
1107         $cu_method = "create";
1108     }
1109
1110     $summary->generated_coverage(join(", ", @$formatted_parts));
1111     my $method = "${cu_method}_serial_${type}_summary";
1112     return $e->die_event unless $e->$method($summary);
1113 }
1114
1115 __PACKAGE__->register_method(
1116     "method" => "receive_items_one_unit_per",
1117     "api_name" => "open-ils.serial.receive_items.one_unit_per",
1118     "stream" => 1,
1119     "api_level" => 1,
1120     "argc" => 3,
1121     "signature" => {
1122         "desc" => "Marks items in a list as received, creates a new unit for each item if any unit is fleshed on, and updates summaries as needed",
1123         "params" => [
1124             {
1125                  "name" => "auth",
1126                  "desc" => "authtoken",
1127                  "type" => "string"
1128             },
1129             {
1130                  "name" => "items",
1131                  "desc" => "array of serial items, possibly fleshed with units and definitely fleshed with stream->distribution",
1132                  "type" => "array"
1133             },
1134             {
1135                 "name" => "record",
1136                 "desc" => "id of bib record these items are associated with
1137                     (XXX could/should be derived from items)",
1138                 "type" => "number"
1139             }
1140         ],
1141         "return" => {
1142             "desc" => "The item ID for each item successfully received",
1143             "type" => "int"
1144         }
1145     }
1146 );
1147
1148 sub receive_items_one_unit_per {
1149     # XXX This function may be temporary, as it does some of what
1150     # unitize_items() does, just in a different way.
1151     my ($self, $client, $auth, $items, $record) = @_;
1152
1153     my $e = new_editor("authtoken" => $auth, "xact" => 1);
1154     return $e->die_event unless $e->checkauth;
1155
1156     my $user_id = $e->requestor->id;
1157
1158     # Get a list of all the non-virtual field names in a serial::unit for
1159     # merging given unit objects with template-built units later.
1160     # XXX move this somewhere global so it isn't re-run all the time
1161     my $all_unit_fields =
1162         $Fieldmapper::fieldmap->{"Fieldmapper::serial::unit"}->{"fields"};
1163     my @real_unit_fields = grep {
1164         not $all_unit_fields->{$_}->{"virtual"}
1165     } keys %$all_unit_fields;
1166
1167     foreach my $item (@$items) {
1168         # Note that we expect a certain fleshing on the items we're getting.
1169         my $sdist = $item->stream->distribution;
1170
1171         # Create unit if given by user
1172         if (ref $item->unit) {
1173             # detach from the item, as we need to create separately
1174             my $user_unit = $item->unit;
1175
1176             # get a unit based on associated template
1177             my $template_unit = _build_unit($e, $sdist, "receive", 1);
1178             if ($U->event_code($template_unit)) {
1179                 $e->rollback;
1180                 $template_unit->{"note"} = "Item ID: " . $item->id;
1181                 return $template_unit;
1182             }
1183
1184             # merge built unit with provided unit from user
1185             foreach (@real_unit_fields) {
1186                 unless ($user_unit->$_) {
1187                     $user_unit->$_($template_unit->$_);
1188                 }
1189             }
1190
1191             # Treat call number specially: the provided value from the
1192             # user will really be a string.
1193             if ($user_unit->call_number) {
1194                 my $real_cn = _find_or_create_call_number(
1195                     $e, $sdist->holding_lib->id,
1196                     $user_unit->call_number, $record
1197                 );
1198
1199                 if ($U->event_code($real_cn)) {
1200                     $e->rollback;
1201                     return $real_cn;
1202                 } else {
1203                     $user_unit->call_number($real_cn);
1204                 }
1205             }
1206
1207             my $evt = _prepare_unit_label(
1208                 $e, $user_unit, $sdist, $item->issuance
1209             );
1210             if ($U->event_code($evt)) {
1211                 $e->rollback;
1212                 return $evt;
1213             }
1214
1215             # fetch a list of issuances with received copies already existing
1216             # on this distribution.
1217             my $issuances = _issuances_received($e, $item); #XXX optimize later
1218             if ($U->event_code($issuances)) {
1219                 $e->rollback;
1220                 return $issuances;
1221             }
1222
1223             # create/update summary objects related to this distribution
1224             $evt = _prepare_summaries($e, $item, $issuances);
1225             if ($U->event_code($evt)) {
1226                 $e->rollback;
1227                 return $evt;
1228             }
1229
1230             # set the incontrovertibles on the unit
1231             $user_unit->edit_date("now");
1232             $user_unit->create_date("now");
1233             $user_unit->editor($user_id);
1234             $user_unit->creator($user_id);
1235
1236             return $e->die_event unless $e->create_serial_unit($user_unit);
1237
1238             # save reference to new unit
1239             $item->unit($e->data->id);
1240         }
1241
1242         # Create notes if given by user
1243         if (ref($item->notes) and @{$item->notes}) {
1244             foreach my $note (@{$item->notes}) {
1245                 $note->creator($user_id);
1246                 $note->create_date("now");
1247
1248                 return $e->die_event unless $e->create_serial_item_note($note);
1249             }
1250
1251             $item->clear_notes; # They're saved; we no longer want them here.
1252         }
1253
1254         # Set the incontrovertibles on the item
1255         $item->status("Received");
1256         $item->date_received("now");
1257         $item->edit_date("now");
1258         $item->editor($user_id);
1259
1260         return $e->die_event unless $e->update_serial_item($item);
1261
1262         # send client a response
1263         $client->respond($item->id);
1264     }
1265
1266     $e->commit or return $e->die_event;
1267     undef;
1268 }
1269
1270 sub _build_unit {
1271     my $editor = shift;
1272     my $sdist = shift;
1273     my $mode = shift;
1274     my $skip_call_number = shift;
1275
1276     my $attr = $mode . '_unit_template';
1277     my $template = $editor->retrieve_asset_copy_template($sdist->$attr) or
1278         return new OpenILS::Event("SERIAL_DISTRIBUTION_HAS_NO_COPY_TEMPLATE");
1279
1280     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 );
1281
1282     my $unit = new Fieldmapper::serial::unit;
1283     foreach my $part (@parts) {
1284         my $value = $template->$part;
1285         next if !defined($value);
1286         $unit->$part($value);
1287     }
1288
1289     # ignore circ_lib in template, set to distribution holding_lib
1290     $unit->circ_lib($sdist->holding_lib);
1291     $unit->creator($editor->requestor->id);
1292     $unit->editor($editor->requestor->id);
1293
1294     unless ($skip_call_number) {
1295         $attr = $mode . '_call_number';
1296         my $cn = $sdist->$attr or
1297             return new OpenILS::Event("SERIAL_DISTRIBUTION_HAS_NO_CALL_NUMBER");
1298
1299         $unit->call_number($cn);
1300     }
1301
1302     $unit->barcode('AUTO');
1303     $unit->sort_key('');
1304     $unit->summary_contents('');
1305     $unit->detailed_contents('');
1306
1307     return $unit;
1308 }
1309
1310
1311 sub _summarize_contents {
1312     my $editor = shift;
1313     my $issuances = shift;
1314
1315     # create MFHD record
1316     my $mfhd = MFHD->new(MARC::Record->new());
1317     my %scaps;
1318     my %scap_fields;
1319     my @scap_fields_ordered;
1320     my $seqno = 1;
1321     my $link_id = 1;
1322     foreach my $issuance (@$issuances) {
1323         my $scap_id = $issuance->caption_and_pattern;
1324         next if (!$scap_id); # skip issuances with no caption/pattern
1325
1326         my $scap;
1327         my $scap_field;
1328         # if this is the first appearance of this scap, retrieve it and add it to the temporary record
1329         if (!exists $scaps{$issuance->caption_and_pattern}) {
1330             $scaps{$scap_id} = $editor->retrieve_serial_caption_and_pattern($scap_id);
1331             $scap = $scaps{$scap_id};
1332             $scap_field = _revive_caption($scap);
1333             $scap_fields{$scap_id} = $scap_field;
1334             push(@scap_fields_ordered, $scap_field);
1335             $scap_field->update('8' => $link_id);
1336             $mfhd->append_fields($scap_field);
1337             $link_id++;
1338         } else {
1339             $scap = $scaps{$scap_id};
1340             $scap_field = $scap_fields{$scap_id};
1341         }
1342
1343         $mfhd->append_fields(_revive_holding($issuance->holding_code, $scap_field, $seqno));
1344         $seqno++;
1345     }
1346
1347     my @formatted_parts;
1348     foreach my $scap_field (@scap_fields_ordered) { #TODO: use generic MFHD "summarize" method, once available
1349        my @updated_holdings = $mfhd->get_compressed_holdings($scap_field);
1350        foreach my $holding (@updated_holdings) {
1351            push(@formatted_parts, $holding->format);
1352        }
1353     }
1354
1355     return ($mfhd, \@formatted_parts);
1356 }
1357
1358 ##########################################################################
1359 # note methods
1360 #
1361 __PACKAGE__->register_method(
1362     method      => 'fetch_notes',
1363     api_name        => 'open-ils.serial.item_note.retrieve.all',
1364     signature   => q/
1365         Returns an array of copy note objects.  
1366         @param args A named hash of parameters including:
1367             authtoken   : Required if viewing non-public notes
1368             item_id      : The id of the item whose notes we want to retrieve
1369             pub         : True if all the caller wants are public notes
1370         @return An array of note objects
1371     /
1372 );
1373
1374 __PACKAGE__->register_method(
1375     method      => 'fetch_notes',
1376     api_name        => 'open-ils.serial.subscription_note.retrieve.all',
1377     signature   => q/
1378         Returns an array of copy note objects.  
1379         @param args A named hash of parameters including:
1380             authtoken       : Required if viewing non-public notes
1381             subscription_id : The id of the item whose notes we want to retrieve
1382             pub             : True if all the caller wants are public notes
1383         @return An array of note objects
1384     /
1385 );
1386
1387 __PACKAGE__->register_method(
1388     method      => 'fetch_notes',
1389     api_name        => 'open-ils.serial.distribution_note.retrieve.all',
1390     signature   => q/
1391         Returns an array of copy note objects.  
1392         @param args A named hash of parameters including:
1393             authtoken       : Required if viewing non-public notes
1394             distribution_id : The id of the item whose notes we want to retrieve
1395             pub             : True if all the caller wants are public notes
1396         @return An array of note objects
1397     /
1398 );
1399
1400 # TODO: revisit this method to consider replacing cstore direct calls
1401 sub fetch_notes {
1402     my( $self, $connection, $args ) = @_;
1403     
1404     $self->api_name =~ /serial\.(\w*)_note/;
1405     my $type = $1;
1406
1407     my $id = $$args{object_id};
1408     my $authtoken = $$args{authtoken};
1409     my( $r, $evt);
1410
1411     if( $$args{pub} ) {
1412         return $U->cstorereq(
1413             'open-ils.cstore.direct.serial.'.$type.'_note.search.atomic',
1414             { $type => $id, pub => 't' } );
1415     } else {
1416         # FIXME: restore perm check
1417         # ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_COPY_NOTES');
1418         # return $evt if $evt;
1419         return $U->cstorereq(
1420             'open-ils.cstore.direct.serial.'.$type.'_note.search.atomic', {$type => $id} );
1421     }
1422
1423     return undef;
1424 }
1425
1426 __PACKAGE__->register_method(
1427     method      => 'create_note',
1428     api_name        => 'open-ils.serial.item_note.create',
1429     signature   => q/
1430         Creates a new item note
1431         @param authtoken The login session key
1432         @param note The note object to create
1433         @return The id of the new note object
1434     /
1435 );
1436
1437 __PACKAGE__->register_method(
1438     method      => 'create_note',
1439     api_name        => 'open-ils.serial.subscription_note.create',
1440     signature   => q/
1441         Creates a new subscription note
1442         @param authtoken The login session key
1443         @param note The note object to create
1444         @return The id of the new note object
1445     /
1446 );
1447
1448 __PACKAGE__->register_method(
1449     method      => 'create_note',
1450     api_name        => 'open-ils.serial.distribution_note.create',
1451     signature   => q/
1452         Creates a new distribution note
1453         @param authtoken The login session key
1454         @param note The note object to create
1455         @return The id of the new note object
1456     /
1457 );
1458
1459 sub create_note {
1460     my( $self, $connection, $authtoken, $note ) = @_;
1461
1462     $self->api_name =~ /serial\.(\w*)_note/;
1463     my $type = $1;
1464
1465     my $e = new_editor(xact=>1, authtoken=>$authtoken);
1466     return $e->event unless $e->checkauth;
1467
1468     # FIXME: restore permission support
1469 #    my $item = $e->retrieve_serial_item(
1470 #        [
1471 #            $note->item
1472 #        ]
1473 #    );
1474 #
1475 #    return $e->event unless
1476 #        $e->allowed('CREATE_COPY_NOTE', $item->call_number->owning_lib);
1477
1478     $note->create_date('now');
1479     $note->creator($e->requestor->id);
1480     $note->pub( ($U->is_true($note->pub)) ? 't' : 'f' );
1481     $note->clear_id;
1482
1483     my $method = "create_serial_${type}_note";
1484     $e->$method($note) or return $e->event;
1485     $e->commit;
1486     return $note->id;
1487 }
1488
1489 __PACKAGE__->register_method(
1490     method      => 'delete_note',
1491     api_name        =>  'open-ils.serial.item_note.delete',
1492     signature   => q/
1493         Deletes an existing item note
1494         @param authtoken The login session key
1495         @param noteid The id of the note to delete
1496         @return 1 on success - Event otherwise.
1497         /
1498 );
1499
1500 __PACKAGE__->register_method(
1501     method      => 'delete_note',
1502     api_name        =>  'open-ils.serial.subscription_note.delete',
1503     signature   => q/
1504         Deletes an existing subscription note
1505         @param authtoken The login session key
1506         @param noteid The id of the note to delete
1507         @return 1 on success - Event otherwise.
1508         /
1509 );
1510
1511 __PACKAGE__->register_method(
1512     method      => 'delete_note',
1513     api_name        =>  'open-ils.serial.distribution_note.delete',
1514     signature   => q/
1515         Deletes an existing distribution note
1516         @param authtoken The login session key
1517         @param noteid The id of the note to delete
1518         @return 1 on success - Event otherwise.
1519         /
1520 );
1521
1522 sub delete_note {
1523     my( $self, $conn, $authtoken, $noteid ) = @_;
1524
1525     $self->api_name =~ /serial\.(\w*)_note/;
1526     my $type = $1;
1527
1528     my $e = new_editor(xact=>1, authtoken=>$authtoken);
1529     return $e->die_event unless $e->checkauth;
1530
1531     my $method = "retrieve_serial_${type}_note";
1532     my $note = $e->$method([
1533         $noteid,
1534     ]) or return $e->die_event;
1535
1536 # FIXME: restore permissions check
1537 #    if( $note->creator ne $e->requestor->id ) {
1538 #        return $e->die_event unless
1539 #            $e->allowed('DELETE_COPY_NOTE', $note->item->call_number->owning_lib);
1540 #    }
1541
1542     $method = "delete_serial_${type}_note";
1543     $e->$method($note) or return $e->die_event;
1544     $e->commit;
1545     return 1;
1546 }
1547
1548
1549 ##########################################################################
1550 # subscription methods
1551 #
1552 __PACKAGE__->register_method(
1553     method    => 'fleshed_ssub_alter',
1554     api_name  => 'open-ils.serial.subscription.fleshed.batch.update',
1555     api_level => 1,
1556     argc      => 2,
1557     signature => {
1558         desc     => 'Receives an array of one or more subscriptions and updates the database as needed',
1559         'params' => [ {
1560                  name => 'authtoken',
1561                  desc => 'Authtoken for current user session',
1562                  type => 'string'
1563             },
1564             {
1565                  name => 'subscriptions',
1566                  desc => 'Array of fleshed subscriptions',
1567                  type => 'array'
1568             }
1569
1570         ],
1571         'return' => {
1572             desc => 'Returns 1 if successful, event if failed',
1573             type => 'mixed'
1574         }
1575     }
1576 );
1577
1578 sub fleshed_ssub_alter {
1579     my( $self, $conn, $auth, $ssubs ) = @_;
1580     return 1 unless ref $ssubs;
1581     my( $reqr, $evt ) = $U->checkses($auth);
1582     return $evt if $evt;
1583     my $editor = new_editor(requestor => $reqr, xact => 1);
1584     my $override = $self->api_name =~ /override/;
1585
1586 # TODO: permission check
1587 #        return $editor->event unless
1588 #            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
1589
1590     for my $ssub (@$ssubs) {
1591
1592         my $ssubid = $ssub->id;
1593
1594         if( $ssub->isdeleted ) {
1595             $evt = _delete_ssub( $editor, $override, $ssub);
1596         } elsif( $ssub->isnew ) {
1597             _cleanse_dates($ssub, ['start_date','end_date']);
1598             $evt = _create_ssub( $editor, $ssub );
1599         } else {
1600             _cleanse_dates($ssub, ['start_date','end_date']);
1601             $evt = _update_ssub( $editor, $override, $ssub );
1602         }
1603     }
1604
1605     if( $evt ) {
1606         $logger->info("fleshed subscription-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
1607         $editor->rollback;
1608         return $evt;
1609     }
1610     $logger->debug("subscription-alter: done updating subscription batch");
1611     $editor->commit;
1612     $logger->info("fleshed subscription-alter successfully updated ".scalar(@$ssubs)." subscriptions");
1613     return 1;
1614 }
1615
1616 sub _delete_ssub {
1617     my ($editor, $override, $ssub) = @_;
1618     $logger->info("subscription-alter: delete subscription ".OpenSRF::Utils::JSON->perl2JSON($ssub));
1619     my $sdists = $editor->search_serial_distribution(
1620             { subscription => $ssub->id }, { limit => 1 } ); #TODO: 'deleted' support?
1621     my $cps = $editor->search_serial_caption_and_pattern(
1622             { subscription => $ssub->id }, { limit => 1 } ); #TODO: 'deleted' support?
1623     my $sisses = $editor->search_serial_issuance(
1624             { subscription => $ssub->id }, { limit => 1 } ); #TODO: 'deleted' support?
1625     return OpenILS::Event->new(
1626             'SERIAL_SUBSCRIPTION_NOT_EMPTY', payload => $ssub->id ) if (@$sdists or @$cps or @$sisses);
1627
1628     return $editor->event unless $editor->delete_serial_subscription($ssub);
1629     return 0;
1630 }
1631
1632 sub _create_ssub {
1633     my ($editor, $ssub) = @_;
1634
1635     $logger->info("subscription-alter: new subscription ".OpenSRF::Utils::JSON->perl2JSON($ssub));
1636     return $editor->event unless $editor->create_serial_subscription($ssub);
1637     return 0;
1638 }
1639
1640 sub _update_ssub {
1641     my ($editor, $override, $ssub) = @_;
1642
1643     $logger->info("subscription-alter: retrieving subscription ".$ssub->id);
1644     my $orig_ssub = $editor->retrieve_serial_subscription($ssub->id);
1645
1646     $logger->info("subscription-alter: original subscription ".OpenSRF::Utils::JSON->perl2JSON($orig_ssub));
1647     $logger->info("subscription-alter: updated subscription ".OpenSRF::Utils::JSON->perl2JSON($ssub));
1648     return $editor->event unless $editor->update_serial_subscription($ssub);
1649     return 0;
1650 }
1651
1652 __PACKAGE__->register_method(
1653     method  => "fleshed_serial_subscription_retrieve_batch",
1654     authoritative => 1,
1655     api_name    => "open-ils.serial.subscription.fleshed.batch.retrieve"
1656 );
1657
1658 sub fleshed_serial_subscription_retrieve_batch {
1659     my( $self, $client, $ids ) = @_;
1660 # FIXME: permissions?
1661     $logger->info("Fetching fleshed subscriptions @$ids");
1662     return $U->cstorereq(
1663         "open-ils.cstore.direct.serial.subscription.search.atomic",
1664         { id => $ids },
1665         { flesh => 1,
1666           flesh_fields => {ssub => [ qw/owning_lib notes/ ]}
1667         });
1668 }
1669
1670 __PACKAGE__->register_method(
1671         method  => "retrieve_sub_tree",
1672     authoritative => 1,
1673         api_name        => "open-ils.serial.subscription_tree.retrieve"
1674 );
1675
1676 __PACKAGE__->register_method(
1677         method  => "retrieve_sub_tree",
1678         api_name        => "open-ils.serial.subscription_tree.global.retrieve"
1679 );
1680
1681 sub retrieve_sub_tree {
1682
1683         my( $self, $client, $user_session, $docid, @org_ids ) = @_;
1684
1685         if(ref($org_ids[0])) { @org_ids = @{$org_ids[0]}; }
1686
1687         $docid = "$docid";
1688
1689         # TODO: permission support
1690         if(!@org_ids and $user_session) {
1691                 my $user_obj = 
1692                         OpenILS::Application::AppUtils->check_user_session( $user_session ); #throws EX on error
1693                         @org_ids = ($user_obj->home_ou);
1694         }
1695
1696         if( $self->api_name =~ /global/ ) {
1697                 return _build_subs_list( { record_entry => $docid } ); # TODO: filter for !deleted, or active?
1698
1699         } else {
1700
1701                 my @all_subs;
1702                 for my $orgid (@org_ids) {
1703                         my $subs = _build_subs_list( 
1704                                         { record_entry => $docid, owning_lib => $orgid } );# TODO: filter for !deleted, or active?
1705                         push( @all_subs, @$subs );
1706                 }
1707                 
1708                 return \@all_subs;
1709         }
1710
1711         return undef;
1712 }
1713
1714 sub _build_subs_list {
1715         my $search_hash = shift;
1716
1717         #$search_hash->{deleted} = 'f';
1718         my $e = new_editor();
1719
1720         my $subs = $e->search_serial_subscription([$search_hash, { 'order_by' => {'ssub' => 'id'} }]);
1721
1722         my @built_subs;
1723
1724         for my $sub (@$subs) {
1725
1726         # TODO: filter on !deleted?
1727                 my $dists = $e->search_serial_distribution(
1728             [{ subscription => $sub->id }, { 'order_by' => {'sdist' => 'label'} }]
1729             );
1730
1731                 #$dists = [ sort { $a->label cmp $b->label } @$dists  ];
1732
1733                 $sub->distributions($dists);
1734         
1735         # TODO: filter on !deleted?
1736                 my $issuances = $e->search_serial_issuance(
1737                         [{ subscription => $sub->id }, { 'order_by' => {'siss' => 'label'} }]
1738             );
1739
1740                 #$issuances = [ sort { $a->label cmp $b->label } @$issuances  ];
1741                 $sub->issuances($issuances);
1742
1743         # TODO: filter on !deleted?
1744                 my $scaps = $e->search_serial_caption_and_pattern(
1745                         [{ subscription => $sub->id }, { 'order_by' => {'scap' => 'id'} }]
1746             );
1747
1748                 #$scaps = [ sort { $a->id cmp $b->id } @$scaps  ];
1749                 $sub->scaps($scaps);
1750                 push( @built_subs, $sub );
1751         }
1752
1753         return \@built_subs;
1754
1755 }
1756
1757 __PACKAGE__->register_method(
1758     method  => "subscription_orgs_for_title",
1759     authoritative => 1,
1760     api_name    => "open-ils.serial.subscription.retrieve_orgs_by_title"
1761 );
1762
1763 sub subscription_orgs_for_title {
1764     my( $self, $client, $record_id ) = @_;
1765
1766     my $subs = $U->simple_scalar_request(
1767         "open-ils.cstore",
1768         "open-ils.cstore.direct.serial.subscription.search.atomic",
1769         { record_entry => $record_id }); # TODO: filter on !deleted?
1770
1771     my $orgs = { map {$_->owning_lib => 1 } @$subs };
1772     return [ keys %$orgs ];
1773 }
1774
1775
1776 ##########################################################################
1777 # distribution methods
1778 #
1779 __PACKAGE__->register_method(
1780     method    => 'fleshed_sdist_alter',
1781     api_name  => 'open-ils.serial.distribution.fleshed.batch.update',
1782     api_level => 1,
1783     argc      => 2,
1784     signature => {
1785         desc     => 'Receives an array of one or more distributions and updates the database as needed',
1786         'params' => [ {
1787                  name => 'authtoken',
1788                  desc => 'Authtoken for current user session',
1789                  type => 'string'
1790             },
1791             {
1792                  name => 'distributions',
1793                  desc => 'Array of fleshed distributions',
1794                  type => 'array'
1795             }
1796
1797         ],
1798         'return' => {
1799             desc => 'Returns 1 if successful, event if failed',
1800             type => 'mixed'
1801         }
1802     }
1803 );
1804
1805 sub fleshed_sdist_alter {
1806     my( $self, $conn, $auth, $sdists ) = @_;
1807     return 1 unless ref $sdists;
1808     my( $reqr, $evt ) = $U->checkses($auth);
1809     return $evt if $evt;
1810     my $editor = new_editor(requestor => $reqr, xact => 1);
1811     my $override = $self->api_name =~ /override/;
1812
1813 # TODO: permission check
1814 #        return $editor->event unless
1815 #            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
1816
1817     for my $sdist (@$sdists) {
1818         my $sdistid = $sdist->id;
1819
1820         if( $sdist->isdeleted ) {
1821             $evt = _delete_sdist( $editor, $override, $sdist);
1822         } elsif( $sdist->isnew ) {
1823             $evt = _create_sdist( $editor, $sdist );
1824         } else {
1825             $evt = _update_sdist( $editor, $override, $sdist );
1826         }
1827     }
1828
1829     if( $evt ) {
1830         $logger->info("fleshed distribution-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
1831         $editor->rollback;
1832         return $evt;
1833     }
1834     $logger->debug("distribution-alter: done updating distribution batch");
1835     $editor->commit;
1836     $logger->info("fleshed distribution-alter successfully updated ".scalar(@$sdists)." distributions");
1837     return 1;
1838 }
1839
1840 sub _delete_sdist {
1841     my ($editor, $override, $sdist) = @_;
1842     $logger->info("distribution-alter: delete distribution ".OpenSRF::Utils::JSON->perl2JSON($sdist));
1843     return $editor->event unless $editor->delete_serial_distribution($sdist);
1844     return 0;
1845 }
1846
1847 sub _create_sdist {
1848     my ($editor, $sdist) = @_;
1849
1850     $logger->info("distribution-alter: new distribution ".OpenSRF::Utils::JSON->perl2JSON($sdist));
1851     return $editor->event unless $editor->create_serial_distribution($sdist);
1852
1853     # create summaries too
1854     my $summary = new Fieldmapper::serial::basic_summary;
1855     $summary->distribution($sdist->id);
1856     $summary->generated_coverage('');
1857     return $editor->event unless $editor->create_serial_basic_summary($summary);
1858     $summary = new Fieldmapper::serial::supplement_summary;
1859     $summary->distribution($sdist->id);
1860     $summary->generated_coverage('');
1861     return $editor->event unless $editor->create_serial_supplement_summary($summary);
1862     $summary = new Fieldmapper::serial::index_summary;
1863     $summary->distribution($sdist->id);
1864     $summary->generated_coverage('');
1865     return $editor->event unless $editor->create_serial_index_summary($summary);
1866
1867     # create a starter stream (TODO: reconsider this)
1868     my $stream = new Fieldmapper::serial::stream;
1869     $stream->distribution($sdist->id);
1870     return $editor->event unless $editor->create_serial_stream($stream);
1871
1872     return 0;
1873 }
1874
1875 sub _update_sdist {
1876     my ($editor, $override, $sdist) = @_;
1877
1878     $logger->info("distribution-alter: retrieving distribution ".$sdist->id);
1879     my $orig_sdist = $editor->retrieve_serial_distribution($sdist->id);
1880
1881     $logger->info("distribution-alter: original distribution ".OpenSRF::Utils::JSON->perl2JSON($orig_sdist));
1882     $logger->info("distribution-alter: updated distribution ".OpenSRF::Utils::JSON->perl2JSON($sdist));
1883     return $editor->event unless $editor->update_serial_distribution($sdist);
1884     return 0;
1885 }
1886
1887 __PACKAGE__->register_method(
1888     method  => "fleshed_serial_distribution_retrieve_batch",
1889     authoritative => 1,
1890     api_name    => "open-ils.serial.distribution.fleshed.batch.retrieve"
1891 );
1892
1893 sub fleshed_serial_distribution_retrieve_batch {
1894     my( $self, $client, $ids ) = @_;
1895 # FIXME: permissions?
1896     $logger->info("Fetching fleshed distributions @$ids");
1897     return $U->cstorereq(
1898         "open-ils.cstore.direct.serial.distribution.search.atomic",
1899         { id => $ids },
1900         { flesh => 1,
1901           flesh_fields => {sdist => [ qw/ holding_lib receive_call_number receive_unit_template bind_call_number bind_unit_template streams / ]}
1902         });
1903 }
1904
1905 ##########################################################################
1906 # caption and pattern methods
1907 #
1908 __PACKAGE__->register_method(
1909     method    => 'scap_alter',
1910     api_name  => 'open-ils.serial.caption_and_pattern.batch.update',
1911     api_level => 1,
1912     argc      => 2,
1913     signature => {
1914         desc     => 'Receives an array of one or more caption and patterns and updates the database as needed',
1915         'params' => [ {
1916                  name => 'authtoken',
1917                  desc => 'Authtoken for current user session',
1918                  type => 'string'
1919             },
1920             {
1921                  name => 'scaps',
1922                  desc => 'Array of caption and patterns',
1923                  type => 'array'
1924             }
1925
1926         ],
1927         'return' => {
1928             desc => 'Returns 1 if successful, event if failed',
1929             type => 'mixed'
1930         }
1931     }
1932 );
1933
1934 sub scap_alter {
1935     my( $self, $conn, $auth, $scaps ) = @_;
1936     return 1 unless ref $scaps;
1937     my( $reqr, $evt ) = $U->checkses($auth);
1938     return $evt if $evt;
1939     my $editor = new_editor(requestor => $reqr, xact => 1);
1940     my $override = $self->api_name =~ /override/;
1941
1942 # TODO: permission check
1943 #        return $editor->event unless
1944 #            $editor->allowed('UPDATE_COPY', $class->copy_perm_org($vol, $copy));
1945
1946     for my $scap (@$scaps) {
1947         my $scapid = $scap->id;
1948
1949         if( $scap->isdeleted ) {
1950             $evt = _delete_scap( $editor, $override, $scap);
1951         } elsif( $scap->isnew ) {
1952             $evt = _create_scap( $editor, $scap );
1953         } else {
1954             $evt = _update_scap( $editor, $override, $scap );
1955         }
1956     }
1957
1958     if( $evt ) {
1959         $logger->info("caption_and_pattern-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
1960         $editor->rollback;
1961         return $evt;
1962     }
1963     $logger->debug("caption_and_pattern-alter: done updating caption_and_pattern batch");
1964     $editor->commit;
1965     $logger->info("caption_and_pattern-alter successfully updated ".scalar(@$scaps)." caption_and_patterns");
1966     return 1;
1967 }
1968
1969 sub _delete_scap {
1970     my ($editor, $override, $scap) = @_;
1971     $logger->info("caption_and_pattern-alter: delete caption_and_pattern ".OpenSRF::Utils::JSON->perl2JSON($scap));
1972     my $sisses = $editor->search_serial_issuance(
1973             { caption_and_pattern => $scap->id }, { limit => 1 } ); #TODO: 'deleted' support?
1974     return OpenILS::Event->new(
1975             'SERIAL_CAPTION_AND_PATTERN_HAS_ISSUANCES', payload => $scap->id ) if (@$sisses);
1976
1977     return $editor->event unless $editor->delete_serial_caption_and_pattern($scap);
1978     return 0;
1979 }
1980
1981 sub _create_scap {
1982     my ($editor, $scap) = @_;
1983
1984     $logger->info("caption_and_pattern-alter: new caption_and_pattern ".OpenSRF::Utils::JSON->perl2JSON($scap));
1985     return $editor->event unless $editor->create_serial_caption_and_pattern($scap);
1986     return 0;
1987 }
1988
1989 sub _update_scap {
1990     my ($editor, $override, $scap) = @_;
1991
1992     $logger->info("caption_and_pattern-alter: retrieving caption_and_pattern ".$scap->id);
1993     my $orig_scap = $editor->retrieve_serial_caption_and_pattern($scap->id);
1994
1995     $logger->info("caption_and_pattern-alter: original caption_and_pattern ".OpenSRF::Utils::JSON->perl2JSON($orig_scap));
1996     $logger->info("caption_and_pattern-alter: updated caption_and_pattern ".OpenSRF::Utils::JSON->perl2JSON($scap));
1997     return $editor->event unless $editor->update_serial_caption_and_pattern($scap);
1998     return 0;
1999 }
2000
2001 __PACKAGE__->register_method(
2002     method  => "serial_caption_and_pattern_retrieve_batch",
2003     authoritative => 1,
2004     api_name    => "open-ils.serial.caption_and_pattern.batch.retrieve"
2005 );
2006
2007 sub serial_caption_and_pattern_retrieve_batch {
2008     my( $self, $client, $ids ) = @_;
2009     $logger->info("Fetching caption_and_patterns @$ids");
2010     return $U->cstorereq(
2011         "open-ils.cstore.direct.serial.caption_and_pattern.search.atomic",
2012         { id => $ids }
2013     );
2014 }
2015
2016 __PACKAGE__->register_method(
2017     "method" => "bre_by_identifier",
2018     "api_name" => "open-ils.serial.biblio.record_entry.by_identifier",
2019     "stream" => 1,
2020     "signature" => {
2021         "desc" => "Find instances of biblio.record_entry given a search token" .
2022             " that could be a value for any identifier defined in " .
2023             "config.metabib_field",
2024         "params" => [
2025             {"desc" => "Search token", "type" => "string"},
2026             {"desc" => "Options: require_subscriptions, add_mvr, is_actual_id" .
2027                 " (all boolean)", "type" => "object"}
2028         ],
2029         "return" => {
2030             "desc" => "Any matching BREs, or if the add_mvr option is true, " .
2031                 "objects with a 'bre' key/value pair, and an 'mvr' " .
2032                 "key-value pair.  BREs have subscriptions fleshed on.",
2033             "type" => "object"
2034         }
2035     }
2036 );
2037
2038 sub bre_by_identifier {
2039     my ($self, $client, $term, $options) = @_;
2040
2041     return new OpenILS::Event("BAD_PARAMS") unless $term;
2042
2043     $options ||= {};
2044     my $e = new_editor();
2045
2046     my @ids;
2047
2048     if ($options->{"is_actual_id"}) {
2049         @ids = ($term);
2050     } else {
2051         my $cmf =
2052             $e->search_config_metabib_field({"field_class" => "identifier"})
2053                 or return $e->die_event;
2054
2055         my @identifiers = map { $_->name } @$cmf;
2056         my $query = join(" || ", map { "id|$_: $term" } @identifiers);
2057
2058         my $search = create OpenSRF::AppSession("open-ils.search");
2059         my $search_result = $search->request(
2060             "open-ils.search.biblio.multiclass.query.staff", {}, $query
2061         )->gather(1);
2062         $search->disconnect;
2063
2064         # Un-nest results. They tend to look like [[1],[2],[3]] for some reason.
2065         @ids = map { @{$_} } @{$search_result->{"ids"}};
2066
2067         unless (@ids) {
2068             $e->disconnect;
2069             return undef;
2070         }
2071     }
2072
2073     my $bre = $e->search_biblio_record_entry([
2074         {"id" => \@ids}, {
2075             "flesh" => 2, "flesh_fields" => {
2076                 "bre" => ["subscriptions"],
2077                 "ssub" => ["owning_lib"]
2078             }
2079         }
2080     ]) or return $e->die_event;
2081
2082     if (@$bre && $options->{"require_subscriptions"}) {
2083         $bre = [ grep { @{$_->subscriptions} } @$bre ];
2084     }
2085
2086     $e->disconnect;
2087
2088     if (@$bre) { # re-evaluate after possible grep
2089         if ($options->{"add_mvr"}) {
2090             $client->respond(
2091                 {"bre" => $_, "mvr" => _get_mvr($_->id)}
2092             ) foreach (@$bre);
2093         } else {
2094             $client->respond($_) foreach (@$bre);
2095         }
2096     }
2097
2098     undef;
2099 }
2100
2101 __PACKAGE__->register_method(
2102     "method" => "get_receivable_items",
2103     "api_name" => "open-ils.serial.items.receivable.by_subscription",
2104     "stream" => 1,
2105     "signature" => {
2106         "desc" => "Return all receivable items under a given subscription",
2107         "params" => [
2108             {"desc" => "Authtoken", "type" => "string"},
2109             {"desc" => "Subscription ID", "type" => "number"},
2110         ],
2111         "return" => {
2112             "desc" => "All receivable items under a given subscription",
2113             "type" => "object"
2114         }
2115     }
2116 );
2117
2118 __PACKAGE__->register_method(
2119     "method" => "get_receivable_items",
2120     "api_name" => "open-ils.serial.items.receivable.by_issuance",
2121     "stream" => 1,
2122     "signature" => {
2123         "desc" => "Return all receivable items under a given issuance",
2124         "params" => [
2125             {"desc" => "Authtoken", "type" => "string"},
2126             {"desc" => "Issuance ID", "type" => "number"},
2127         ],
2128         "return" => {
2129             "desc" => "All receivable items under a given issuance",
2130             "type" => "object"
2131         }
2132     }
2133 );
2134
2135 sub get_receivable_items {
2136     my ($self, $client, $auth, $term)  = @_;
2137
2138     my $e = new_editor("authtoken" => $auth);
2139     return $e->die_event unless $e->checkauth;
2140
2141     # XXX permissions
2142
2143     my $by = ($self->api_name =~ /by_(\w+)$/)[0];
2144
2145     my %where = (
2146         "issuance" => {"issuance" => $term},
2147         "subscription" => {"+siss" => {"subscription" => $term}}
2148     );
2149
2150     my $item_ids = $e->json_query(
2151         {
2152             "select" => {"sitem" => ["id"]},
2153             "from" => {"sitem" => "siss"},
2154             "where" => {
2155                 %{$where{$by}}, "date_received" => undef
2156             },
2157             "order_by" => {"sitem" => ["id"]}
2158         }
2159     ) or return $e->die_event;
2160
2161     return undef unless @$item_ids;
2162
2163     foreach (map { $_->{"id"} } @$item_ids) {
2164         $client->respond(
2165             $e->retrieve_serial_item([
2166                 $_, {
2167                     "flesh" => 3,
2168                     "flesh_fields" => {
2169                         "sitem" => ["stream", "issuance"],
2170                         "sstr" => ["distribution"],
2171                         "sdist" => ["holding_lib"]
2172                     }
2173                 }
2174             ])
2175         );
2176     }
2177
2178     $e->disconnect;
2179     undef;
2180 }
2181
2182 __PACKAGE__->register_method(
2183     "method" => "get_receivable_issuances",
2184     "api_name" => "open-ils.serial.issuances.receivable",
2185     "stream" => 1,
2186     "signature" => {
2187         "desc" => "Return all issuances with receivable items given " .
2188             "a subscription ID",
2189         "params" => [
2190             {"desc" => "Authtoken", "type" => "string"},
2191             {"desc" => "Subscription ID", "type" => "number"},
2192         ],
2193         "return" => {
2194             "desc" => "All issuances with receivable items " .
2195                 "(but not the items themselves)", "type" => "object"
2196         }
2197     }
2198 );
2199
2200 sub get_receivable_issuances {
2201     my ($self, $client, $auth, $sub_id) = @_;
2202
2203     my $e = new_editor("authtoken" => $auth);
2204     return $e->die_event unless $e->checkauth;
2205
2206     # XXX permissions
2207
2208     my $issuance_ids = $e->json_query({
2209         "select" => {
2210             "siss" => [
2211                 {"transform" => "distinct", "column" => "id"},
2212                 "date_published"
2213             ]
2214         },
2215         "from" => {"siss" => "sitem"},
2216         "where" => {
2217             "subscription" => $sub_id,
2218             "+sitem" => {"date_received" => undef}
2219         },
2220         "order_by" => {
2221             "siss" => {"date_published" => {"direction" => "asc"}}
2222         }
2223
2224     }) or return $e->die_event;
2225
2226     $client->respond($e->retrieve_serial_issuance($_->{"id"}))
2227         foreach (@$issuance_ids);
2228
2229     $e->disconnect;
2230     undef;
2231 }
2232
2233 1;