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