]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/lib/OpenILS/Application/Serial.pm
04f79c0de3c5eeb34d1908fef3e369a30f3cf458
[working/Evergreen.git] / Open-ILS / src / perlmods / lib / 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 DateTime::Format::ISO8601;
52 use MARC::File::XML (BinaryEncoding => 'utf8');
53
54 use OpenILS::Application::Serial::OPAC;
55
56 my $U = 'OpenILS::Application::AppUtils';
57 my @MFHD_NAMES = ('basic','supplement','index');
58 my %MFHD_NAMES_BY_TAG = (  '853' => $MFHD_NAMES[0],
59                         '863' => $MFHD_NAMES[0],
60                         '854' => $MFHD_NAMES[1],
61                         '864' => $MFHD_NAMES[1],
62                         '855' => $MFHD_NAMES[2],
63                         '865' => $MFHD_NAMES[2] );
64 my %MFHD_TAGS_BY_NAME = (  $MFHD_NAMES[0] => '853',
65                         $MFHD_NAMES[1] => '854',
66                         $MFHD_NAMES[2] => '855');
67 my $_strp_date = new DateTime::Format::Strptime(pattern => '%F');
68 my %FM_NAME_TO_ID = (
69     'subscription' => 'ssub',
70     'distribution' => 'sdist',
71     'item' => 'sitem'
72     );
73
74 # helper method for conforming dates to ISO8601
75 sub _cleanse_dates {
76     my $item = shift;
77     my $fields = shift;
78
79     foreach my $field (@$fields) {
80         $item->$field(OpenSRF::Utils::clense_ISO8601($item->$field)) if $item->$field;
81     }
82     return 0;
83 }
84
85 sub _get_mvr {
86     $U->simplereq(
87         "open-ils.search",
88         "open-ils.search.biblio.record.mods_slim.retrieve",
89         @_
90     );
91 }
92
93
94 ##########################################################################
95 # item methods
96 #
97 __PACKAGE__->register_method(
98     method    => "create_item_safely",
99     api_name  => "open-ils.serial.item.create",
100     api_level => 1,
101     stream    => 1,
102     argc      => 3,
103     signature => {
104         desc => q/Creates any number of items, respecting only a few of the
105         submitted fields, as the user shouldn't be able to freely set certain
106         ones/,
107         params => [
108             {name=> "authtoken", desc => "Authtoken for current user session",
109                 type => "string"},
110             {name => "item", desc => "serial item",
111                 type => "object", class => "sitem"},
112             {name => "count",
113                 desc => "optional: how many items to make " .
114                     "(default 1; 1-100 permitted)",
115                 type => "number"}
116         ],
117         return => {
118             desc => "created items (a stream of them)",
119             type => "object", class => "sitem"
120         }
121     }
122 );
123 __PACKAGE__->register_method(
124     method    => "update_item_safely",
125     api_name  => "open-ils.serial.item.update",
126     api_level => 1,
127     stream    => 1,
128     argc      => 2,
129     signature => {
130         desc => q/Edit a serial item, respecting only a few of the
131         submitted fields, as the user shouldn't be able to freely set certain
132         ones/,
133         params => [
134             {name=> "authtoken", desc => "Authtoken for current user session",
135                 type => "string"},
136             {name => "item", desc => "serial item",
137                 type => "object", class => "sitem"},
138         ],
139         return => {
140             desc => "created item", type => "object", class => "sitem"
141         }
142     }
143 );
144
145 sub _set_safe_item_fields {
146     my $dest = shift;
147     my $source = shift;
148     my $requestor_id = shift;
149     # extra fields remain in @_
150
151     $dest->edit_date("now");
152     $dest->editor($requestor_id);
153
154     my @fields = qw/date_expected date_received status/;
155
156     for my $field (@fields, @_) {
157         $dest->$field($source->$field);
158     }
159 }
160
161 sub update_item_safely {
162     my ($self, $client, $auth, $item) = @_;
163
164     my $e = new_editor("xact" => 1, "authtoken" => $auth);
165     $e->checkauth or return $e->die_event;
166
167     my $orig = $e->retrieve_serial_item([
168         $item->id, {
169             "flesh" => 2, "flesh_fields" => {
170                 "sitem" => ["stream"], "sstr" => ["distribution"]
171             }
172         }
173     ]) or return $e->die_event;
174
175     return $e->die_event unless $e->allowed(
176         "ADMIN_SERIAL_ITEM", $orig->stream->distribution->holding_lib
177     );
178
179     _set_safe_item_fields($orig, $item, $e->requestor->id);
180     $e->update_serial_item($orig) or return $e->die_event;
181
182     $client->respond($e->retrieve_serial_item($item->id));
183     $e->commit or return $e->die_event;
184     undef;
185 }
186
187 sub create_item_safely {
188     my ($self, $client, $auth, $item, $count) = @_;
189
190     $count = int $count;
191     $count ||= 1;
192     return new OpenILS::Event(
193         "BAD_PARAMS", note => "Count should be from 1 to 100"
194     ) unless $count >= 1 and $count <= 100;
195
196     my $e = new_editor("xact" => 1, "authtoken" => $auth);
197     $e->checkauth or return $e->die_event;
198
199     my $stream = $e->retrieve_serial_stream([
200         $item->stream, {
201             "flesh" => 1, "flesh_fields" => {"sstr" => ["distribution"]}
202         }
203     ]) or return $e->die_event;
204
205     return $e->die_event unless $e->allowed(
206         "ADMIN_SERIAL_ITEM", $stream->distribution->holding_lib
207     );
208
209     for (my $i = 0; $i < $count; $i++) {
210         my $actual = new Fieldmapper::serial::item;
211         $actual->creator($e->requestor->id);
212         _set_safe_item_fields(
213             $actual, $item, $e->requestor->id, "issuance", "stream"
214         );
215
216         $e->create_serial_item($actual) or return $e->die_event;
217         $client->respond($e->data);
218     }
219
220     $e->commit or return $e->die_event;
221     undef;
222 }
223
224 __PACKAGE__->register_method(
225     method    => 'fleshed_item_alter',
226     api_name  => 'open-ils.serial.item.fleshed.batch.update',
227     api_level => 1,
228     argc      => 2,
229     signature => {
230         desc     => 'Receives an array of one or more items and updates the database as needed',
231         'params' => [ {
232                  name => 'authtoken',
233                  desc => 'Authtoken for current user session',
234                  type => 'string'
235             },
236             {
237                  name => 'items',
238                  desc => 'Array of fleshed items',
239                  type => 'array'
240             }
241
242         ],
243         'return' => {
244             desc => 'Returns 1 if successful, event if failed',
245             type => 'mixed'
246         }
247     }
248 );
249
250 sub fleshed_item_alter {
251     my( $self, $conn, $auth, $items ) = @_;
252     return 1 unless ref $items;
253     my( $reqr, $evt ) = $U->checkses($auth);
254     return $evt if $evt;
255     my $editor = new_editor(requestor => $reqr, xact => 1);
256     my $override = $self->api_name =~ /override/;
257
258     my %found_sdist_ids;
259     my %found_sstr_ids;
260     my %siss_to_potentially_delete;
261     for my $item (@$items) {
262         my $sstr_id = ref $item->stream ? $item->stream->id : $item->stream;
263         if (!exists($found_sstr_ids{$sstr_id})) {
264             my $sstr;
265             if (ref $item->stream) {
266                 $sstr = $item->stream;
267             } else {
268                 $sstr = $editor->retrieve_serial_stream($item->stream) or return $editor->die_event;
269             }
270             if (!exists($found_sdist_ids{$sstr->distribution})) {
271                 my $sdist = $editor->retrieve_serial_distribution($sstr->distribution) or return $editor->die_event;
272                 return $editor->die_event unless
273                     $editor->allowed("ADMIN_SERIAL_STREAM", $sdist->holding_lib);
274                 $found_sdist_ids{$sstr->distribution} = 1;
275             }
276             $found_sstr_ids{$sstr_id} = 1;
277         }
278
279         $item->editor($editor->requestor->id);
280         $item->edit_date('now');
281
282         if( $item->isdeleted ) {
283             my $siss_id = ref $item->issuance ? $item->issuance->id : $item->issuance;
284             $siss_to_potentially_delete{$siss_id}++;
285             $evt = _delete_sitem( $editor, $override, $item);
286         } elsif( $item->isnew ) {
287             # TODO: reconsider this
288             # if the item has a new issuance, create the issuance first
289             if (ref $item->issuance eq 'Fieldmapper::serial::issuance' and $item->issuance->isnew) {
290                 fleshed_issuance_alter($self, $conn, $auth, [$item->issuance]);
291             }
292             _cleanse_dates($item, ['date_expected','date_received']);
293             $evt = _create_sitem( $editor, $item );
294         } else {
295             _cleanse_dates($item, ['date_expected','date_received']);
296             $evt = _update_sitem( $editor, $override, $item );
297         }
298     }
299
300     if( $evt ) {
301         $logger->info("fleshed item-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
302         $editor->rollback;
303         return $evt;
304     }
305     if( %siss_to_potentially_delete ) {
306         foreach my $id (keys %siss_to_potentially_delete) {
307             my $issuance = $editor->retrieve_serial_issuance([
308                 $id, {
309                     "flesh" => 1, "flesh_fields" => {
310                         "siss" => ["items"],
311                     }
312                 }
313             ]);
314             unless ($issuance) {
315                 $logger->warn("fleshed item-alter failed to retrieve issuance $id to potenitally delete");
316                 $editor->rollback;
317                 return $editor->die_event;
318             }
319             unless (@{ $issuance->items }) {
320                 $logger->info("fleshed item-alter deleting issuance $id as it has no items left");
321                 $evt = _delete_siss( $editor, $override, $issuance);
322                 if( $evt ) {
323                     $logger->info("fleshed item-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
324                     $editor->rollback;
325                     return $evt;
326                 }
327             }
328         }
329     }
330     $logger->debug("item-alter: done updating item batch");
331     $editor->commit;
332     $logger->info("fleshed item-alter successfully updated ".scalar(@$items)." items");
333     return 1;
334 }
335
336 sub _delete_sitem {
337     my ($editor, $override, $item) = @_;
338     $logger->info("item-alter: delete item ".OpenSRF::Utils::JSON->perl2JSON($item));
339     return $editor->event unless $editor->delete_serial_item($item);
340     return 0;
341 }
342
343 sub _create_sitem {
344     my ($editor, $item) = @_;
345
346     $item->creator($editor->requestor->id);
347     $item->create_date('now');
348
349     $logger->info("item-alter: new item ".OpenSRF::Utils::JSON->perl2JSON($item));
350     return $editor->event unless $editor->create_serial_item($item);
351     return 0;
352 }
353
354 sub _update_sitem {
355     my ($editor, $override, $item) = @_;
356
357     $logger->info("item-alter: retrieving item ".$item->id);
358     my $orig_item = $editor->retrieve_serial_item($item->id);
359
360     $logger->info("item-alter: original item ".OpenSRF::Utils::JSON->perl2JSON($orig_item));
361     $logger->info("item-alter: updated item ".OpenSRF::Utils::JSON->perl2JSON($item));
362     return $editor->event unless $editor->update_serial_item($item);
363     return 0;
364 }
365
366 __PACKAGE__->register_method(
367     method  => "fleshed_serial_item_retrieve_batch",
368     authoritative => 1,
369     api_name    => "open-ils.serial.item.fleshed.batch.retrieve"
370 );
371
372 sub fleshed_serial_item_retrieve_batch {
373     my( $self, $client, $ids ) = @_;
374 # FIXME: permissions?
375     $logger->info("Fetching fleshed serial items @$ids");
376     return $U->cstorereq(
377         "open-ils.cstore.direct.serial.item.search.atomic",
378         { id => $ids },
379         { flesh => 2,
380           flesh_fields => {sitem => [ qw/issuance creator editor stream unit notes/ ], sunit => ["call_number"], siss => [qw/creator editor subscription/]}
381         });
382 }
383
384
385 ##########################################################################
386 # issuance methods
387 #
388 __PACKAGE__->register_method(
389     method    => 'fleshed_issuance_alter',
390     api_name  => 'open-ils.serial.issuance.fleshed.batch.update',
391     api_level => 1,
392     argc      => 2,
393     signature => {
394         desc     => 'Receives an array of one or more issuances and updates the database as needed',
395         'params' => [ {
396                  name => 'authtoken',
397                  desc => 'Authtoken for current user session',
398                  type => 'string'
399             },
400             {
401                  name => 'issuances',
402                  desc => 'Array of fleshed issuances',
403                  type => 'array'
404             }
405
406         ],
407         'return' => {
408             desc => 'Returns 1 if successful, event if failed',
409             type => 'mixed'
410         }
411     }
412 );
413
414 sub fleshed_issuance_alter {
415     my( $self, $conn, $auth, $issuances ) = @_;
416     return 1 unless ref $issuances;
417     my( $reqr, $evt ) = $U->checkses($auth);
418     return $evt if $evt;
419     my $editor = new_editor(authtoken => $auth, requestor => $reqr, xact => 1);
420     my $override = $self->api_name =~ /override/;
421
422     my %found_ssub_ids;
423     my %regen_ssub_ids;
424     for my $issuance (@$issuances) {
425         my $ssub_id = ref $issuance->subscription ? $issuance->subscription->id : $issuance->subscription;
426         if (!exists($found_ssub_ids{$ssub_id})) {
427             my $owning_lib_id;
428             if (ref $issuance->subscription) {
429                 $owning_lib_id = $issuance->subscription->owning_lib;
430             } else {
431                 my $ssub = $editor->retrieve_serial_subscription($issuance->subscription) or return $editor->die_event;
432                 $owning_lib_id = $ssub->owning_lib;
433             }
434             return $editor->die_event unless
435                 $editor->allowed("ADMIN_SERIAL_SUBSCRIPTION", $owning_lib_id);
436             $found_ssub_ids{$ssub_id} = 1;
437         }
438
439         my $issuanceid = $issuance->id;
440         $issuance->editor($editor->requestor->id);
441         $issuance->edit_date('now');
442
443         if( $issuance->isdeleted ) {
444             $evt = _delete_siss( $editor, $override, $issuance);
445             $regen_ssub_ids{$ssub_id} = 1;
446         } elsif( $issuance->isnew ) {
447             _cleanse_dates($issuance, ['date_published']);
448             $evt = _create_siss( $editor, $issuance );
449         } else {
450             _cleanse_dates($issuance, ['date_published']);
451             $evt = _update_siss( $editor, $override, $issuance );
452         }
453
454         last if $evt;
455     }
456
457     if (!$evt) {
458         # if we deleted any issuances, update the summaries
459         # for all dists in those ssubs
460         my @ssub_ids = keys %regen_ssub_ids;
461         $evt = _regenerate_summaries($editor, {'ssub_ids' => \@ssub_ids}) if @ssub_ids;
462     }
463
464     if ( $evt ) {
465         $logger->info("fleshed issuance-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
466         $editor->rollback;
467         return $evt;
468     }
469
470     $logger->debug("issuance-alter: done updating issuance batch");
471     $editor->commit;
472     $logger->info("fleshed issuance-alter successfully updated ".scalar(@$issuances)." issuances");
473     return 1;
474 }
475
476 sub _delete_siss {
477     my ($editor, $override, $issuance) = @_;
478     $logger->info("issuance-alter: delete issuance ".OpenSRF::Utils::JSON->perl2JSON($issuance));
479     return $editor->event unless $editor->delete_serial_issuance($issuance);
480     return 0;
481 }
482
483 sub _create_siss {
484     my ($editor, $issuance) = @_;
485
486     $issuance->creator($editor->requestor->id);
487     $issuance->create_date('now');
488
489     $logger->info("issuance-alter: new issuance ".OpenSRF::Utils::JSON->perl2JSON($issuance));
490     return $editor->event unless $editor->create_serial_issuance($issuance);
491     return 0;
492 }
493
494 sub _update_siss {
495     my ($editor, $override, $issuance) = @_;
496
497     $logger->info("issuance-alter: retrieving issuance ".$issuance->id);
498     my $orig_issuance = $editor->retrieve_serial_issuance($issuance->id);
499
500     $logger->info("issuance-alter: original issuance ".OpenSRF::Utils::JSON->perl2JSON($orig_issuance));
501     $logger->info("issuance-alter: updated issuance ".OpenSRF::Utils::JSON->perl2JSON($issuance));
502     return $editor->event unless $editor->update_serial_issuance($issuance);
503     return 0;
504 }
505
506 __PACKAGE__->register_method(
507     method  => "fleshed_serial_issuance_retrieve_batch",
508     authoritative => 1,
509     api_name    => "open-ils.serial.issuance.fleshed.batch.retrieve"
510 );
511
512 sub fleshed_serial_issuance_retrieve_batch {
513     my( $self, $client, $ids ) = @_;
514 # FIXME: permissions?
515     $logger->info("Fetching fleshed serial issuances @$ids");
516     return $U->cstorereq(
517         "open-ils.cstore.direct.serial.issuance.search.atomic",
518         { id => $ids },
519         { flesh => 1,
520           flesh_fields => {siss => [ qw/creator editor subscription/ ]}
521         });
522 }
523
524 __PACKAGE__->register_method(
525     method  => "pub_fleshed_serial_issuance_retrieve_batch",
526     api_name    => "open-ils.serial.issuance.pub_fleshed.batch.retrieve",
527     signature => {
528         desc => q/
529             Public (i.e. OPAC) call for getting at the sub and 
530             ultimately the record entry from an issuance
531         /,
532         params => [{name => 'ids', desc => 'Array of IDs', type => 'array'}],
533         return => {
534             desc => q/
535                 issuance objects, fleshed with subscriptions
536             /,
537             class => 'siss'
538         }
539     }
540 );
541 sub pub_fleshed_serial_issuance_retrieve_batch {
542     my( $self, $client, $ids ) = @_;
543     return [] unless $ids and @$ids;
544     return new_editor()->search_serial_issuance([
545         { id => $ids },
546         { 
547             flesh => 1,
548             flesh_fields => {siss => [ qw/subscription/ ]}
549         }
550     ]);
551 }
552
553 sub received_siss_by_bib {
554     # XXX this is somewhat wrong in implementation and should not be used in
555     # new places - senator
556     my $self = shift;
557     my $client = shift;
558     my $bib = shift;
559
560     my $args = shift || {};
561     $$args{order} ||= 'asc';
562
563     my $global = $$args{global} == 0 ? 0 : 1;
564
565     my $e = new_editor();
566     my $issuances = $e->json_query({
567         select  => {
568             siss => [
569                 $global ? { transform => "min", column => "id", aggregate => 1 } : "id",
570                 "label",
571                 "date_published"
572             ],
573             "sitem" => [
574                 # We're not really interested in the minimum here.  This is
575                 # just a way to distinguish issuances whose items have units
576                 # from issuances whose items have no units, without altogether
577                 # excluding the latter type of issuances.
578                 {"transform" => "min", "alias" => "has_units",
579                     "column" => "unit", "aggregate" => 1}
580             ]
581         },
582         from => {
583             ssub => {
584                 siss => {
585                     field => 'subscription',
586                     fkey  => 'id',
587                     join  => {
588                         sitem => {
589                             field  => 'issuance',
590                             fkey   => 'id',
591                             $$args{ou} ? ( join  => {
592                                 sstr => {
593                                     field => 'id',
594                                     fkey  => 'stream',
595                                     join  => {
596                                         sdist => {
597                                             field  => 'id',
598                                             fkey   => 'distribution'
599                                         }
600                                     }
601                                 }
602                             }) : ()
603                         }
604                     }
605                 }
606             }
607         },
608         where => {
609             '+ssub'  => { record_entry => $bib },
610             $$args{type} ? ( '+siss' => { 'holding_type' => $$args{type} } ) : (),
611             '+sitem' => {
612                 # XXX should we also take specific item statuses into account?
613                 date_received => { '!=' => undef },
614                 $$args{status} ? ( 'status' => $$args{status} ) : ()
615             },
616             $$args{ou} ? ( '+sdist' => {
617                 holding_lib => {
618                     'in' => $U->get_org_descendants($$args{ou}, $$args{depth})
619                 }
620             }) : ()
621         },
622         $$args{limit}  ? ( limit  => $$args{limit}  ) : (),
623         $$args{offset} ? ( offset => $$args{offset} ) : (),
624         order_by => [{ class => 'siss', field => 'date_published', direction => $$args{order} }],
625         distinct => 1
626     });
627
628     $client->respond({
629         "issuance" => $e->retrieve_serial_issuance($_->{"id"}),
630         "has_units" => $_->{"has_units"} ? 1 : 0
631     }) for @$issuances;
632
633     return undef;
634 }
635 __PACKAGE__->register_method(
636     method    => 'received_siss_by_bib',
637     api_name  => 'open-ils.serial.received_siss.retrieve.by_bib',
638     api_level => 1,
639     argc      => 1,
640     stream    => 1,
641     signature => {
642         desc   => 'Receives a Bib ID and other optional params and returns "siss" (issuance) objects',
643         params => [
644             {   name => 'bibid',
645                 desc => 'id of the bre to which the issuances belong',
646                 type => 'number'
647             },
648             {   name => 'args',
649                 desc =>
650 q/A hash of optional arguments.  Valid keys and their meanings:
651     global := If true, return only one representative version of a conceptual issuance regardless of the number of subscriptions, otherwise return all issuance objects meeting the requested criteria, including conceptual duplicates. Valid values are 0 (false) and 1 (true, default).
652     order  := date_published sort direction, either "asc" (chronological, default) or "desc" (reverse chronological)
653     limit  := Number of issuances to return.  Useful for paging results, or finding the oldest or newest
654     offset := Number of issuance to skip before returning results.  Useful for paging.
655     orgid  := OU id used to scope retrieval, based on distribution.holding_lib
656     depth  := OU depth used to range the scope of orgid
657     type   := Holding type filter. Valid values are "basic", "supplement" and "index". Can be a scalar (one) or arrayref (one or more).
658     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).
659 /
660             }
661         ]
662     }
663 );
664
665
666 sub scoped_bib_holdings_summary {
667     # XXX this is somewhat wrong in implementation and should not be used in
668     # new places - senator
669     my $self = shift;
670     my $client = shift;
671     my $bibid = shift;
672     my $args = shift || {};
673
674     $args->{order} = 'asc';
675
676     my ($issuances) = $self->method_lookup('open-ils.serial.received_siss.retrieve.by_bib.atomic')->run( $bibid => $args );
677
678     # split into issuance type sets
679     my %type_blob = (basic => [], supplement => [], index => []);
680     push @{ $type_blob{ $_->{"issuance"}->holding_type } }, $_->{"issuance"}
681         for (@$issuances);
682
683     # generate a statement list for each type
684     my %statement_blob;
685     for my $type ( keys %type_blob ) {
686         my ($mfhd,$list) = _summarize_contents(new_editor(), $type_blob{$type});
687
688         return {} if $U->event_code($mfhd); # _summarize_contents() failed, bad data?
689
690         $statement_blob{$type} = $list;
691     }
692
693     return \%statement_blob;
694 }
695 __PACKAGE__->register_method(
696     method    => 'scoped_bib_holdings_summary',
697     api_name  => 'open-ils.serial.bib.summary_statements',
698     api_level => 1,
699     argc      => 1,
700     signature => {
701         desc   => '** DEPRECATED and only used by JSPAC. Somewhat wrong in implementation. *** Receives a Bib ID and other optional params and returns set of holdings statements',
702         params => [
703             {   name => 'bibid',
704                 desc => 'id of the bre to which the issuances belong',
705                 type => 'number'
706             },
707             {   name => 'args',
708                 desc =>
709 q/A hash of optional arguments.  Valid keys and their meanings:
710     orgid  := OU id used to scope retrieval, based on distribution.holding_lib
711     depth  := OU depth used to range the scope of orgid
712     type   := Holding type filter. Valid values are "basic", "supplement" and "index". Can be a scalar (one) or arrayref (one or more).
713     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).
714 /
715             }
716         ]
717     }
718 );
719
720
721 ##########################################################################
722 # unit methods
723 #
724 __PACKAGE__->register_method(
725     method    => 'fleshed_sunit_alter',
726     api_name  => 'open-ils.serial.sunit.fleshed.batch.update',
727     api_level => 1,
728     argc      => 2,
729     signature => {
730         desc     => 'Receives an array of one or more Units and updates the database as needed',
731         'params' => [ {
732                  name => 'authtoken',
733                  desc => 'Authtoken for current user session',
734                  type => 'string'
735             },
736             {
737                  name => 'sunits',
738                  desc => 'Array of fleshed Units',
739                  type => 'array'
740             }
741
742         ],
743         'return' => {
744             desc => 'Returns 1 if successful, event if failed',
745             type => 'mixed'
746         }
747     }
748 );
749
750 sub fleshed_sunit_alter {
751     my( $self, $conn, $auth, $sunits ) = @_;
752     return 1 unless ref $sunits;
753     my( $reqr, $evt ) = $U->checkses($auth);
754     return $evt if $evt;
755     my $editor = new_editor(requestor => $reqr, xact => 1);
756     my $override = $self->api_name =~ /override/;
757
758     my %found_cn_ids;
759     for my $sunit (@$sunits) {
760         my $cn_id = ref $sunit->call_number ? $sunit->call_number->id : $sunit->call_number;
761         if (!exists($found_cn_ids{$cn_id})) {
762             my $owning_lib_id;
763             if (ref $sunit->call_number) {
764                 $owning_lib_id = $sunit->call_number->owning_lib;
765             } else {
766                 my $cn = $editor->retrieve_asset_call_number($sunit->call_number) or return $editor->die_event;
767                 $owning_lib_id = $cn->owning_lib;
768             }
769             return $editor->die_event unless
770                 $editor->allowed("UPDATE_COPY", $owning_lib_id);
771             $found_cn_ids{$cn_id} = 1;
772         }
773
774         if( $sunit->isdeleted ) {
775             $evt = _delete_sunit( $editor, $override, $sunit );
776         } else {
777             $sunit->default_location( $sunit->default_location->id ) if ref $sunit->default_location;
778
779             if( $sunit->isnew ) {
780                 $evt = _create_sunit( $editor, $sunit );
781             } else {
782                 $evt = _update_sunit( $editor, $override, $sunit );
783             }
784         }
785     }
786
787     if( $evt ) {
788         $logger->info("fleshed sunit-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
789         $editor->rollback;
790         return $evt;
791     }
792     $logger->debug("sunit-alter: done updating sunit batch");
793     $editor->commit;
794     $logger->info("fleshed sunit-alter successfully updated ".scalar(@$sunits)." Units");
795     return 1;
796 }
797
798 sub _delete_sunit {
799     my ($editor, $override, $sunit) = @_;
800     $logger->info("sunit-alter: delete sunit ".OpenSRF::Utils::JSON->perl2JSON($sunit));
801     return $editor->event unless $editor->delete_serial_unit($sunit);
802     return 0;
803 }
804
805 sub _create_sunit {
806     my ($editor, $sunit) = @_;
807
808     # The unique barcode constraint does not span asset.copy and serial.unit.
809     # ensure the barcode on the new unit does not collide with an existing
810     # asset.copy barcode.
811     my $existing = $editor->search_asset_copy(
812         {deleted => 'f', barcode => $sunit->barcode})->[0];
813
814     if (!$existing) {
815         # The DB will prevent duplicate serial.unit barcodes, but for 
816         # consistency (and a more specific error message for the
817         # user), prevent creation attempts on serial unit barcode
818         # collisions as well.
819         $existing = $editor->search_serial_unit(
820             {deleted => 'f', barcode => $sunit->barcode})->[0];
821     }
822
823     if ($existing) {
824         $editor->rollback;
825         return new OpenILS::Event(
826             'SERIAL_UNIT_BARCODE_COLLISION', note => 
827             'Serial unit barcode collides with existing unit/copy barcode',
828             payload => {barcode => $sunit->barcode}
829         );
830     }
831
832     $logger->info("sunit-alter: new Unit ".OpenSRF::Utils::JSON->perl2JSON($sunit));
833     return $editor->die_event unless $editor->create_serial_unit($sunit);
834     return 0;
835 }
836
837 sub _update_sunit {
838     my ($editor, $override, $sunit) = @_;
839
840     $logger->info("sunit-alter: retrieving sunit ".$sunit->id);
841     my $orig_sunit = $editor->retrieve_serial_unit($sunit->id);
842
843     $logger->info("sunit-alter: original sunit ".OpenSRF::Utils::JSON->perl2JSON($orig_sunit));
844     $logger->info("sunit-alter: updated sunit ".OpenSRF::Utils::JSON->perl2JSON($sunit));
845     return $editor->event unless $editor->update_serial_unit($sunit);
846     return 0;
847 }
848
849 __PACKAGE__->register_method(
850     method  => "retrieve_unit_list",
851     authoritative => 1,
852     api_name    => "open-ils.serial.unit_list.retrieve"
853 );
854
855 sub retrieve_unit_list {
856
857     my( $self, $client, @sdist_ids ) = @_;
858
859     if(ref($sdist_ids[0])) { @sdist_ids = @{$sdist_ids[0]}; }
860
861     my $e = new_editor();
862
863     my $query = {
864         'select' => 
865             { 'sunit' => [ 'id', 'summary_contents', 'sort_key' ],
866               'sitem' => ['stream'],
867               'sstr' => ['distribution'],
868               'sdist' => [{'column' => 'label', 'alias' => 'sdist_label'}]
869             },
870         'from' =>
871             { 'sdist' =>
872                 { 'sstr' =>
873                     { 'join' =>
874                         { 'sitem' =>
875                             { 'join' => { 'sunit' => {} } }
876                         }
877                     }
878                 }
879             },
880         'distinct' => 'true',
881         'where' => { '+sdist' => {'id' => \@sdist_ids} },
882         'order_by' => [{'class' => 'sunit', 'field' => 'sort_key'}]
883     };
884
885     my $unit_list_entries = $e->json_query($query);
886     
887     my @entries;
888     foreach my $entry (@$unit_list_entries) {
889         my $value = {'sunit' => $entry->{id}, 'sstr' => $entry->{stream}, 'sdist' => $entry->{distribution}};
890         my $label = $entry->{summary_contents};
891         if (length($label) > 100) {
892             $label = substr($label, 0, 100) . '...'; # limited space in dropdown / menu
893         }
894         $label = "[$entry->{sdist_label}/$entry->{stream} #$entry->{id}] " . $label;
895         push (@entries, [$label, OpenSRF::Utils::JSON->perl2JSON($value)]);
896     }
897
898     return \@entries;
899 }
900
901
902
903 ##########################################################################
904 # predict and receive methods
905 #
906 __PACKAGE__->register_method(
907     method    => 'make_predictions',
908     api_name  => 'open-ils.serial.make_predictions',
909     api_level => 1,
910     argc      => 1,
911     signature => {
912         desc     => 'Receives an ssub id and populates the issuance and item tables',
913         'params' => [ {
914                  name => 'ssub_id',
915                  desc => 'Serial Subscription ID',
916                  type => 'int'
917             }
918         ]
919     }
920 );
921
922 sub make_predictions {
923     my ($self, $conn, $authtoken, $args) = @_;
924
925     my $ssub_id = $args->{ssub_id};
926
927     my $editor = OpenILS::Utils::CStoreEditor->new();
928     my $ssub = $editor->retrieve_serial_subscription([$ssub_id]);
929     my $sdists = $editor->search_serial_distribution( [{ subscription => $ssub->id }, { flesh => 1, flesh_fields => {sdist => [ qw/ streams / ]} }] ); #TODO: 'deleted' support?
930
931     return store_predictions(
932         $self, $conn, $authtoken, $args, $ssub, $sdists,
933         make_prediction_values($self, $conn, $authtoken, $args, $ssub, $sdists, $editor)
934     );
935 }
936
937 __PACKAGE__->register_method(
938     method    => 'make_prediction_values',
939     api_name  => 'open-ils.serial.make_prediction_values',
940     api_level => 1,
941     argc      => 1,
942     signature => {
943         desc     => 'Receives an ssub id and returns objects that can be used to populate the issuance and item tables',
944         'params' => [ {
945                  name => 'ssub_id',
946                  desc => 'Serial Subscription ID',
947                  type => 'int'
948             }
949         ]
950     }
951 );
952
953 sub make_prediction_values {
954     my ($self, $conn, $authtoken, $args, $ssub, $sdists, $editor) = @_;
955     $logger->debug('make_prediction_values with args: ' . OpenSRF::Utils::JSON->perl2JSON($args));
956
957     my $ssub_id = $args->{ssub_id};
958
959     $editor ||= OpenILS::Utils::CStoreEditor->new();
960     $ssub ||= $editor->retrieve_serial_subscription([$ssub_id]);
961     $sdists ||= $editor->search_serial_distribution( [{ subscription => $ssub->id }, { flesh => 1, flesh_fields => {sdist => [ qw/ streams / ]} }] ); #TODO: 'deleted' support?
962
963     my $scaps = $editor->search_serial_caption_and_pattern({ subscription => $ssub_id, active => 't'});
964     my $mfhd = MFHD->new(MARC::Record->new());
965
966     my $total_streams = 0;
967     foreach (@$sdists) {
968         $total_streams += scalar(@{$_->streams});
969     }
970     if ($total_streams < 1) {
971         $editor->disconnect;
972         # XXX TODO new event type
973         return new OpenILS::Event(
974             "BAD_PARAMS", note =>
975                 "There are no streams to direct items. Can't predict."
976         );
977     }
978
979     unless (@$scaps) {
980         $editor->disconnect;
981         # XXX TODO new event type
982         return new OpenILS::Event(
983             "BAD_PARAMS", note =>
984                 "There are no active caption-and-pattern objects associated " .
985                 "with this subscription. Can't predict."
986         );
987     }
988
989     my @predictions;
990     my $link_id = 1;
991     foreach my $scap (@$scaps) {
992         my $caption_field = _revive_caption($scap);
993         $caption_field->update('8' => $link_id);
994         my $fake_chron_needed = 0;
995         # if we have missing chron pieces, we will add them later for prediction purposes
996         if (!$caption_field->enumeration_is_chronology) {
997             if (!$caption_field->subfield('i') # no year
998                 or !$caption_field->subfield('j')) { # we had a year, but no month or season
999                 $fake_chron_needed = '1';
1000             }
1001         }
1002         $mfhd->append_fields($caption_field);
1003         my $options = {
1004                 'caption' => $caption_field,
1005                 'scap_id' => $scap->id,
1006                 'include_base_issuance' => $args->{include_base_issuance},
1007                 'num_to_predict' => $args->{num_to_predict},
1008                 'end_date' => defined $args->{end_date} ?
1009                     $_strp_date->parse_datetime($args->{end_date}) : undef
1010                 };
1011         my $predict_from_siss;
1012         if ($args->{base_issuance}) { # predict from a given issuance
1013             $predict_from_siss = $args->{base_issuance};
1014         } else { # default to predicting from last published
1015             my $last_published = $editor->search_serial_issuance([
1016                     {'caption_and_pattern' => $scap->id,
1017                     'subscription' => $ssub_id},
1018                 {limit => 1, order_by => { siss => "date_published DESC" }}]
1019                 );
1020             if ($last_published->[0]) {
1021                 $predict_from_siss = $last_published->[0];
1022                 unless ($predict_from_siss->holding_code) {
1023                     $editor->disconnect;
1024                     # XXX TODO new event type
1025                     return new OpenILS::Event(
1026                         "BAD_PARAMS", note =>
1027                             "Last issuance has no holding code. Can't predict."
1028                     );
1029                 }
1030             } else {
1031                 $editor->disconnect;
1032                 # XXX TODO make a new event type instead of hijacking this one
1033                 return new OpenILS::Event(
1034                     "BAD_PARAMS", note => "No issuance from which to predict!"
1035                 );
1036             }
1037         }
1038         $logger->debug('make_prediction_values reviving holdings: ' . OpenSRF::Utils::JSON->perl2JSON($predict_from_siss));
1039         $options->{predict_from} = _revive_holding($predict_from_siss->holding_code, $caption_field, 1); # fresh MFHD Record, so we simply default to 1 for seqno
1040         if ($fake_chron_needed) {
1041             $options->{faked_chron_date} = DateTime::Format::ISO8601->new->parse_datetime(cleanse_ISO8601($predict_from_siss->date_published));
1042         }
1043         $logger->debug('make_prediction_values predicting with options: ' . OpenSRF::Utils::JSON->perl2JSON($options));
1044         push( @predictions, _generate_issuance_values($mfhd, $options) );
1045         $link_id++;
1046     }
1047
1048     $logger->debug('make_prediction_values predictions: ' . OpenSRF::Utils::JSON->perl2JSON(\@predictions));
1049     return \@predictions;
1050 }
1051
1052 sub store_predictions {
1053     my ($self, $conn, $authtoken, $args, $ssub, $sdists, $predictions) = @_;
1054
1055     my @issuances;
1056     foreach my $prediction (@$predictions) {
1057         my $issuance = new Fieldmapper::serial::issuance;
1058         $issuance->isnew(1);
1059         $issuance->label($prediction->{label});
1060         $issuance->date_published($prediction->{date_published}->strftime('%F'));
1061         $issuance->holding_code(OpenSRF::Utils::JSON->perl2JSON($prediction->{holding_code}));
1062         $issuance->holding_type($prediction->{holding_type});
1063         $issuance->caption_and_pattern($prediction->{caption_and_pattern});
1064         $issuance->subscription($ssub->id);
1065         push (@issuances, $issuance);
1066     }
1067
1068     my $evt = fleshed_issuance_alter($self, $conn, $authtoken, \@issuances);
1069     return $evt if ref $evt;
1070
1071     my @items;
1072     for (my $i = 0; $i < @issuances; $i++) {
1073         my $date_expected = $$predictions[$i]->{date_published}->add(seconds => interval_to_seconds($ssub->expected_date_offset))->strftime('%F');
1074         my $issuance = $issuances[$i];
1075         #$issuance->label(interval_to_seconds($ssub->expected_date_offset));
1076         foreach my $sdist (@$sdists) {
1077             my $streams = $sdist->streams;
1078             foreach my $stream (@$streams) {
1079                 my $item = new Fieldmapper::serial::item;
1080                 $item->isnew(1);
1081                 $item->stream($stream->id);
1082                 $item->date_expected($date_expected);
1083                 $item->issuance($issuance->id);
1084                 push (@items, $item);
1085             }
1086         }
1087     }
1088     fleshed_item_alter($self, $conn, $authtoken, \@items); # FIXME: catch events
1089     return \@items;
1090 }
1091
1092 #
1093 # _generate_issuance_values() is an initial attempt at a function which can be used
1094 # to populate an issuance table with a list of predicted issues.  It accepts
1095 # a hash ref of options initially defined as:
1096 # caption : the caption field to predict on
1097 # num_to_predict : the number of issues you wish to predict
1098 # faked_chron_date : if the serial does not actually have a chronology caption (but we need one for prediction's sake), base predictions on this date
1099 #
1100 # The basic method is to first convert to a single holding if compressed, then
1101 # increment the holding and save the resulting values to @issuances.
1102
1103 # returns @issuance_values, an array of hashrefs containing (formatted
1104 # label, formatted chronology date, formatted estimated arrival date, and an
1105 # array ref of holding subfields as (key, value, key, value ...)) (not a hash
1106 # to protect order and possible duplicate keys), and a holding type.
1107 #
1108 sub _generate_issuance_values {
1109     my ($mfhd, $options) = @_;
1110     my $caption = $options->{caption};
1111     my $scap_id = $options->{scap_id};
1112     my $include_base_issuance = $options->{include_base_issuance};
1113     my $num_to_predict = $options->{num_to_predict};
1114     my $end_date = $options->{end_date};
1115     my $predict_from = $options->{predict_from};   # MFHD::Holding to predict from
1116     my $faked_chron_date = $options->{faked_chron_date};   # serial does not have a (complete) chronology caption, so add one (temporarily) based on this date 
1117
1118     $logger->debug('_generate_issuance_values predict_from: ' . OpenSRF::Utils::JSON->perl2JSON($predict_from));
1119
1120 # Only needed for 'real' MFHD records, not our temp records
1121 #    my $link_id = $caption->link_id;
1122 #    if(!$predict_from) {
1123 #        my $htag = $caption->tag;
1124 #        $htag =~ s/^85/86/;
1125 #        my @holdings = $mfhd->holdings($htag, $link_id);
1126 #        my $last_holding = $holdings[-1];
1127 #
1128 #        #if ($last_holding->is_compressed) {
1129 #        #    $last_holding->compressed_to_last; # convert to last in range
1130 #        #}
1131 #        $predict_from = $last_holding;
1132 #    }
1133 #
1134
1135     $predict_from->notes('public',  []);
1136 # add a note marker for system use (?)
1137     $predict_from->notes('private', ['AUTOGEN']);
1138
1139     # our basic method for dealing with 'faked' chronologies will be to add it in, do the predicting, then take it back out
1140     my @faked_subfield_chars;
1141     if ($faked_chron_date) {
1142         my $faked_caption = new MARC::Field($caption->tag, $caption->indicator(1), $caption->indicator(2), $caption->subfields_list);
1143
1144         my %mfhd_chron_labels = ('i' => 'year', 'j' => 'month', 'k' => 'day');
1145         foreach my $subfield_char ('i', 'j', 'k') {
1146             if (!$caption->subfield($subfield_char)) { # if we are missing a piece, add it
1147                 push(@faked_subfield_chars, $subfield_char);
1148                 my $chron_name = $mfhd_chron_labels{$subfield_char};
1149                 $faked_caption->add_subfields($subfield_char => "($chron_name)");
1150                 my $method = $mfhd_chron_labels{$subfield_char};
1151                 $predict_from->add_subfields($subfield_char => $faked_chron_date->$chron_name);
1152             }
1153         }
1154         # because of the way MFHD::Caption and Holding work, it is simplest
1155         # to recreate rather than try to update
1156         $faked_caption = new MFHD::Caption($faked_caption);
1157         $predict_from = new MFHD::Holding($predict_from->seqno, new MARC::Field($predict_from->tag, $predict_from->indicator(1), $predict_from->indicator(2), $predict_from->subfields_list), $faked_caption);
1158         $logger->debug('_generate_issuance_values fake predict_from: ' . OpenSRF::Utils::JSON->perl2JSON($predict_from));
1159     }
1160
1161     my @predictions = $mfhd->generate_predictions({
1162         'include_base_issuance' => $include_base_issuance,
1163         'base_holding' => $predict_from,
1164         'num_to_predict' => $num_to_predict,
1165         'end_date' => $end_date
1166     });
1167     $logger->debug('_generate_issuance_values predictions: ' . OpenSRF::Utils::JSON->perl2JSON(\@predictions));
1168
1169     my $pub_date;
1170     my @issuance_values;
1171     foreach my $prediction (@predictions) {
1172         $pub_date = $_strp_date->parse_datetime($prediction->chron_to_date);
1173         if ($faked_chron_date) { # get rid of the chronology portions and restore original caption
1174             $prediction->delete_subfield(code => \@faked_subfield_chars);
1175             $prediction = new MFHD::Holding($prediction->seqno, new MARC::Field($prediction->tag, $prediction->indicator(1), $prediction->indicator(2), $prediction->subfields_list), $caption);
1176         }
1177         push(
1178                 @issuance_values,
1179                 {
1180                     #$link_id,
1181                     label => $prediction->format,
1182                     date_published => $pub_date,
1183                     #date_expected => $date_expected->strftime('%F'),
1184                     holding_code => [$prediction->indicator(1),$prediction->indicator(2),$prediction->subfields_list],
1185                     holding_type => $MFHD_NAMES_BY_TAG{$caption->tag},
1186                     caption_and_pattern => $scap_id
1187                 }
1188             );
1189     }
1190
1191     return @issuance_values;
1192 }
1193
1194 sub _revive_caption {
1195     my $scap = shift;
1196
1197     my $pattern_code = $scap->pattern_code;
1198
1199     # build MARC::Field
1200     my $pattern_parts = OpenSRF::Utils::JSON->JSON2perl($pattern_code);
1201     unshift(@$pattern_parts, $MFHD_TAGS_BY_NAME{$scap->type});
1202     my $pattern_field = new MARC::Field(@$pattern_parts);
1203
1204     # build MFHD::Caption
1205     return new MFHD::Caption($pattern_field);
1206 }
1207
1208 sub _revive_holding {
1209     my $holding_code = shift;
1210     my $caption_field = shift;
1211     my $seqno = shift;
1212
1213     # build MARC::Field
1214     my $holding_parts = OpenSRF::Utils::JSON->JSON2perl($holding_code);
1215     my $captag = $caption_field->tag;
1216     $captag =~ s/^85/86/;
1217     unshift(@$holding_parts, $captag);
1218     my $holding_field = new MARC::Field(@$holding_parts);
1219
1220     # build MFHD::Holding
1221     return new MFHD::Holding($seqno, $holding_field, $caption_field);
1222
1223     # TODO(?) the underlying MARC and the Holding object end up in conflict concerning subfield '8'
1224 }
1225
1226 __PACKAGE__->register_method(
1227     method    => 'unitize_items',
1228     api_name  => 'open-ils.serial.receive_items',
1229     api_level => 1,
1230     argc      => 1,
1231     signature => {
1232         desc     => 'Marks an item as received, updates the shelving unit (creating a new shelving unit if needed), and updates the summaries',
1233         'params' => [ {
1234                  name => 'items',
1235                  desc => 'array of serial items',
1236                  type => 'array'
1237             },
1238             {
1239                  name => 'barcodes',
1240                  desc => 'hash of item_ids => barcodes',
1241                  type => 'hash'
1242             },
1243             {
1244                  name => 'call_numbers',
1245                  desc => 'hash of item_ids => call_numbers',
1246                  type => 'hash'
1247             },
1248             {
1249                  name => 'donor_unit_ids',
1250                  desc => 'hash of unit_ids => 1, keyed with ids of any units giving up items',
1251                  type => 'hash'
1252             },
1253             {
1254                  name => 'extras',
1255                  desc => 'hash of hashes, circ_mod code and copy_location id, keyed as above',
1256                  type => 'hash'
1257             }
1258         ],
1259         'return' => {
1260             desc => 'Returns number of received items (num_items) and new unit ID, if applicable (new_unit_id)',
1261             type => 'hashref'
1262         }
1263     }
1264 );
1265
1266 __PACKAGE__->register_method(
1267     method    => 'unitize_items',
1268     api_name  => 'open-ils.serial.bind_items',
1269     api_level => 1,
1270     argc      => 1,
1271     signature => {
1272         desc     => 'Marks an item as bound, updates the shelving unit (creating a new shelving unit if needed)',
1273         'params' => [ {
1274                  name => 'items',
1275                  desc => 'array of serial items',
1276                  type => 'array'
1277             },
1278             {
1279                  name => 'barcodes',
1280                  desc => 'hash of item_ids => barcodes',
1281                  type => 'hash'
1282             },
1283             {
1284                  name => 'call_numbers',
1285                  desc => 'hash of item_ids => call_numbers',
1286                  type => 'hash'
1287             },
1288             {
1289                  name => 'donor_unit_ids',
1290                  desc => 'hash of unit_ids => 1, keyed with ids of any units giving up items',
1291                  type => 'hash'
1292             },
1293             {
1294                  name => 'extras',
1295                  desc => 'hash of hashes, circ_mod code and copy_location id, keyed as above',
1296                  type => 'hash'
1297             }
1298         ],
1299         'return' => {
1300             desc => 'Returns number of bound items (num_items) and new unit ID, if applicable (new_unit_id)',
1301             type => 'hashref'
1302         }
1303     }
1304 );
1305
1306 # TODO: reset/delete claims information once implemented
1307 # XXX: deal with emptied call numbers here?
1308 __PACKAGE__->register_method(
1309     method    => 'unitize_items',
1310     api_name  => 'open-ils.serial.reset_items',
1311     api_level => 1,
1312     argc      => 1,
1313     signature => {
1314         desc     => 'Resets the items to Expected, updates the shelving unit (deleting the shelving unit if empty), and updates the summaries',
1315         'params' => [ {
1316                  name => 'items',
1317                  desc => 'array of serial items',
1318                  type => 'array'
1319             }
1320         ],
1321         'return' => {
1322             desc => 'Returns number of reset items (num_items)',
1323             type => 'hashref'
1324         }
1325     }
1326 );
1327
1328 sub unitize_items {
1329     my ($self, $conn, $auth, $items, $barcodes, $call_numbers, $donor_unit_ids, $extras) = @_;
1330
1331     my $editor = new_editor("authtoken" => $auth, "xact" => 1);
1332     return $editor->die_event unless $editor->checkauth;
1333     return $editor->die_event unless $editor->allowed("RECEIVE_SERIAL");
1334     $self->api_name =~ /serial\.(\w*)_items/;
1335     my $mode = $1;
1336     
1337     my %found_unit_ids;
1338     if ($donor_unit_ids) { # units giving up items need updating as well
1339         %found_unit_ids = %$donor_unit_ids;
1340     }
1341     my %found_stream_ids;
1342     my %found_types;
1343     my $prev_loc_setting_map = {};
1344
1345     my %stream_ids_by_unit_id;
1346
1347     my %unit_map;
1348     my %sdist_by_unit_id;
1349     my %call_number_by_unit_id;
1350     my %sdist_by_stream_id;
1351
1352     my $new_unit_id; # id for '-2' units to share
1353     foreach my $item (@$items) {
1354         # for debugging only, TODO: delete
1355         if (!ref $item) { # hopefully we got an id instead
1356             $item = $editor->retrieve_serial_item($item);
1357         }
1358         # get ids
1359         my $unit_id = ref($item->unit) ? $item->unit->id : $item->unit;
1360         my $stream_id = ref($item->stream) ? $item->stream->id : $item->stream;
1361         my $issuance_id = ref($item->issuance) ? $item->issuance->id : $item->issuance;
1362         #TODO: evt on any missing ids
1363
1364         if ($mode eq 'receive') {
1365             $item->date_received('now');
1366             $item->status('Received');
1367         } elsif ($mode eq 'reset') {
1368             # clear date_received
1369             $item->clear_date_received;
1370             # Set status to 'Expected'
1371             $item->status('Expected');
1372             # remove from unit
1373             $item->clear_unit;
1374         }
1375
1376         # check for types to trigger summary updates
1377         my $scap;
1378         if (!ref $item->issuance) {
1379             my $scaps = $editor->search_serial_caption_and_pattern([{"+siss" => {"id" => $issuance_id}}, { "join" => {"siss" => {}} }]);
1380             $scap = $scaps->[0];
1381         } elsif (!ref $item->issuance->caption_and_pattern) {
1382             $scap = $editor->retrieve_serial_caption_and_pattern($item->issuance->caption_and_pattern);
1383         } else {
1384             $scap = $editor->issuance->caption_and_pattern;
1385         }
1386         if (!exists($found_types{$stream_id})) {
1387             $found_types{$stream_id} = {};
1388         }
1389         $found_types{$stream_id}->{$scap->type} = 1 if ($scap);
1390
1391         # create unit if needed
1392         if ($unit_id == -1 or (!$new_unit_id and $unit_id == -2)) { # create unit per item
1393             my $unit;
1394             my $sdists = $editor->search_serial_distribution([
1395                 {"+sstr" => {"id" => $stream_id}},
1396                 {
1397                     "join" => {"sstr" => {}},
1398                     "flesh" => 1,
1399                     "flesh_fields" => {"sdist" => ["subscription"]}
1400                 }]);
1401             $unit = _build_unit($editor, $sdists->[0], $mode);
1402             # if _build_unit fails, $unit is an event, so return it
1403             if ($U->event_code($unit)) {
1404                 $editor->rollback;
1405                 $unit->{"note"} = "Item ID: " . $item->id;
1406                 return $unit;
1407             }
1408
1409             $unit->barcode($barcodes->{$item->id}) if exists($barcodes->{$item->id});
1410             $unit->location($extras->{copy_locations}->{$item->id}) if exists($extras->{copy_locations}->{$item->id});
1411             $unit->circ_modifier($extras->{circ_mods}->{$item->id}) if exists($extras->{circ_mods}->{$item->id});
1412
1413             my $evt =  _create_sunit($editor, $unit);
1414             return $evt if $evt;
1415             if ($unit_id == -2) {
1416                 $new_unit_id = $unit->id;
1417                 $unit_id = $new_unit_id;
1418             } else {
1419                 $unit_id = $unit->id;
1420             }
1421             $item->unit($unit_id);
1422             
1423             # get unit with 'DEFAULT's and save unit, sdist, and call number for later use
1424             $unit = $editor->retrieve_serial_unit($unit->id);
1425             $unit_map{$unit_id} = $unit;
1426             $sdist_by_unit_id{$unit_id} = $sdists->[0];
1427             $call_number_by_unit_id{$unit_id} = $call_numbers->{$item->id};
1428             $sdist_by_stream_id{$stream_id} = $sdists->[0];
1429         } elsif ($unit_id == -2) { # create one unit for all '-2' items
1430             $unit_id = $new_unit_id;
1431             $item->unit($unit_id);
1432         }
1433
1434         $found_stream_ids{$stream_id} = 1;
1435
1436         if (defined($unit_id) and $unit_id ne '') {
1437             $found_unit_ids{$unit_id} = 1;
1438             # save the stream_id for this unit_id
1439             # TODO: prevent items from different streams in same unit? (perhaps in interface)
1440             $stream_ids_by_unit_id{$unit_id} = $stream_id;
1441         } else {
1442             $item->clear_unit;
1443         }
1444
1445         my $evt = _update_sitem($editor, undef, $item);
1446         return $evt if $evt;
1447
1448         if ($mode eq 'receive') {
1449             my $sdists = $editor->search_serial_distribution([
1450                 {"+sstr" => {"id" => $stream_id}},
1451                 {
1452                     "join" => {"sstr" => {}},
1453                     "flesh" => 1,
1454                     "flesh_fields" => {"sdist" => ["subscription"]}
1455                 }]);
1456
1457             #-------------------------------------------------------------------------
1458             # The following is copied from open-ils.serial.receive_items.one_unit_per
1459     
1460             # Fetch a list of issuances with received copies already existing
1461             # on this distribution (and with the same holding type on the
1462             # issuance).  This will be used in up to two places: once when building
1463             # a summary, once when changing the copy location of the previous
1464             # issuance's copy.
1465             my $issuances_received = _issuances_received($editor, $item);
1466             if ($U->event_code($issuances_received)) {
1467                 $editor->rollback;
1468                 return $issuances_received;
1469             }
1470     
1471             # Find out if we need to to deal with previous copy location changing.
1472             my $ou = $sdists->[0]->holding_lib;
1473             unless (exists $prev_loc_setting_map->{$ou}) {
1474                 $prev_loc_setting_map->{$ou} = $U->ou_ancestor_setting_value(
1475                     $ou, "serial.prev_issuance_copy_location", $editor
1476                 );
1477             }
1478     
1479             # If there is a previous copy location setting, we need the previous
1480             # issuance, from which we can in turn look up the item attached to the
1481             # same stream we're on now.
1482             if ($prev_loc_setting_map->{$ou}) {
1483                 if (my $prev_iss =
1484                     _previous_issuance($issuances_received, $item->issuance)) {
1485     
1486                     # Now we can change the copy location of the previous unit,
1487                     # if needed.
1488                     return $editor->event if defined $U->event_code(
1489                         move_previous_unit(
1490                             $editor, $prev_iss, $item, $prev_loc_setting_map->{$ou}
1491                         )
1492                     );
1493                 }
1494             }
1495             #-------------------------------------------------------------------------
1496         }
1497
1498     }
1499
1500     # cleanup 'dead' units (units which are now emptied of their items)
1501     my $dead_units = $editor->search_serial_unit([{'+sitem' => {'id' => undef}, 'deleted' => 'f'}, {'join' => {'sitem' => {'type' => 'left'}}}]);
1502     foreach my $unit (@$dead_units) {
1503         _delete_sunit($editor, undef, $unit);
1504         delete $found_unit_ids{$unit->id};
1505     }
1506
1507     # deal with unit level contents
1508     foreach my $unit_id (keys %found_unit_ids) {
1509
1510         # get all the needed issuances for unit
1511         # TODO remove 'Bindery' from this search (leaving it in for now for backwards compatibility with any current test environment data)
1512         my $issuances = $editor->search_serial_issuance([ {"+sitem" => {"unit" => $unit_id, "status" => ["Received", "Bindery"]}}, {"join" => {"sitem" => {}}, "order_by" => {"siss" => "date_published"}} ]);
1513         #TODO: evt on search failure
1514
1515         # retrieve and update unit contents
1516         my $sunit;
1517         my $sdist;
1518         my $call_number_string;
1519         my $record_id;
1520         # if we just created the unit, we will already have it and the distribution stored, and we will need to assign the call number
1521         if (exists $unit_map{$unit_id}) {
1522             $sunit = $unit_map{$unit_id};
1523             $sdist = $sdist_by_unit_id{$unit_id};
1524             $call_number_string = $call_number_by_unit_id{$unit_id};
1525             $record_id = $sdist->subscription->record_entry;
1526         } else {
1527             # XXX: this code assumes you will not have units which mix streams/distributions, but current code does not enforce this
1528             $sunit = $editor->retrieve_serial_unit($unit_id);
1529             if ($stream_ids_by_unit_id{$unit_id}) {
1530                 $sdist = $editor->search_serial_distribution([{"+sstr" => {"id" => $stream_ids_by_unit_id{$unit_id}}}, { "join" => {"sstr" => {}}, 'limit' => 1 }]);
1531             } else {
1532                 $sdist = $editor->search_serial_distribution([
1533                     {'+sunit' => {'id' => $unit_id}},
1534                     { 'join' =>
1535                         {'sstr' =>
1536                             { 'join' =>
1537                                 { 'sitem' =>
1538                                     { 'join' => 'sunit' }
1539                                 } 
1540                             } 
1541                         },
1542                       'limit' => 1
1543                     }]);
1544             }
1545             $sdist = $sdist->[0];
1546         }
1547
1548         my $evt = _prepare_unit($editor, $sunit, $sdist, $issuances, $call_number_string, $record_id);
1549         if ($U->event_code($evt)) {
1550             $editor->rollback;
1551             return $evt;
1552         }
1553
1554         $evt = _update_sunit($editor, undef, $sunit);
1555         if ($U->event_code($evt)) {
1556             $editor->rollback;
1557             return $evt;
1558         }
1559     }
1560
1561     if ($mode ne 'bind') { # the summary holdings do not change when binding
1562         # deal with stream level summaries
1563         # summaries will be built from the "primary" stream only, that is, the stream with the lowest ID per distribution
1564         # (TODO: consider direct designation)
1565         my %primary_streams_by_sdist;
1566         my %streams_by_sdist;
1567
1568         # see if we have primary streams, and if so, associate them with their distributions
1569         foreach my $stream_id (keys %found_stream_ids) {
1570             my $sdist;
1571             if (exists $sdist_by_stream_id{$stream_id}) {
1572                 $sdist = $sdist_by_stream_id{$stream_id};
1573             } else {
1574                 $sdist = $editor->search_serial_distribution([{"+sstr" => {"id" => $stream_id}}, { "join" => {"sstr" => {}} }]);
1575                 $sdist = $sdist->[0];
1576                 $sdist_by_stream_id{$stream_id} = $sdist;
1577             }
1578             my $streams;
1579             if (!exists($streams_by_sdist{$sdist->id})) {
1580                 $streams = $editor->search_serial_stream([{"distribution" => $sdist->id}, {"order_by" => {"sstr" => "id"}}]);
1581                 $streams_by_sdist{$sdist->id} = $streams;
1582             } else {
1583                 $streams = $streams_by_sdist{$sdist->id};
1584             }
1585             $primary_streams_by_sdist{$sdist->id} = $streams->[0] if ($stream_id == $streams->[0]->id);
1586         }
1587
1588         # retrieve and update summaries for each affected primary stream's distribution
1589         foreach my $sdist_id (keys %primary_streams_by_sdist) {
1590             my $stream = $primary_streams_by_sdist{$sdist_id};
1591             my $stream_id = $stream->id;
1592             # get all the needed issuances for stream
1593             # FIXME: search in Bindery/Bound/Not Published? as well as Received
1594             foreach my $type (keys %{$found_types{$stream_id}}) {
1595                 my $issuances = $editor->search_serial_issuance([ {"+sitem" => {"stream" => $stream_id, "status" => "Received"}, "+scap" => {"type" => $type}}, {"join" => {"sitem" => {}, "scap" => {}}, "order_by" => {"siss" => "date_published"}} ]);
1596                 #TODO: evt on search failure
1597                 my $evt = _prepare_summaries($editor, $issuances, $sdist_by_stream_id{$stream_id}, $type);
1598                 if ($U->event_code($evt)) {
1599                     $editor->rollback;
1600                     return $evt;
1601                 }
1602             }
1603         }
1604     }
1605
1606     $editor->commit;
1607     return {'num_items' => scalar @$items, 'new_unit_id' => $new_unit_id};
1608 }
1609
1610 sub _find_or_create_call_number {
1611     my ($e, $lib, $cn_string, $record) = @_;
1612
1613     my ($prefix,$suffix) = ('','');
1614     if (ref($cn_string)) {
1615         ($prefix,$cn_string,$suffix) = @$cn_string;
1616     }
1617
1618     my $existing = $e->search_asset_call_number([{
1619         owning_lib  => $lib,
1620         label       => $cn_string,
1621         record      => $record,
1622         deleted     => "f",
1623         '+acnp'     => { label => $prefix },
1624         '+acns'     => { label => $suffix },
1625         
1626     },{
1627         join => { acnp => {}, acns => {} }
1628     }]) or return $e->die_event;
1629
1630     if (@$existing) {
1631         return $existing->[0]->id;
1632     } else {
1633         return $e->die_event unless
1634             $e->allowed("CREATE_VOLUME", $lib);
1635
1636         $prefix = -1 if (!$prefix);
1637         $suffix = -1 if (!$suffix);
1638
1639         if ($prefix ne '-1') {
1640             my $acnp = $e->search_asset_call_number_prefix({
1641                 owning_lib  => $lib,
1642                 label       => $prefix,
1643             })->[0];
1644
1645             if (!$acnp) {
1646                 $acnp = new Fieldmapper::asset::call_number_prefix;
1647                 $acnp->label($prefix);
1648                 $acnp->owning_lib($lib);
1649                 $e->create_asset_call_number_prefix($acnp) or return $e->die_event;
1650                 $prefix = $e->data->id;
1651             } else {
1652                 $prefix = $acnp->id;
1653             }
1654         }
1655
1656         if ($suffix ne '-1') {
1657             my $acns = $e->search_asset_call_number_suffix({
1658                 owning_lib  => $lib,
1659                 label       => $suffix,
1660             })->[0];
1661
1662             if (!$acns) {
1663                 $acns = new Fieldmapper::asset::call_number_suffix;
1664                 $acns->label($suffix);
1665                 $acns->owning_lib($lib);
1666                 $e->create_asset_call_number_suffix($acns) or return $e->die_event;
1667                 $suffix = $e->data->id;
1668             } else {
1669                 $suffix = $acns->id;
1670             }
1671         }
1672
1673         my $acn = new Fieldmapper::asset::call_number;
1674
1675         $acn->creator($e->requestor->id);
1676         $acn->editor($e->requestor->id);
1677         $acn->record($record);
1678         $acn->label($cn_string);
1679         $acn->owning_lib($lib);
1680         $acn->prefix($prefix);
1681         $acn->suffix($suffix);
1682
1683         $e->create_asset_call_number($acn) or return $e->die_event;
1684         return $e->data->id;
1685     }
1686 }
1687
1688 sub _issuances_received {
1689     # XXX TODO: Add some caching or something. This is getting called
1690     # more often than it has to be.
1691     my ($e, $sitem) = @_;
1692
1693     my $results = $e->json_query({
1694         "select" => {"sitem" => ["issuance"]},
1695         "from" => {"sitem" => {"sstr" => {}, "siss" => {}}},
1696         "where" => {
1697             "+sstr" => {"distribution" => $sitem->stream->distribution->id},
1698             "+siss" => {"holding_type" => $sitem->issuance->holding_type},
1699             "+sitem" => {"date_received" => {"!=" => undef}}
1700         },
1701         "order_by" => {
1702             "siss" => {"date_published" => {"direction" => "asc"}}
1703         }
1704     }) or return $e->die_event;
1705
1706     my %seen;
1707     my $issuances = [];
1708     for my $iss_id (map { $_->{"issuance"} } @$results) {
1709         next if $seen{$iss_id};
1710         $seen{$iss_id} = 1;
1711         push(@$issuances, $e->retrieve_serial_issuance($iss_id));
1712     }
1713     return $issuances;
1714 }
1715
1716 # _prepare_unit populates the detailed_contents, summary_contents, and
1717 # sort_key fields for a given unit based on a given set of issuances
1718 # Also finds/creates call number as needed
1719 sub _prepare_unit {
1720     my ($e, $sunit, $sdist, $issuances, $call_number_string, $record_id) = @_;
1721
1722     # Handle call number first if we have one
1723     if ($call_number_string) {
1724         my $org_unit_id = ref $sdist->holding_lib ? $sdist->holding_lib->id : $sdist->holding_lib;
1725         my $real_cn = _find_or_create_call_number(
1726             $e, $org_unit_id,
1727             $call_number_string, $record_id
1728         );
1729
1730         if ($U->event_code($real_cn)) {
1731             return $real_cn;
1732         } else {
1733             $sunit->call_number($real_cn);
1734         }
1735     }
1736
1737     my ($mfhd, $formatted_parts) = _summarize_contents($e, $issuances);
1738     return $mfhd if $U->event_code($mfhd);
1739
1740     # special case for single formatted_part (may have summarized version)
1741     if (@$formatted_parts == 1) {
1742         #TODO: MFHD.pm should have a 'format_summary' method for this
1743     }
1744
1745     $sunit->detailed_contents(
1746         join(
1747             " ",
1748             $sdist->unit_label_prefix,
1749             join(", ", @$formatted_parts),
1750             $sdist->unit_label_suffix
1751         )
1752     );
1753
1754     # TODO: change this when real summary contents are available
1755     $sunit->summary_contents($sunit->detailed_contents);
1756
1757     # Create sort_key by left padding numbers to 6 digits.
1758     (my $sort_key = $sunit->detailed_contents) =~
1759         s/(\d+)/sprintf '%06d', $1/eg;
1760     $sunit->sort_key($sort_key);
1761 }
1762
1763 # _prepare_summaries populates the generated_coverage field for a given summary 
1764 # type ('basic', 'index', 'supplement') for a given distribution.
1765 # It also creates the summary if it doesn't yet exist.
1766 sub _prepare_summaries {
1767     my ($e, $issuances, $sdist, $type) = @_;
1768
1769     my ($mfhd, $formatted_parts) = _summarize_contents($e, $issuances, $sdist, $type);
1770     return $mfhd if $U->event_code($mfhd);
1771
1772     my $search_method = "search_serial_${type}_summary";
1773     my $summary = $e->$search_method([{"distribution" => $sdist->id}]);
1774
1775     my $cu_method = "update";
1776
1777     if (@$summary) {
1778         $summary = $summary->[0];
1779     } else {
1780         my $class = "Fieldmapper::serial::${type}_summary";
1781         $summary = $class->new;
1782         $summary->distribution($sdist->id);
1783         $cu_method = "create";
1784     }
1785
1786     if (@$formatted_parts) {
1787         $summary->generated_coverage(OpenSRF::Utils::JSON->perl2JSON($formatted_parts));
1788     } else {
1789         # we had no issuances or MFHD data for this type, so clear any
1790         # generated data which may have existed before
1791         $summary->generated_coverage('');
1792     }
1793     my $method = "${cu_method}_serial_${type}_summary";
1794     return $e->die_event unless $e->$method($summary);
1795 }
1796
1797
1798 __PACKAGE__->register_method(
1799     method    => 'regen_summaries',
1800     api_name  => 'open-ils.serial.regenerate_summaries',
1801     api_level => 1,
1802     argc      => 1,
1803     signature => {
1804         'desc'   => 'Regenerate all the generated_coverage fields for given distributions or subscriptions (depending on params given). Params are expected to be hash members.',
1805         'params' => [ {
1806                  name => 'sdist_ids',
1807                  desc => 'IDs of the distribution whose coverage you want to regenerate',
1808                  type => 'array'
1809             },
1810             {
1811                  name => 'ssub_ids',
1812                  desc => 'IDs of the subscriptions whose coverage you want to regenerate',
1813                  type => 'array'
1814             }
1815         ],
1816         'return' => {
1817             desc => 'Returns undef if successful, event if failed',
1818             type => 'mixed'
1819         }
1820 #TODO: best practices for return values
1821     }
1822 );
1823
1824 sub regen_summaries {
1825     my ($self, $conn, $auth, $opts) = @_;
1826
1827     my $e = new_editor("authtoken" => $auth, "xact" => 1);
1828     return $e->die_event unless $e->checkauth;
1829     # Perm checks not necessary since generated_coverage is akin to
1830     # caching of data, not actual editing.  XXX This might need more
1831     # consideration.
1832     #return $editor->die_event unless $editor->allowed("RECEIVE_SERIAL");
1833
1834     my $evt = _regenerate_summaries($e, $opts);
1835     if ($U->event_code($evt)) {
1836         $e->rollback;
1837         return $evt;
1838     }
1839
1840     $e->commit;
1841
1842     return undef;
1843 }
1844
1845 sub _regenerate_summaries {
1846     my ($e, $opts) = @_;
1847
1848     $logger->debug('_regenerate_summaries with opts: ' . OpenSRF::Utils::JSON->perl2JSON($opts));
1849     my @sdist_ids;
1850     if ($opts->{'ssub_ids'}) {
1851         foreach my $ssub_id (@{$opts->{'ssub_ids'}}) {
1852             my $sdist_ids_temp = $e->search_serial_distribution(
1853                 { 'subscription' => $ssub_id },
1854                 { 'idlist' => 1 }
1855             );
1856             push(@sdist_ids, @$sdist_ids_temp);
1857         }
1858     } elsif ($opts->{'sdist_ids'}) {
1859         @sdist_ids = @$opts->{'sdist_ids'};
1860     }
1861
1862     foreach my $sdist_id (@sdist_ids) {
1863         # get distribution
1864         my $sdist = $e->retrieve_serial_distribution($sdist_id)
1865             or return $e->die_event;
1866
1867 # See large comment below
1868 #        my $has_merged_mfhd;
1869         foreach my $type (@MFHD_NAMES) {
1870             # get issuances
1871             my $issuances = $e->search_serial_issuance([
1872                 {
1873                     "+sdist" => {"id" => $sdist_id},
1874                     "+sitem" => {"status" => "Received"},
1875                     "+scap" => {"type" => $type}
1876                 },
1877                 {
1878                     "join" => {
1879                         "sitem" => {},
1880                         "scap" => {},
1881                         "ssub" => {
1882                             "join" => {"sdist" =>{}}
1883                         }
1884                     },
1885                     "order_by" => {
1886                         "siss" => "date_published"
1887                     }
1888                 }
1889             ]) or return $e->die_event;
1890
1891 # This level of nuance doesn't appear to be necessary.
1892 # At the moment, we pass down an empty issuance list,
1893 # and the inner methods will "do the right thing" and
1894 # pull in the MFHD if called for, but in some cases not
1895 # ultimately generate any coverage.  The code below is
1896 # broken in cases where we delete the last issuance, since
1897 # the now empty summary never gets updated.
1898 #
1899 # Leaving this code for now (2014/04) in case pushing
1900 # the logic down ends up being too slow or complicates
1901 # the inner methods beyond their scope.
1902 #
1903 #            if (!@$issuances and !$has_merged_mfhd) {
1904 #                if (!defined($has_merged_mfhd)) {
1905 #                    # even without issuances, we can generate a summary
1906 #                    # from a merged MFHD record, so look for one
1907 #                    my $mfhd_ids = $e->search_serial_record_entry(
1908 #                        {
1909 #                            '+sdist' => {
1910 #                                'id' => $sdist_id,
1911 #                                'summary_method' => 'merge_with_sre'
1912 #                            }
1913 #                        },
1914 #                        {
1915 #                            'join' => { 'sdist' => {} },
1916 #                            'idlist' => 1
1917 #                        }
1918 #                    );
1919 #                    if ($mfhd_ids and @$mfhd_ids) {
1920 #                        $has_merged_mfhd = 1;
1921 #                    } else {
1922 #                        next;
1923 #                    }
1924 #                } else {
1925 #                    next; # abort to prevent empty summary creation (i.e. '[]')
1926 #                }
1927 #            }
1928             my $evt = _prepare_summaries($e, $issuances, $sdist, $type);
1929             if ($U->event_code($evt)) {
1930                 $e->rollback;
1931                 return $evt;
1932             }
1933         }
1934     }
1935
1936     return undef;
1937 }
1938
1939 sub _unit_by_iss_and_str {
1940     my ($e, $issuance, $stream) = @_;
1941
1942     my $unit = $e->json_query({
1943         "select" => {"sunit" => ["id"]},
1944         "from" => {"sitem" => {"sunit" => {}}},
1945         "where" => {
1946             "+sitem" => {
1947                 "issuance" => $issuance->id,
1948                 "stream" => $stream->id
1949             }
1950         }
1951     }) or return $e->die_event;
1952     return 0 if not @$unit;
1953
1954     $e->retrieve_serial_unit($unit->[0]->{"id"}) or $e->die_event;
1955 }
1956
1957 sub move_previous_unit {
1958     my ($e, $prev_iss, $curr_item, $new_loc) = @_;
1959
1960     my $prev_unit = _unit_by_iss_and_str($e,$prev_iss,$curr_item->stream);
1961     return $prev_unit if defined $U->event_code($prev_unit);
1962     return 0 if not $prev_unit;
1963
1964     if ($prev_unit->location != $new_loc) {
1965         $prev_unit->location($new_loc);
1966         $e->update_serial_unit($prev_unit) or return $e->die_event;
1967     }
1968     0;
1969 }
1970
1971 # _previous_issuance() assumes $existing is an ordered array
1972 sub _previous_issuance {
1973     my ($existing, $issuance) = @_;
1974
1975     my $last = $existing->[-1];
1976     return undef unless $last;
1977     return ($last->id == $issuance->id ? $existing->[-2] : $last);
1978 }
1979
1980 __PACKAGE__->register_method(
1981     "method" => "receive_items_one_unit_per",
1982     "api_name" => "open-ils.serial.receive_items.one_unit_per",
1983     "stream" => 1,
1984     "api_level" => 1,
1985     "argc" => 3,
1986     "signature" => {
1987         "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",
1988         "params" => [
1989             {
1990                  "name" => "auth",
1991                  "desc" => "authtoken",
1992                  "type" => "string"
1993             },
1994             {
1995                  "name" => "items",
1996                  "desc" => "array of serial items, possibly fleshed with units and definitely fleshed with stream->distribution",
1997                  "type" => "array"
1998             },
1999             {
2000                 "name" => "record",
2001                 "desc" => "id of bib record these items are associated with
2002                     (XXX could/should be derived from items)",
2003                 "type" => "number"
2004             }
2005         ],
2006         "return" => {
2007             "desc" => "The item ID for each item successfully received",
2008             "type" => "int"
2009         }
2010     }
2011 );
2012
2013 sub receive_items_one_unit_per {
2014     # XXX This function may be temporary, as it does some of what
2015     # unitize_items() does, just in a different way.
2016     my ($self, $client, $auth, $items, $record) = @_;
2017
2018     my $e = new_editor("authtoken" => $auth, "xact" => 1);
2019     return $e->die_event unless $e->checkauth;
2020     return $e->die_event unless $e->allowed("RECEIVE_SERIAL");
2021
2022     my $prev_loc_setting_map = {};
2023     my $user_id = $e->requestor->id;
2024
2025     # Get a list of all the non-virtual field names in a serial::unit for
2026     # merging given unit objects with template-built units later.
2027     # XXX move this somewhere global so it isn't re-run all the time
2028     my $all_unit_fields =
2029         $Fieldmapper::fieldmap->{"Fieldmapper::serial::unit"}->{"fields"};
2030     my @real_unit_fields = grep {
2031         not $all_unit_fields->{$_}->{"virtual"}
2032     } keys %$all_unit_fields;
2033
2034     foreach my $item (@$items) {
2035         # Note that we expect a certain fleshing on the items we're getting.
2036         my $sdist = $item->stream->distribution;
2037
2038         # Fetch a list of issuances with received copies already existing
2039         # on this distribution (and with the same holding type on the
2040         # issuance).  This will be used in up to two places: once when building
2041         # a summary, once when changing the copy location of the previous
2042         # issuance's copy.
2043         my $issuances_received = _issuances_received($e, $item);
2044         if ($U->event_code($issuances_received)) {
2045             $e->rollback;
2046             return $issuances_received;
2047         }
2048
2049         # Find out if we need to to deal with previous copy location changing.
2050         my $ou = $sdist->holding_lib->id;
2051         unless (exists $prev_loc_setting_map->{$ou}) {
2052             $prev_loc_setting_map->{$ou} = $U->ou_ancestor_setting_value(
2053                 $ou, "serial.prev_issuance_copy_location", $e
2054             );
2055         }
2056
2057         # If there is a previous copy location setting, we need the previous
2058         # issuance, from which we can in turn look up the item attached to the
2059         # same stream we're on now.
2060         if ($prev_loc_setting_map->{$ou}) {
2061             if (my $prev_iss =
2062                 _previous_issuance($issuances_received, $item->issuance)) {
2063
2064                 # Now we can change the copy location of the previous unit,
2065                 # if needed.
2066                 return $e->event if defined $U->event_code(
2067                     move_previous_unit(
2068                         $e, $prev_iss, $item, $prev_loc_setting_map->{$ou}
2069                     )
2070                 );
2071             }
2072         }
2073
2074         # Create unit if given by user
2075         if (ref $item->unit) {
2076             # detach from the item, as we need to create separately
2077             my $user_unit = $item->unit;
2078
2079             # get a unit based on associated template
2080             my $template_unit = _build_unit($e, $sdist, "receive");
2081             if ($U->event_code($template_unit)) {
2082                 $e->rollback;
2083                 $template_unit->{"note"} = "Item ID: " . $item->id;
2084                 return $template_unit;
2085             }
2086
2087             # merge built unit with provided unit from user
2088             foreach (@real_unit_fields) {
2089                 unless ($user_unit->$_) {
2090                     $user_unit->$_($template_unit->$_);
2091                 }
2092             }
2093
2094             # Treat call number specially: the provided value from the
2095             # user will really be a string.
2096             my $call_number_string;
2097             if ($user_unit->call_number) {
2098                 $call_number_string = $user_unit->call_number;
2099                 # clear call number for now (replaced in _prepare_unit)
2100                 $user_unit->clear_call_number;
2101             }
2102
2103             my $evt = _prepare_unit(
2104                 $e, $user_unit, $sdist, [$item->issuance],
2105                 $call_number_string, $record
2106             );
2107             if ($U->event_code($evt)) {
2108                 $e->rollback;
2109                 return $evt;
2110             }
2111
2112             # create/update summary objects related to this distribution
2113             # Make sure @$issuances_received contains current item's issuance
2114             unless (grep { $_->id == $item->issuance->id } @$issuances_received) {
2115                 push @$issuances_received, $item->issuance;
2116             }
2117             $evt = _prepare_summaries($e, $issuances_received, $item->stream->distribution, $item->issuance->holding_type);
2118             if ($U->event_code($evt)) {
2119                 $e->rollback;
2120                 return $evt;
2121             }
2122
2123             # set the incontrovertibles on the unit
2124             $user_unit->edit_date("now");
2125             $user_unit->create_date("now");
2126             $user_unit->editor($user_id);
2127             $user_unit->creator($user_id);
2128
2129             $evt = _create_sunit($e, $user_unit);
2130             return $evt if $evt;
2131
2132             # save reference to new unit
2133             $item->unit($e->data->id);
2134         }
2135
2136         # Create notes if given by user
2137         if (ref($item->notes) and @{$item->notes}) {
2138             foreach my $note (@{$item->notes}) {
2139                 $note->creator($user_id);
2140                 $note->create_date("now");
2141
2142                 return $e->die_event unless $e->create_serial_item_note($note);
2143             }
2144
2145             $item->clear_notes; # They're saved; we no longer want them here.
2146         }
2147
2148         # Set the incontrovertibles on the item
2149         $item->status("Received");
2150         $item->date_received("now");
2151         $item->edit_date("now");
2152         $item->editor($user_id);
2153
2154         return $e->die_event unless $e->update_serial_item($item);
2155
2156         # send client a response
2157         $client->respond($item->id);
2158     }
2159
2160     $e->commit or return $e->die_event;
2161     undef;
2162 }
2163
2164 sub _build_unit {
2165     my $editor = shift;
2166     my $sdist = shift;
2167     my $mode = shift;
2168     #my $skip_call_number = shift;
2169
2170     my $attr = $mode . '_unit_template';
2171     my $template = $editor->retrieve_asset_copy_template($sdist->$attr) or
2172         return new OpenILS::Event("SERIAL_DISTRIBUTION_HAS_NO_COPY_TEMPLATE");
2173
2174     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 );
2175
2176     my $unit = new Fieldmapper::serial::unit;
2177     foreach my $part (@parts) {
2178         my $value = $template->$part;
2179         next if !defined($value);
2180         $unit->$part($value);
2181     }
2182
2183     # ignore circ_lib in template, set to distribution holding_lib
2184     $unit->circ_lib($sdist->holding_lib);
2185     $unit->creator($editor->requestor->id);
2186     $unit->editor($editor->requestor->id);
2187
2188 # XXX: this feature has been pushed back until after 2.0 at least
2189 #    unless ($skip_call_number) {
2190 #        $attr = $mode . '_call_number';
2191 #        my $cn = $sdist->$attr or
2192 #            return new OpenILS::Event("SERIAL_DISTRIBUTION_HAS_NO_CALL_NUMBER");
2193 #
2194 #        $unit->call_number($cn);
2195 #    }
2196     $unit->call_number('-1'); # default to the dummy call number
2197     $unit->barcode('@@PLACEHOLDER'); # generic unit will start with a generated placeholder barcode
2198     $unit->sort_key('');
2199     $unit->summary_contents('');
2200     $unit->detailed_contents('');
2201
2202     return $unit;
2203 }
2204
2205 sub _summarize_contents {
2206     my $editor = shift;
2207     my $issuances = shift;
2208     my $sdist = shift;
2209     my $type = shift;
2210
2211     # create or lookup MFHD record
2212     my $mfhd;
2213     if ($sdist and defined($sdist->record_entry) and $sdist->summary_method eq 'merge_with_sre') {
2214         my $sre;
2215         if (ref $sdist->record_entry) {
2216             $sre = $sdist->record_entry; 
2217         } else {
2218             $sre = $editor->retrieve_serial_record_entry($sdist->record_entry);
2219         }
2220         $mfhd = MFHD->new(MARC::Record->new_from_xml($sre->marc)); 
2221     } else {
2222         $logger->info($sdist);
2223         $mfhd = MFHD->new(MARC::Record->new());
2224     }
2225
2226     my %scaps;
2227     my %scap_fields;
2228     my $seqno = 1;
2229     # We keep track of these separately to avoid link_id contamination,
2230     # e.g. a basic issuance, followed by a merging supplement, followed by
2231     # another basic.  If we could be sure that they were not mixed, one
2232     # value could suffice.
2233     my %link_ids = ('basic' => 10000, 'index' => 10000, 'supplement' => 10000);
2234     my %first_scap = ('basic' => 1, 'index' => 1, 'supplement' => 1);
2235     foreach my $issuance (@$issuances) {
2236         my $scap_id = $issuance->caption_and_pattern;
2237         next if (!$scap_id); # skip issuances with no caption/pattern
2238
2239         my $scap;
2240         my $scap_field;
2241         # if this is the first appearance of this scap, retrieve it and add it to the temporary record
2242         if (!exists $scaps{$issuance->caption_and_pattern}) {
2243             $scaps{$scap_id} = $editor->retrieve_serial_caption_and_pattern($scap_id);
2244             $scap = $scaps{$scap_id};
2245             $scap_field = _revive_caption($scap);
2246             my $did_merge = 0;
2247             if ($first_scap{$scap->type}) { # special merge processing
2248                 $first_scap{$MFHD_TAGS_BY_NAME{$scap->type}} = 0;
2249                 if ($sdist and $sdist->summary_method eq 'merge_with_sre') {
2250                     # MFHD Caption objects do not yet have a built-in compare (TODO), so let's do a basic one
2251                     my @field_85xs = $mfhd->field($MFHD_TAGS_BY_NAME{$scap->type});
2252                     if (@field_85xs) {
2253                         my $last_caption_field = $field_85xs[-1];
2254                         my $last_link_id = $last_caption_field->subfield('8');
2255                         # set the link id to match, temporarily, for comparison
2256                         $last_caption_field->update('8' => $scap_field->subfield('8'));
2257                         my $last_caption_json = OpenSRF::Utils::JSON->perl2JSON([$last_caption_field->indicator(1), $last_caption_field->indicator(2), $last_caption_field->subfields_list]);
2258                         if ($last_caption_json eq $scap->pattern_code) { # merge is possible, they match
2259                             # restore link id
2260                             $link_ids{$scap->type} = $last_link_id;
2261                             # set scap_field to last field
2262                             $scap_field = $last_caption_field;
2263                             $did_merge = 1;
2264                         }
2265                     }
2266                 }
2267             }
2268             $scap_fields{$scap_id} = $scap_field;
2269             $scap_field->update('8' => $link_ids{$scap->type});
2270             # TODO: make MFHD/Caption smarter about this
2271             $scap_field->{_mfhdc_LINK_ID} = $link_ids{$scap->type};
2272             $mfhd->append_fields($scap_field) if !$did_merge;
2273             $link_ids{$scap->type}++;
2274         } else {
2275             $scap_field = $scap_fields{$scap_id};
2276         }
2277
2278         $mfhd->append_fields(_revive_holding($issuance->holding_code, $scap_field, $seqno));
2279         $seqno++;
2280     }
2281
2282     my @formatted_parts;
2283     my @scap_fields_ordered;
2284     if ($type) {
2285         @scap_fields_ordered = $mfhd->field($MFHD_TAGS_BY_NAME{$type});
2286     } else {
2287         # if they didn't give a type, send back whatever holdings we have.
2288         # this is really only sensible right now for summarizing one type,
2289         # and is used by the unitize code for this purpose
2290         #
2291         # TODO: possible future support for binding (unitizing) of multiple
2292         # types into a sensible summary string
2293         @scap_fields_ordered = $mfhd->field('85[345]');
2294     }
2295
2296     foreach my $scap_field (@scap_fields_ordered) { #TODO: use generic MFHD "summarize" method, once available
2297         my @updated_holdings;
2298         eval {
2299             @updated_holdings = $mfhd->get_combined_holdings($scap_field);
2300         };
2301         if ($@) {
2302             my $msg = "get_combined_holdings(): $@ ; using sdist ID #" .
2303                 ($sdist ? $sdist->id : "<NONE>") . " and " .
2304                 scalar(@$issuances) . " issuances, of which one has ID #" .
2305                 $issuances->[0]->id;
2306
2307             $msg =~ s/\n//gm;
2308             $logger->error($msg);
2309             return new OpenILS::Event("BAD_PARAMS", note => $msg);
2310         }
2311
2312         push @formatted_parts, map { $_->format } @updated_holdings;
2313     }
2314
2315     return ($mfhd, \@formatted_parts);
2316 }
2317
2318 ##########################################################################
2319 # note methods
2320 #
2321 __PACKAGE__->register_method(
2322     method      => 'fetch_notes',
2323     api_name        => 'open-ils.serial.item_note.retrieve.all',
2324     signature   => q/
2325         Returns an array of copy note objects.  
2326         @param args A named hash of parameters including:
2327             authtoken   : Required if viewing non-public notes
2328             item_id      : The id of the item whose notes we want to retrieve
2329             pub         : True if all the caller wants are public notes
2330         @return An array of note objects
2331     /
2332 );
2333
2334 __PACKAGE__->register_method(
2335     method      => 'fetch_notes',
2336     api_name        => 'open-ils.serial.subscription_note.retrieve.all',
2337     signature   => q/
2338         Returns an array of copy note objects.  
2339         @param args A named hash of parameters including:
2340             authtoken       : Required if viewing non-public notes
2341             subscription_id : The id of the item whose notes we want to retrieve
2342             pub             : True if all the caller wants are public notes
2343         @return An array of note objects
2344     /
2345 );
2346
2347 __PACKAGE__->register_method(
2348     method      => 'fetch_notes',
2349     api_name        => 'open-ils.serial.distribution_note.retrieve.all',
2350     signature   => q/
2351         Returns an array of copy note objects.  
2352         @param args A named hash of parameters including:
2353             authtoken       : Required if viewing non-public notes
2354             distribution_id : The id of the item whose notes we want to retrieve
2355             pub             : True if all the caller wants are public notes
2356         @return An array of note objects
2357     /
2358 );
2359
2360 # TODO: revisit this method to consider replacing cstore direct calls
2361 sub fetch_notes {
2362     my( $self, $connection, $args ) = @_;
2363     
2364     $self->api_name =~ /serial\.(\w*)_note/;
2365     my $type = $1;
2366
2367     my $id = $$args{object_id};
2368     my $authtoken = $$args{authtoken};
2369     my $order_by = $$args{order_by} || 'create_date';
2370     my( $r, $evt);
2371
2372     if( $$args{pub} ) {
2373         return $U->cstorereq(
2374             'open-ils.cstore.direct.serial.'.$type.'_note.search.atomic',
2375             { $type => $id, pub => 't' }, {'order_by' => {$FM_NAME_TO_ID{$type}.'n' => $order_by}} );
2376     } else {
2377         # FIXME: restore perm check
2378         # ( $r, $evt ) = $U->checksesperm($authtoken, 'VIEW_COPY_NOTES');
2379         # return $evt if $evt;
2380         return $U->cstorereq(
2381             'open-ils.cstore.direct.serial.'.$type.'_note.search.atomic', {$type => $id}, {'order_by' => {$FM_NAME_TO_ID{$type}.'n' => $order_by}} );
2382     }
2383
2384     return undef;
2385 }
2386
2387 __PACKAGE__->register_method(
2388     method      => 'update_note',
2389     api_name        => 'open-ils.serial.item_note.update',
2390     signature   => q/
2391         Updates or creates an item note
2392         @param authtoken The login session key
2393         @param note The note object to update or create
2394         @return The id of the note object
2395     /
2396 );
2397
2398 __PACKAGE__->register_method(
2399     method      => 'update_note',
2400     api_name        => 'open-ils.serial.subscription_note.update',
2401     signature   => q/
2402         Updates or creates a subscription note
2403         @param authtoken The login session key
2404         @param note The note object to update or create
2405         @return The id of the note object
2406     /
2407 );
2408
2409 __PACKAGE__->register_method(
2410     method      => 'update_note',
2411     api_name        => 'open-ils.serial.distribution_note.update',
2412     signature   => q/
2413         Updates or creates a distribution note
2414         @param authtoken The login session key
2415         @param note The note object to update or create
2416         @return The id of the note object
2417     /
2418 );
2419
2420 sub update_note {
2421     my( $self, $connection, $authtoken, $note ) = @_;
2422
2423     $self->api_name =~ /serial\.(\w*)_note/;
2424     my $type = $1;
2425
2426     my $e = new_editor(xact=>1, authtoken=>$authtoken);
2427     return $e->event unless $e->checkauth;
2428
2429     if ($type eq 'item') {
2430         my $sitem = $e->retrieve_serial_item([
2431             $note->item, {
2432                 "flesh" => 2, "flesh_fields" => {
2433                     "sitem" => ["stream"], "sstr" => ["distribution"]
2434                 }
2435             }
2436         ]) or return $e->die_event;
2437
2438         return $e->die_event unless $e->allowed(
2439             "ADMIN_SERIAL_ITEM", $sitem->stream->distribution->holding_lib
2440         );
2441     } elsif ($type eq 'distribution') {
2442         my $sdist = $e->retrieve_serial_distribution($note->distribution)
2443             or return $e->die_event;
2444
2445         return $e->die_event unless
2446             $e->allowed("ADMIN_SERIAL_DISTRIBUTION", $sdist->holding_lib);
2447     } else { # subscription
2448         my $sub = $e->retrieve_serial_subscription($note->subscription)
2449             or return $e->die_event;
2450
2451         return $e->die_event unless
2452             $e->allowed("ADMIN_SERIAL_SUBSCRIPTION", $sub->owning_lib);
2453     }
2454
2455     $note->pub( ($U->is_true($note->pub)) ? 't' : 'f' );
2456     my $method;
2457     if ($note->isnew) {
2458         $note->create_date('now');
2459         $note->creator($e->requestor->id);
2460         $note->clear_id;
2461         $method = "create_serial_${type}_note";
2462     } else {
2463         $method = "update_serial_${type}_note";
2464     }
2465     $e->$method($note) or return $e->event;
2466     $e->commit;
2467     return $note->id;
2468 }
2469
2470 __PACKAGE__->register_method(
2471     method      => 'delete_note',
2472     api_name        =>  'open-ils.serial.item_note.delete',
2473     signature   => q/
2474         Deletes an existing item note
2475         @param authtoken The login session key
2476         @param noteid The id of the note to delete
2477         @return 1 on success - Event otherwise.
2478         /
2479 );
2480
2481 __PACKAGE__->register_method(
2482     method      => 'delete_note',
2483     api_name        =>  'open-ils.serial.subscription_note.delete',
2484     signature   => q/
2485         Deletes an existing subscription note
2486         @param authtoken The login session key
2487         @param noteid The id of the note to delete
2488         @return 1 on success - Event otherwise.
2489         /
2490 );
2491
2492 __PACKAGE__->register_method(
2493     method      => 'delete_note',
2494     api_name        =>  'open-ils.serial.distribution_note.delete',
2495     signature   => q/
2496         Deletes an existing distribution note
2497         @param authtoken The login session key
2498         @param noteid The id of the note to delete
2499         @return 1 on success - Event otherwise.
2500         /
2501 );
2502
2503 sub delete_note {
2504     my( $self, $conn, $authtoken, $noteid ) = @_;
2505
2506     $self->api_name =~ /serial\.(\w*)_note/;
2507     my $type = $1;
2508
2509     my $e = new_editor(xact=>1, authtoken=>$authtoken);
2510     return $e->die_event unless $e->checkauth;
2511
2512     my $method = "retrieve_serial_${type}_note";
2513     my $note = $e->$method([
2514         $noteid,
2515     ]) or return $e->die_event;
2516
2517     if ($type eq 'item') {
2518         my $sitem = $e->retrieve_serial_item([
2519             $note->item, {
2520                 "flesh" => 2, "flesh_fields" => {
2521                     "sitem" => ["stream"], "sstr" => ["distribution"]
2522                 }
2523             }
2524         ]) or return $e->die_event;
2525
2526         return $e->die_event unless $e->allowed(
2527             "ADMIN_SERIAL_ITEM", $sitem->stream->distribution->holding_lib
2528         );
2529     } elsif ($type eq 'distribution') {
2530         my $sdist = $e->retrieve_serial_distribution($note->distribution)
2531             or return $e->die_event;
2532
2533         return $e->die_event unless
2534             $e->allowed("ADMIN_SERIAL_DISTRIBUTION", $sdist->holding_lib);
2535     } else { # subscription
2536         my $sub = $e->retrieve_serial_subscription($note->subscription)
2537             or return $e->die_event;
2538
2539         return $e->die_event unless
2540             $e->allowed("ADMIN_SERIAL_SUBSCRIPTION", $sub->owning_lib);
2541     }
2542
2543     $method = "delete_serial_${type}_note";
2544     $e->$method($note) or return $e->die_event;
2545     $e->commit;
2546     return 1;
2547 }
2548
2549
2550 ##########################################################################
2551 # subscription methods
2552 #
2553
2554 __PACKAGE__->register_method(
2555     method      => 'safe_delete',
2556     api_name        =>  'open-ils.serial.subscription.safe_delete',
2557     signature   => q/
2558         Deletes an existing subscription and related records
2559         (distributions, streams, etc.), but only if there are no serial
2560         items with a status other than Expected, and no non-deleted 
2561         serial units.
2562         @param authtoken The login session key
2563         @param subid The id of the subscription to delete
2564         @return 1 on success - Event otherwise.
2565         /
2566 );
2567
2568 __PACKAGE__->register_method(
2569     method      => 'safe_delete',
2570     api_name        =>  'open-ils.serial.distribution.safe_delete',
2571     signature   => q/
2572         Deletes an existing distribution and related records
2573         (streams, etc.), but only if there are no attached serial items
2574         with a status other than Expected, and no non-deleted serial
2575         units.
2576         @param authtoken The login session key
2577         @param subid The id of the distribution to delete
2578         @return 1 on success - Event otherwise.
2579         /
2580 );
2581
2582 __PACKAGE__->register_method(
2583     method      => 'safe_delete',
2584     api_name        =>  'open-ils.serial.stream.safe_delete',
2585     signature   => q/
2586         Deletes an existing stream and associated routing list, but only
2587         if there are no attached serial items with a status other than
2588         Expected, and no non-deleted serial units.
2589         items and no issuances.
2590         @param authtoken The login session key
2591         @param strid The id of the stream to delete
2592         @return 1 on success - Event otherwise.
2593         /
2594 );
2595
2596 __PACKAGE__->register_method(
2597     method      => 'safe_delete',
2598     api_name        =>  'open-ils.serial.caption_and_pattern.safe_delete',
2599     signature   => q/
2600         Deletes an existing caption and pattern object, but only
2601         if there are no attached serial issuances. 
2602         @param authtoken The login session key
2603         @param strid The id of the scap to delete
2604         @return 1 on success - Event otherwise.
2605         /
2606 );
2607
2608 __PACKAGE__->register_method(
2609     method      => 'safe_delete',
2610     api_name        =>  'open-ils.serial.subscription.safe_delete.dry_run',
2611 );
2612 __PACKAGE__->register_method(
2613     method      => 'safe_delete',
2614     api_name        =>  'open-ils.serial.distribution.safe_delete.dry_run',
2615 );
2616 __PACKAGE__->register_method(
2617     method      => 'safe_delete',
2618     api_name        =>  'open-ils.serial.stream.safe_delete.dry_run',
2619 );
2620 __PACKAGE__->register_method(
2621     method      => 'safe_delete',
2622     api_name        =>  'open-ils.serial.caption_and_pattern.safe_delete.dry_run',
2623 );
2624
2625 sub safe_delete {
2626     my( $self, $conn, $authtoken, $id ) = @_;
2627
2628     $self->api_name =~ /serial\.(\w*)\.safe_delete/;
2629     my $type = $1;
2630
2631     my $e = new_editor(xact=>1, authtoken=>$authtoken);
2632     return $e->die_event unless $e->checkauth;
2633
2634     my $obj;
2635
2636     if ($type eq 'stream') {
2637         my $sstr = $e->retrieve_serial_stream([
2638             $id, {
2639                 "flesh" => 2, "flesh_fields" => {
2640                     "sstr" => ["items","distribution"],
2641                     "sitem" => ["unit"]
2642                 }
2643             }
2644         ]) or return $e->die_event;
2645
2646         return $e->die_event unless $e->allowed(
2647             "ADMIN_SERIAL_STREAM", $sstr->distribution->holding_lib
2648         );
2649
2650         foreach my $sitem (@{$sstr->items}) {
2651             if ($sitem->status ne 'Expected') {
2652                 return $e->die_event(OpenILS::Event->new('SERIAL_STREAM_NOT_EMPTY', payload=>$id));
2653             }
2654             if ($sitem->unit && !$U->is_true($sitem->unit->deleted)) {
2655                 return $e->die_event(OpenILS::Event->new('SERIAL_STREAM_NOT_EMPTY', payload=>$id));
2656             }
2657         }
2658
2659         $obj = $sstr;
2660
2661     } elsif ($type eq 'distribution') {
2662         my $sdist = $e->retrieve_serial_distribution([
2663             $id, {
2664                 "flesh" => 3, "flesh_fields" => {
2665                     "sstr" => ["items"],
2666                     "sdist" => ["streams"],
2667                     "sitem" => ["unit"]
2668                 }
2669             }
2670         ]) or return $e->die_event;
2671
2672         return $e->die_event unless
2673             $e->allowed("ADMIN_SERIAL_DISTRIBUTION", $sdist->holding_lib);
2674
2675         foreach my $sstr (@{$sdist->streams}) {
2676             foreach my $sitem (@{$sstr->items}) {
2677                 if ($sitem->status ne 'Expected') {
2678                     return $e->die_event(OpenILS::Event->new('SERIAL_DISTRIBUTION_NOT_EMPTY', payload=>$id));
2679                 }
2680                 if ($sitem->unit && !$U->is_true($sitem->unit->deleted)) {
2681                     return $e->die_event(OpenILS::Event->new('SERIAL_DISTRIBUTION_NOT_EMPTY', payload=>$id));
2682                 }
2683             }
2684         }
2685
2686         $obj = $sdist;
2687
2688     } elsif ($type eq 'caption_and_pattern') {
2689         my $scap = $e->retrieve_serial_caption_and_pattern([
2690             $id,
2691             { flesh => 1, flesh_fields => { scap => ['subscription'] } }
2692         ]) or return $e->die_event;
2693
2694         return $e->die_event unless
2695             $e->allowed("ADMIN_SERIAL_CAPTION_PATTERN", $scap->subscription->owning_lib);
2696
2697         my $issuances = $e->search_serial_issuance([{
2698             caption_and_pattern => $id
2699         },{
2700             flesh => 2,
2701             flesh_fields => {
2702                 siss  => ['items'],
2703                 sitem => ['unit']
2704             }
2705         }]);
2706
2707         foreach my $siss (@$issuances) {
2708             foreach my $sitem (@{$siss->items}) {
2709                 if ($sitem->status ne 'Expected') {
2710                     return $e->die_event(OpenILS::Event->new('SERIAL_CAPTION_AND_PATTERN_NOT_EMPTY', payload=>$id));
2711                 }
2712                 if ($sitem->unit && !$U->is_true($sitem->unit->deleted)) {
2713                     return $e->die_event(OpenILS::Event->new('SERIAL_CAPTION_AND_PATTERN_NOT_EMPTY', payload=>$id));
2714                 }
2715             }
2716         }
2717
2718         $obj = $scap;
2719
2720     } else { # subscription
2721         my $sub = $e->retrieve_serial_subscription([
2722             $id, {
2723                 "flesh" => 4, "flesh_fields" => {
2724                     "ssub" => [qw/distributions issuances/],
2725                     "sdist" => [qw/streams/],
2726                     "sstr" => ["items"],
2727                     "sitem" => ["unit"]
2728                 }
2729             }
2730         ]) or return $e->die_event;
2731
2732         return $e->die_event unless
2733             $e->allowed("ADMIN_SERIAL_SUBSCRIPTION", $sub->owning_lib);
2734
2735         foreach my $sdist (@{$sub->distributions}) {
2736             foreach my $sstr (@{$sdist->streams}) {
2737                 foreach my $sitem (@{$sstr->items}) {
2738                     if ($sitem->status ne 'Expected') {
2739                         return $e->die_event(OpenILS::Event->new('SERIAL_SUBSCRIPTION_NOT_EMPTY', payload=>$id));
2740                     }
2741                     if ($sitem->unit && !$U->is_true($sitem->unit->deleted)) {
2742                         return $e->die_event(OpenILS::Event->new('SERIAL_SUBSCRIPTION_NOT_EMPTY', payload=>$id));
2743                     }
2744                 }
2745             }
2746         }
2747
2748         $obj = $sub;
2749     }
2750
2751     if (! ($self->api_name =~ /dry_run/)) {
2752         my $method = "delete_serial_${type}";
2753         $e->$method($obj) or return $e->die_event;
2754         $e->commit;
2755     }
2756
2757     return 1;
2758 }
2759
2760 __PACKAGE__->register_method(
2761     method    => 'fleshed_ssub_alter',
2762     api_name  => 'open-ils.serial.subscription.fleshed.batch.update',
2763     api_level => 1,
2764     argc      => 2,
2765     signature => {
2766         desc     => 'Receives an array of one or more subscriptions and updates the database as needed',
2767         'params' => [ {
2768                  name => 'authtoken',
2769                  desc => 'Authtoken for current user session',
2770                  type => 'string'
2771             },
2772             {
2773                  name => 'subscriptions',
2774                  desc => 'Array of fleshed subscriptions',
2775                  type => 'array'
2776             }
2777
2778         ],
2779         'return' => {
2780             desc => 'Returns 1 if successful, event if failed',
2781             type => 'mixed'
2782         }
2783     }
2784 );
2785
2786 sub fleshed_ssub_alter {
2787     my( $self, $conn, $auth, $ssubs ) = @_;
2788     return 1 unless ref $ssubs;
2789     my( $reqr, $evt ) = $U->checkses($auth);
2790     return $evt if $evt;
2791     my $editor = new_editor(requestor => $reqr, xact => 1);
2792     my $override = $self->api_name =~ /override/;
2793
2794     for my $ssub (@$ssubs) {
2795         my $owning_lib_id = ref $ssub->owning_lib ? $ssub->owning_lib->id : $ssub->owning_lib;
2796         return $editor->die_event unless
2797             $editor->allowed("ADMIN_SERIAL_SUBSCRIPTION", $owning_lib_id);
2798
2799         my $ssubid = $ssub->id;
2800
2801         if( $ssub->isdeleted ) {
2802             $evt = _delete_ssub( $editor, $override, $ssub);
2803         } elsif( $ssub->isnew ) {
2804             _cleanse_dates($ssub, ['start_date','end_date']);
2805             $evt = _create_ssub( $editor, $ssub );
2806         } else {
2807             _cleanse_dates($ssub, ['start_date','end_date']);
2808             $evt = _update_ssub( $editor, $override, $ssub );
2809         }
2810     }
2811
2812     if( $evt ) {
2813         $logger->info("fleshed subscription-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
2814         $editor->rollback;
2815         return $evt;
2816     }
2817     $logger->debug("subscription-alter: done updating subscription batch");
2818     $editor->commit;
2819     $logger->info("fleshed subscription-alter successfully updated ".scalar(@$ssubs)." subscriptions");
2820     return 1;
2821 }
2822
2823 sub _delete_ssub {
2824     my ($editor, $override, $ssub) = @_;
2825     $logger->info("subscription-alter: delete subscription ".OpenSRF::Utils::JSON->perl2JSON($ssub));
2826     my $sdists = $editor->search_serial_distribution(
2827             { subscription => $ssub->id }, { limit => 1 } ); #TODO: 'deleted' support?
2828     my $cps = $editor->search_serial_caption_and_pattern(
2829             { subscription => $ssub->id }, { limit => 1 } ); #TODO: 'deleted' support?
2830     my $sisses = $editor->search_serial_issuance(
2831             { subscription => $ssub->id }, { limit => 1 } ); #TODO: 'deleted' support?
2832     return OpenILS::Event->new(
2833             'SERIAL_SUBSCRIPTION_NOT_EMPTY', payload => $ssub->id ) if (@$sdists or @$cps or @$sisses);
2834
2835     return $editor->event unless $editor->delete_serial_subscription($ssub);
2836     return 0;
2837 }
2838
2839 sub _create_ssub {
2840     my ($editor, $ssub) = @_;
2841
2842     $logger->info("subscription-alter: new subscription ".OpenSRF::Utils::JSON->perl2JSON($ssub));
2843     return $editor->event unless $editor->create_serial_subscription($ssub);
2844     return 0;
2845 }
2846
2847 sub _update_ssub {
2848     my ($editor, $override, $ssub) = @_;
2849
2850     $logger->info("subscription-alter: retrieving subscription ".$ssub->id);
2851     my $orig_ssub = $editor->retrieve_serial_subscription($ssub->id);
2852
2853     $logger->info("subscription-alter: original subscription ".OpenSRF::Utils::JSON->perl2JSON($orig_ssub));
2854     $logger->info("subscription-alter: updated subscription ".OpenSRF::Utils::JSON->perl2JSON($ssub));
2855     return $editor->event unless $editor->update_serial_subscription($ssub);
2856     return 0;
2857 }
2858
2859 __PACKAGE__->register_method(
2860     method  => "fleshed_serial_subscription_retrieve_batch",
2861     authoritative => 1,
2862     api_name    => "open-ils.serial.subscription.fleshed.batch.retrieve"
2863 );
2864
2865 sub fleshed_serial_subscription_retrieve_batch {
2866     my( $self, $client, $ids ) = @_;
2867 # FIXME: permissions?
2868     $logger->info("Fetching fleshed subscriptions @$ids");
2869     return $U->cstorereq(
2870         "open-ils.cstore.direct.serial.subscription.search.atomic",
2871         { id => $ids },
2872         { flesh => 1,
2873           flesh_fields => {ssub => [ qw/owning_lib notes/ ]}
2874         });
2875 }
2876
2877 __PACKAGE__->register_method(
2878     method  => "retrieve_sub_tree",
2879     authoritative => 1,
2880     api_name    => "open-ils.serial.subscription_tree.retrieve"
2881 );
2882
2883 __PACKAGE__->register_method(
2884     method  => "retrieve_sub_tree",
2885     api_name    => "open-ils.serial.subscription_tree.global.retrieve"
2886 );
2887
2888 sub retrieve_sub_tree {
2889
2890     my( $self, $client, $user_session, $docid, @org_ids ) = @_;
2891
2892     if(ref($org_ids[0])) { @org_ids = @{$org_ids[0]}; }
2893
2894     $docid = "$docid";
2895
2896     # TODO: permission support
2897     if(!@org_ids and $user_session) {
2898         my $user_obj = 
2899             OpenILS::Application::AppUtils->check_user_session( $user_session ); #throws EX on error
2900             @org_ids = ($user_obj->home_ou);
2901     }
2902
2903     if( $self->api_name =~ /global/ ) {
2904         return _build_subs_list( { record_entry => $docid } ); # TODO: filter for !deleted, or active?
2905
2906     } else {
2907
2908         my @all_subs;
2909         for my $orgid (@org_ids) {
2910             my $subs = _build_subs_list( 
2911                     { record_entry => $docid, owning_lib => $orgid } );# TODO: filter for !deleted, or active?
2912             push( @all_subs, @$subs );
2913         }
2914         
2915         return \@all_subs;
2916     }
2917
2918     return undef;
2919 }
2920
2921 sub _build_subs_list {
2922     my $search_hash = shift;
2923
2924     #$search_hash->{deleted} = 'f';
2925     my $e = new_editor();
2926
2927     my $subs = $e->search_serial_subscription([$search_hash, { 'order_by' => {'ssub' => 'id'} }]);
2928
2929     my @built_subs;
2930
2931     for my $sub (@$subs) {
2932
2933         # TODO: filter on !deleted?
2934         my $dists = $e->search_serial_distribution(
2935             [{ subscription => $sub->id }, { 'order_by' => {'sdist' => 'label'} }]
2936             );
2937
2938         #$dists = [ sort { $a->label cmp $b->label } @$dists  ];
2939
2940         $sub->distributions($dists);
2941         
2942         # TODO: filter on !deleted?
2943         my $issuances = $e->search_serial_issuance(
2944             [{ subscription => $sub->id }, { 'order_by' => {'siss' => 'label'} }]
2945             );
2946
2947         #$issuances = [ sort { $a->label cmp $b->label } @$issuances  ];
2948         $sub->issuances($issuances);
2949
2950         # TODO: filter on !deleted?
2951         my $scaps = $e->search_serial_caption_and_pattern(
2952             [{ subscription => $sub->id }, { 'order_by' => {'scap' => 'id'} }]
2953             );
2954
2955         #$scaps = [ sort { $a->id cmp $b->id } @$scaps  ];
2956         $sub->scaps($scaps);
2957         push( @built_subs, $sub );
2958     }
2959
2960     return \@built_subs;
2961
2962 }
2963
2964 __PACKAGE__->register_method(
2965     method  => "subscription_orgs_for_title",
2966     authoritative => 1,
2967     api_name    => "open-ils.serial.subscription.retrieve_orgs_by_title"
2968 );
2969
2970 sub subscription_orgs_for_title {
2971     my( $self, $client, $record_id ) = @_;
2972
2973     my $subs = $U->simple_scalar_request(
2974         "open-ils.cstore",
2975         "open-ils.cstore.direct.serial.subscription.search.atomic",
2976         { record_entry => $record_id }); # TODO: filter on !deleted?
2977
2978     my $orgs = { map {$_->owning_lib => 1 } @$subs };
2979     return [ keys %$orgs ];
2980 }
2981
2982
2983 ##########################################################################
2984 # distribution methods
2985 #
2986 __PACKAGE__->register_method(
2987     method    => 'fleshed_sdist_alter',
2988     api_name  => 'open-ils.serial.distribution.fleshed.batch.update',
2989     api_level => 1,
2990     argc      => 2,
2991     signature => {
2992         desc     => 'Receives an array of one or more distributions and updates the database as needed',
2993         'params' => [ {
2994                  name => 'authtoken',
2995                  desc => 'Authtoken for current user session',
2996                  type => 'string'
2997             },
2998             {
2999                  name => 'distributions',
3000                  desc => 'Array of fleshed distributions',
3001                  type => 'array'
3002             }
3003
3004         ],
3005         'return' => {
3006             desc => 'Returns 1 if successful, event if failed',
3007             type => 'mixed'
3008         }
3009     }
3010 );
3011
3012 sub fleshed_sdist_alter {
3013     my( $self, $conn, $auth, $sdists ) = @_;
3014     return 1 unless ref $sdists;
3015     my( $reqr, $evt ) = $U->checkses($auth);
3016     return $evt if $evt;
3017     my $editor = new_editor(requestor => $reqr, xact => 1);
3018     my $override = $self->api_name =~ /override/;
3019
3020     for my $sdist (@$sdists) {
3021         my $holding_lib_id = ref $sdist->holding_lib ? $sdist->holding_lib->id : $sdist->holding_lib;
3022         return $editor->die_event unless
3023             $editor->allowed("ADMIN_SERIAL_DISTRIBUTION", $holding_lib_id);
3024
3025         if( $sdist->isdeleted ) {
3026             $evt = _delete_sdist( $editor, $override, $sdist);
3027         } elsif( $sdist->isnew ) {
3028             $evt = _create_sdist( $editor, $sdist );
3029         } else {
3030             $evt = _update_sdist( $editor, $override, $sdist );
3031         }
3032     }
3033
3034     if( $evt ) {
3035         $logger->info("fleshed distribution-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
3036         $editor->rollback;
3037         return $evt;
3038     }
3039     $logger->debug("distribution-alter: done updating distribution batch");
3040     $editor->commit;
3041     $logger->info("fleshed distribution-alter successfully updated ".scalar(@$sdists)." distributions");
3042     return 1;
3043 }
3044
3045 sub _delete_sdist {
3046     my ($editor, $override, $sdist) = @_;
3047     $logger->info("distribution-alter: delete distribution ".OpenSRF::Utils::JSON->perl2JSON($sdist));
3048     return $editor->event unless $editor->delete_serial_distribution($sdist);
3049     return 0;
3050 }
3051
3052 sub _create_sdist {
3053     my ($editor, $sdist) = @_;
3054
3055     $logger->info("distribution-alter: new distribution ".OpenSRF::Utils::JSON->perl2JSON($sdist));
3056     return $editor->event unless $editor->create_serial_distribution($sdist);
3057
3058     # create summaries too
3059     my $summary = new Fieldmapper::serial::basic_summary;
3060     $summary->distribution($sdist->id);
3061     $summary->generated_coverage('');
3062     return $editor->event unless $editor->create_serial_basic_summary($summary);
3063     $summary = new Fieldmapper::serial::supplement_summary;
3064     $summary->distribution($sdist->id);
3065     $summary->generated_coverage('');
3066     return $editor->event unless $editor->create_serial_supplement_summary($summary);
3067     $summary = new Fieldmapper::serial::index_summary;
3068     $summary->distribution($sdist->id);
3069     $summary->generated_coverage('');
3070     return $editor->event unless $editor->create_serial_index_summary($summary);
3071
3072     # create a starter stream (TODO: reconsider this)
3073     my $stream = new Fieldmapper::serial::stream;
3074     $stream->distribution($sdist->id);
3075     return $editor->event unless $editor->create_serial_stream($stream);
3076
3077     return 0;
3078 }
3079
3080 sub _update_sdist {
3081     my ($editor, $override, $sdist) = @_;
3082
3083     $logger->info("distribution-alter: retrieving distribution ".$sdist->id);
3084     my $orig_sdist = $editor->retrieve_serial_distribution($sdist->id);
3085
3086     $logger->info("distribution-alter: original distribution ".OpenSRF::Utils::JSON->perl2JSON($orig_sdist));
3087     $logger->info("distribution-alter: updated distribution ".OpenSRF::Utils::JSON->perl2JSON($sdist));
3088     return $editor->event unless $editor->update_serial_distribution($sdist);
3089     return 0;
3090 }
3091
3092 __PACKAGE__->register_method(
3093     method  => "fleshed_serial_distribution_retrieve_batch",
3094     authoritative => 1,
3095     api_name    => "open-ils.serial.distribution.fleshed.batch.retrieve"
3096 );
3097
3098 sub fleshed_serial_distribution_retrieve_batch {
3099     my( $self, $client, $ids ) = @_;
3100 # FIXME: permissions?
3101     $logger->info("Fetching fleshed distributions @$ids");
3102     return $U->cstorereq(
3103         "open-ils.cstore.direct.serial.distribution.search.atomic",
3104         { id => $ids },
3105         { flesh => 1,
3106           flesh_fields => {sdist => [ qw/ holding_lib receive_call_number receive_unit_template bind_call_number bind_unit_template streams notes / ]}
3107         });
3108 }
3109
3110 __PACKAGE__->register_method(
3111     method  => "retrieve_dist_tree",
3112     authoritative => 1,
3113     api_name    => "open-ils.serial.distribution_tree.retrieve"
3114 );
3115
3116 __PACKAGE__->register_method(
3117     method  => "retrieve_dist_tree",
3118     api_name    => "open-ils.serial.distribution_tree.global.retrieve"
3119 );
3120
3121 sub retrieve_dist_tree {
3122     my( $self, $client, $user_session, $docid, @org_ids ) = @_;
3123
3124     if(ref($org_ids[0])) { @org_ids = @{$org_ids[0]}; }
3125
3126     $docid = "$docid";
3127
3128     # TODO: permission support
3129     if(!@org_ids and $user_session) {
3130         my $user_obj =
3131             OpenILS::Application::AppUtils->check_user_session( $user_session ); #throws EX on error
3132             @org_ids = ($user_obj->home_ou);
3133     }
3134
3135     my $e = new_editor();
3136
3137     if( $self->api_name =~ /global/ ) {
3138         return $e->search_serial_distribution([{'+ssub' => { record_entry => $docid }},
3139             {   flesh => 1,
3140                 flesh_fields => {sdist => [ qw/ holding_lib receive_call_number receive_unit_template bind_call_number bind_unit_template streams basic_summary supplement_summary index_summary / ]},
3141                 order_by => {'sdist' => 'id'},
3142                 'join' => {'ssub' => {}}
3143             }
3144         ]); # TODO: filter for !deleted?
3145
3146     } else {
3147         my @all_dists;
3148         for my $orgid (@org_ids) {
3149             my $dists = $e->search_serial_distribution([{'+ssub' => { record_entry => $docid }, holding_lib => $orgid},
3150                 {   flesh => 1,
3151                     flesh_fields => {sdist => [ qw/ holding_lib receive_call_number receive_unit_template bind_call_number bind_unit_template streams basic_summary supplement_summary index_summary / ]},
3152                     order_by => {'sdist' => 'id'},
3153                     'join' => {'ssub' => {}}
3154                 }
3155             ]); # TODO: filter for !deleted?
3156             push( @all_dists, @$dists ) if $dists;
3157         }
3158
3159         return \@all_dists;
3160     }
3161
3162     return undef;
3163 }
3164
3165
3166 __PACKAGE__->register_method(
3167     method  => "distribution_orgs_for_title",
3168     authoritative => 1,
3169     api_name    => "open-ils.serial.distribution.retrieve_orgs_by_title"
3170 );
3171
3172 sub distribution_orgs_for_title {
3173     my( $self, $client, $record_id ) = @_;
3174
3175     my $dists = $U->cstorereq(
3176         "open-ils.cstore.direct.serial.distribution.search.atomic",
3177         { '+ssub' => { record_entry => $record_id } },
3178         { 'join' => {'ssub' => {}} }); # TODO: filter on !deleted?
3179
3180     my $orgs = { map {$_->holding_lib => 1 } @$dists };
3181     return [ keys %$orgs ];
3182 }
3183
3184
3185 ##########################################################################
3186 # caption and pattern methods
3187 #
3188 __PACKAGE__->register_method(
3189     method    => 'scap_alter',
3190     api_name  => 'open-ils.serial.caption_and_pattern.batch.update',
3191     api_level => 1,
3192     argc      => 2,
3193     signature => {
3194         desc     => 'Receives an array of one or more caption and patterns and updates the database as needed',
3195         'params' => [ {
3196                  name => 'authtoken',
3197                  desc => 'Authtoken for current user session',
3198                  type => 'string'
3199             },
3200             {
3201                  name => 'scaps',
3202                  desc => 'Array of caption and patterns',
3203                  type => 'array'
3204             }
3205
3206         ],
3207         'return' => {
3208             desc => 'Returns 1 if successful, event if failed',
3209             type => 'mixed'
3210         }
3211     }
3212 );
3213
3214 sub scap_alter {
3215     my( $self, $conn, $auth, $scaps ) = @_;
3216     return 1 unless ref $scaps;
3217     my( $reqr, $evt ) = $U->checkses($auth);
3218     return $evt if $evt;
3219     my $editor = new_editor(requestor => $reqr, xact => 1);
3220     my $override = $self->api_name =~ /override/;
3221
3222     my %found_ssub_ids;
3223     for my $scap (@$scaps) {
3224         if (!exists($found_ssub_ids{$scap->subscription})) {
3225             my $ssub = $editor->retrieve_serial_subscription($scap->subscription) or return $editor->die_event;
3226             return $editor->die_event unless
3227                 $editor->allowed("ADMIN_SERIAL_CAPTION_PATTERN", $ssub->owning_lib);
3228             $found_ssub_ids{$scap->subscription} = 1;
3229         }
3230
3231         if( $scap->isdeleted ) {
3232             $evt = _delete_scap( $editor, $override, $scap);
3233         } elsif( $scap->isnew ) {
3234             $evt = _create_scap( $editor, $scap );
3235         } else {
3236             $evt = _update_scap( $editor, $override, $scap );
3237         }
3238     }
3239
3240     if( $evt ) {
3241         $logger->info("caption_and_pattern-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
3242         $editor->rollback;
3243         return $evt;
3244     }
3245     $logger->debug("caption_and_pattern-alter: done updating caption_and_pattern batch");
3246     $editor->commit;
3247     $logger->info("caption_and_pattern-alter successfully updated ".scalar(@$scaps)." caption_and_patterns");
3248     return 1;
3249 }
3250
3251 sub _delete_scap {
3252     my ($editor, $override, $scap) = @_;
3253     $logger->info("caption_and_pattern-alter: delete caption_and_pattern ".OpenSRF::Utils::JSON->perl2JSON($scap));
3254     my $sisses = $editor->search_serial_issuance(
3255             { caption_and_pattern => $scap->id }, { limit => 1 } ); #TODO: 'deleted' support?
3256     return OpenILS::Event->new(
3257             'SERIAL_CAPTION_AND_PATTERN_HAS_ISSUANCES', payload => $scap->id ) if (@$sisses);
3258
3259     return $editor->event unless $editor->delete_serial_caption_and_pattern($scap);
3260     return 0;
3261 }
3262
3263 sub _create_scap {
3264     my ($editor, $scap) = @_;
3265
3266     $logger->info("caption_and_pattern-alter: new caption_and_pattern ".OpenSRF::Utils::JSON->perl2JSON($scap));
3267     return $editor->event unless $editor->create_serial_caption_and_pattern($scap);
3268     return 0;
3269 }
3270
3271 sub _update_scap {
3272     my ($editor, $override, $scap) = @_;
3273
3274     $logger->info("caption_and_pattern-alter: retrieving caption_and_pattern ".$scap->id);
3275     my $orig_scap = $editor->retrieve_serial_caption_and_pattern($scap->id);
3276
3277     $logger->info("caption_and_pattern-alter: original caption_and_pattern ".OpenSRF::Utils::JSON->perl2JSON($orig_scap));
3278     $logger->info("caption_and_pattern-alter: updated caption_and_pattern ".OpenSRF::Utils::JSON->perl2JSON($scap));
3279     return $editor->event unless $editor->update_serial_caption_and_pattern($scap);
3280     return 0;
3281 }
3282
3283 __PACKAGE__->register_method(
3284     method  => "serial_caption_and_pattern_retrieve_batch",
3285     authoritative => 1,
3286     api_name    => "open-ils.serial.caption_and_pattern.batch.retrieve"
3287 );
3288
3289 sub serial_caption_and_pattern_retrieve_batch {
3290     my( $self, $client, $ids ) = @_;
3291     $logger->info("Fetching caption_and_patterns @$ids");
3292     return $U->cstorereq(
3293         "open-ils.cstore.direct.serial.caption_and_pattern.search.atomic",
3294         { id => $ids }
3295     );
3296 }
3297
3298 ##########################################################################
3299 # stream methods
3300 #
3301 __PACKAGE__->register_method(
3302     method    => 'sstr_alter',
3303     api_name  => 'open-ils.serial.stream.batch.update',
3304     api_level => 1,
3305     argc      => 2,
3306     signature => {
3307         desc     => 'Receives an array of one or more streams and updates the database as needed',
3308         'params' => [ {
3309                  name => 'authtoken',
3310                  desc => 'Authtoken for current user session',
3311                  type => 'string'
3312             },
3313             {
3314                  name => 'sstrs',
3315                  desc => 'Array of streams',
3316                  type => 'array'
3317             }
3318
3319         ],
3320         'return' => {
3321             desc => 'Returns 1 if successful, event if failed',
3322             type => 'mixed'
3323         }
3324     }
3325 );
3326
3327 sub sstr_alter {
3328     my( $self, $conn, $auth, $sstrs ) = @_;
3329     return 1 unless ref $sstrs;
3330     my( $reqr, $evt ) = $U->checkses($auth);
3331     return $evt if $evt;
3332     my $editor = new_editor(requestor => $reqr, xact => 1);
3333     my $override = $self->api_name =~ /override/;
3334
3335     my %found_sdist_ids;
3336     for my $sstr (@$sstrs) {
3337         if (!exists($found_sdist_ids{$sstr->distribution})) {
3338             my $sdist = $editor->retrieve_serial_distribution($sstr->distribution) or return $editor->die_event;
3339             return $editor->die_event unless
3340                 $editor->allowed("ADMIN_SERIAL_STREAM", $sdist->holding_lib);
3341             $found_sdist_ids{$sstr->distribution} = 1;
3342         }
3343
3344         if( $sstr->isdeleted ) {
3345             $evt = _delete_sstr( $editor, $override, $sstr);
3346         } elsif( $sstr->isnew ) {
3347             $evt = _create_sstr( $editor, $sstr );
3348         } else {
3349             $evt = _update_sstr( $editor, $override, $sstr );
3350         }
3351     }
3352
3353     if( $evt ) {
3354         $logger->info("stream-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
3355         $editor->rollback;
3356         return $evt;
3357     }
3358     $logger->debug("stream-alter: done updating stream batch");
3359     $editor->commit;
3360     $logger->info("stream-alter successfully updated ".scalar(@$sstrs)." streams");
3361     return 1;
3362 }
3363
3364 sub _delete_sstr {
3365     my ($editor, $override, $sstr) = @_;
3366     $logger->info("stream-alter: delete stream ".OpenSRF::Utils::JSON->perl2JSON($sstr));
3367     my $sitems = $editor->search_serial_item(
3368             { stream => $sstr->id }, { limit => 1 } ); #TODO: 'deleted' support?
3369     return OpenILS::Event->new(
3370             'SERIAL_STREAM_HAS_ITEMS', payload => $sstr->id ) if (@$sitems);
3371
3372     return $editor->event unless $editor->delete_serial_stream($sstr);
3373     return 0;
3374 }
3375
3376 sub _create_sstr {
3377     my ($editor, $sstr) = @_;
3378
3379     $logger->info("stream-alter: new stream ".OpenSRF::Utils::JSON->perl2JSON($sstr));
3380     return $editor->event unless $editor->create_serial_stream($sstr);
3381     return 0;
3382 }
3383
3384 sub _update_sstr {
3385     my ($editor, $override, $sstr) = @_;
3386
3387     $logger->info("stream-alter: retrieving stream ".$sstr->id);
3388     my $orig_sstr = $editor->retrieve_serial_stream($sstr->id);
3389
3390     $logger->info("stream-alter: original stream ".OpenSRF::Utils::JSON->perl2JSON($orig_sstr));
3391     $logger->info("stream-alter: updated stream ".OpenSRF::Utils::JSON->perl2JSON($sstr));
3392     return $editor->event unless $editor->update_serial_stream($sstr);
3393     return 0;
3394 }
3395
3396 __PACKAGE__->register_method(
3397     method  => "serial_stream_retrieve_batch",
3398     authoritative => 1,
3399     api_name    => "open-ils.serial.stream.batch.retrieve"
3400 );
3401
3402 sub serial_stream_retrieve_batch {
3403     my( $self, $client, $ids ) = @_;
3404     $logger->info("Fetching streams @$ids");
3405     return $U->cstorereq(
3406         "open-ils.cstore.direct.serial.stream.search.atomic",
3407         { id => $ids }
3408     );
3409 }
3410
3411
3412 ##########################################################################
3413 # summary methods
3414 #
3415 __PACKAGE__->register_method(
3416     method    => 'sum_alter',
3417     api_name  => 'open-ils.serial.basic_summary.batch.update',
3418     api_level => 1,
3419     argc      => 2,
3420     signature => {
3421         desc     => 'Receives an array of one or more summaries and updates the database as needed',
3422         'params' => [ {
3423                  name => 'authtoken',
3424                  desc => 'Authtoken for current user session',
3425                  type => 'string'
3426             },
3427             {
3428                  name => 'sbsums',
3429                  desc => 'Array of basic summaries',
3430                  type => 'array'
3431             }
3432
3433         ],
3434         'return' => {
3435             desc => 'Returns 1 if successful, event if failed',
3436             type => 'mixed'
3437         }
3438     }
3439 );
3440
3441 __PACKAGE__->register_method(
3442     method    => 'sum_alter',
3443     api_name  => 'open-ils.serial.supplement_summary.batch.update',
3444     api_level => 1,
3445     argc      => 2,
3446     signature => {
3447         desc     => 'Receives an array of one or more summaries and updates the database as needed',
3448         'params' => [ {
3449                  name => 'authtoken',
3450                  desc => 'Authtoken for current user session',
3451                  type => 'string'
3452             },
3453             {
3454                  name => 'sbsums',
3455                  desc => 'Array of supplement summaries',
3456                  type => 'array'
3457             }
3458
3459         ],
3460         'return' => {
3461             desc => 'Returns 1 if successful, event if failed',
3462             type => 'mixed'
3463         }
3464     }
3465 );
3466
3467 __PACKAGE__->register_method(
3468     method    => 'sum_alter',
3469     api_name  => 'open-ils.serial.index_summary.batch.update',
3470     api_level => 1,
3471     argc      => 2,
3472     signature => {
3473         desc     => 'Receives an array of one or more summaries and updates the database as needed',
3474         'params' => [ {
3475                  name => 'authtoken',
3476                  desc => 'Authtoken for current user session',
3477                  type => 'string'
3478             },
3479             {
3480                  name => 'sbsums',
3481                  desc => 'Array of index summaries',
3482                  type => 'array'
3483             }
3484
3485         ],
3486         'return' => {
3487             desc => 'Returns 1 if successful, event if failed',
3488             type => 'mixed'
3489         }
3490     }
3491 );
3492
3493 sub sum_alter {
3494     my( $self, $conn, $auth, $sums ) = @_;
3495     return 1 unless ref $sums;
3496
3497     $self->api_name =~ /serial\.(\w*)_summary/;
3498     my $type = $1;
3499
3500     my( $reqr, $evt ) = $U->checkses($auth);
3501     return $evt if $evt;
3502     my $editor = new_editor(requestor => $reqr, xact => 1);
3503     my $override = $self->api_name =~ /override/;
3504
3505     my %found_sdist_ids;
3506     for my $sum (@$sums) {
3507         if (!exists($found_sdist_ids{$sum->distribution})) {
3508             my $sdist = $editor->retrieve_serial_distribution($sum->distribution) or return $editor->die_event;
3509             return $editor->die_event unless
3510                 $editor->allowed("ADMIN_SERIAL_DISTRIBUTION", $sdist->holding_lib);
3511             $found_sdist_ids{$sum->distribution} = 1;
3512         }
3513
3514         # XXX: (for now, at least) summaries should be created/deleted by the distribution functions
3515         if( $sum->isdeleted ) {
3516             $evt = OpenILS::Event->new('SERIAL_SUMMARIES_NOT_INDEPENDENT');
3517         } elsif( $sum->isnew ) {
3518             $evt = OpenILS::Event->new('SERIAL_SUMMARIES_NOT_INDEPENDENT');
3519         } else {
3520             $evt = _update_sum( $editor, $override, $sum, $type );
3521         }
3522     }
3523
3524     if( $evt ) {
3525         $logger->info("${type}_summary-alter failed with event: ".OpenSRF::Utils::JSON->perl2JSON($evt));
3526         $editor->rollback;
3527         return $evt;
3528     }
3529     $logger->debug("${type}_summary-alter: done updating ${type}_summary batch");
3530     $editor->commit;
3531     $logger->info("${type}_summary-alter successfully updated ".scalar(@$sums)." ${type}_summaries");
3532     return 1;
3533 }
3534
3535 sub _update_sum {
3536     my ($editor, $override, $sum, $type) = @_;
3537
3538     $logger->info("${type}_summary-alter: retrieving ${type}_summary ".$sum->id);
3539     my $retrieve_method = "retrieve_serial_${type}_summary";
3540     my $orig_sum = $editor->$retrieve_method($sum->id);
3541
3542     $logger->info("${type}_summary-alter: original ${type}_summary ".OpenSRF::Utils::JSON->perl2JSON($orig_sum));
3543     $logger->info("${type}_summary-alter: updated ${type}_summary ".OpenSRF::Utils::JSON->perl2JSON($sum));
3544     my $update_method = "update_serial_${type}_summary";
3545     return $editor->event unless $editor->$update_method($sum);
3546     return 0;
3547 }
3548
3549 __PACKAGE__->register_method(
3550     method  => "serial_summary_retrieve_batch",
3551     authoritative => 1,
3552     api_name    => "open-ils.serial.basic_summary.batch.retrieve"
3553 );
3554
3555 __PACKAGE__->register_method(
3556     method  => "serial_summary_retrieve_batch",
3557     authoritative => 1,
3558     api_name    => "open-ils.serial.supplement_summary.batch.retrieve"
3559 );
3560
3561 __PACKAGE__->register_method(
3562     method  => "serial_summary_retrieve_batch",
3563     authoritative => 1,
3564     api_name    => "open-ils.serial.index_summary.batch.retrieve"
3565 );
3566
3567 sub serial_summary_retrieve_batch {
3568     my( $self, $client, $ids ) = @_;
3569
3570     $self->api_name =~ /serial\.(\w*)_summary/;
3571     my $type = $1;
3572
3573     $logger->info("Fetching ${type}_summaries @$ids");
3574     return $U->cstorereq(
3575         "open-ils.cstore.direct.serial.".$type."_summary.search.atomic",
3576         { id => $ids }
3577     );
3578 }
3579
3580
3581 ##########################################################################
3582 # other methods
3583 #
3584 __PACKAGE__->register_method(
3585     "method" => "bre_by_identifier",
3586     "api_name" => "open-ils.serial.biblio.record_entry.by_identifier",
3587     "stream" => 1,
3588     "signature" => {
3589         "desc" => "Find instances of biblio.record_entry given a search token" .
3590             " that could be a value for any identifier defined in " .
3591             "config.metabib_field",
3592         "params" => [
3593             {"desc" => "Search token", "type" => "string"},
3594             {"desc" => "Options: require_subscriptions, add_mvr, is_actual_id" .
3595                 ", id_list (all boolean)", "type" => "object"}
3596         ],
3597         "return" => {
3598             "desc" => "Any matching BREs, or if the add_mvr option is true, " .
3599                 "objects with a 'bre' key/value pair, and an 'mvr' " .
3600                 "key-value pair.  BREs have subscriptions fleshed on.",
3601             "type" => "object"
3602         }
3603     }
3604 );
3605
3606 sub bre_by_identifier {
3607     my ($self, $client, $term, $options) = @_;
3608
3609     return new OpenILS::Event("BAD_PARAMS") unless $term;
3610
3611     $options ||= {};
3612     my $e = new_editor();
3613
3614     my @ids;
3615
3616     if ($options->{"is_actual_id"}) {
3617         @ids = ($term);
3618     } else {
3619         my $cmf =
3620             $e->search_config_metabib_field({"field_class" => "identifier"})
3621                 or return $e->die_event;
3622
3623         my @identifiers = map { $_->name } @$cmf;
3624         my $query = join(" || ", map { "id|$_: $term" } @identifiers);
3625
3626         my $search = create OpenSRF::AppSession("open-ils.search");
3627         my $search_result = $search->request(
3628             "open-ils.search.biblio.multiclass.query.staff", {}, $query
3629         )->gather(1);
3630         $search->disconnect;
3631
3632         # Un-nest results. They tend to look like [[1],[2],[3]] for some reason.
3633         @ids = map { @{$_} } @{$search_result->{"ids"}};
3634
3635         unless (@ids) {
3636             $e->disconnect;
3637             return undef;
3638         }
3639
3640         if ($options->{"id_list"}) {
3641             $e->disconnect;
3642             $client->respond($_) foreach (@ids);
3643             return undef;
3644         }
3645     }
3646
3647     my $bre = $e->search_biblio_record_entry([
3648         {"id" => \@ids}, {
3649             "flesh" => 2, "flesh_fields" => {
3650                 "bre" => ["subscriptions"],
3651                 "ssub" => ["owning_lib"]
3652             }
3653         }
3654     ]) or return $e->die_event;
3655
3656     if (@$bre && $options->{"require_subscriptions"}) {
3657         $bre = [ grep { @{$_->subscriptions} } @$bre ];
3658     }
3659
3660     $e->disconnect;
3661
3662     if (@$bre) { # re-evaluate after possible grep
3663         if ($options->{"add_mvr"}) {
3664             $client->respond(
3665                 {"bre" => $_, "mvr" => _get_mvr($_->id)}
3666             ) foreach (@$bre);
3667         } else {
3668             $client->respond($_) foreach (@$bre);
3669         }
3670     }
3671
3672     undef;
3673 }
3674
3675 __PACKAGE__->register_method(
3676     "method" => "get_items_by",
3677     "api_name" => "open-ils.serial.items.receivable.by_subscription",
3678     "stream" => 1,
3679     "signature" => {
3680         "desc" => "Return all receivable items under a given subscription",
3681         "params" => [
3682             {"desc" => "Authtoken", "type" => "string"},
3683             {"desc" => "Subscription ID", "type" => "number"},
3684         ],
3685         "return" => {
3686             "desc" => "All receivable items under a given subscription",
3687             "type" => "object", "class" => "sitem"
3688         }
3689     }
3690 );
3691
3692 __PACKAGE__->register_method(
3693     "method" => "get_items_by",
3694     "api_name" => "open-ils.serial.items.receivable.by_issuance",
3695     "stream" => 1,
3696     "signature" => {
3697         "desc" => "Return all receivable items under a given issuance",
3698         "params" => [
3699             {"desc" => "Authtoken", "type" => "string"},
3700             {"desc" => "Issuance ID", "type" => "number"},
3701         ],
3702         "return" => {
3703             "desc" => "All receivable items under a given issuance",
3704             "type" => "object", "class" => "sitem"
3705         }
3706     }
3707 );
3708
3709 __PACKAGE__->register_method(
3710     "method" => "get_items_by",
3711     "api_name" => "open-ils.serial.items.by_issuance",
3712     "stream" => 1,
3713     "signature" => {
3714         "desc" => "Return all items under a given issuance",
3715         "params" => [
3716             {"desc" => "Authtoken", "type" => "string"},
3717             {"desc" => "Issuance ID", "type" => "number"},
3718         ],
3719         "return" => {
3720             "desc" => "All items under a given issuance",
3721             "type" => "object", "class" => "sitem"
3722         }
3723     }
3724 );
3725
3726 sub get_items_by {
3727     my ($self, $client, $auth, $term, $opts)  = @_;
3728
3729     # Not to be used in the json_query, but after limiting by perm check.
3730     $opts = {} unless ref $opts eq "HASH";
3731     $opts->{"limit"} ||= 10000;    # some existing users may want all results
3732     $opts->{"offset"} ||= 0;
3733     $opts->{"limit"} = int($opts->{"limit"});
3734     $opts->{"offset"} = int($opts->{"offset"});
3735
3736     my $e = new_editor("authtoken" => $auth);
3737     return $e->die_event unless $e->checkauth;
3738
3739     my $by = ($self->api_name =~ /by_(\w+)$/)[0];
3740     my $receivable = ($self->api_name =~ /receivable/);
3741
3742     my %where = (
3743         "issuance" => {"issuance" => $term},
3744         "subscription" => {"+siss" => {"subscription" => $term}}
3745     );
3746
3747     my $item_rows = $e->json_query(
3748         {
3749             "select" => {"sitem" => ["id"], "sdist" => ["holding_lib"]},
3750             "from" => {
3751                 "sitem" => {
3752                     "siss" => {},
3753                     "sstr" => {"join" => {"sdist" => {}}}
3754                 }
3755             },
3756             "where" => {
3757                 %{$where{$by}}, $receivable ? ("date_received" => undef) : ()
3758             },
3759             "order_by" => {"sitem" => ["id"]}
3760         }
3761     ) or return $e->die_event;
3762
3763     return undef unless @$item_rows;
3764
3765     my $skipped = 0;
3766     my $returned = 0;
3767     foreach (@$item_rows) {
3768         last if $returned >= $opts->{"limit"};
3769         next unless $e->allowed("RECEIVE_SERIAL", $_->{"holding_lib"});
3770         if ($skipped < $opts->{"offset"}) {
3771             $skipped++;
3772             next;
3773         }
3774
3775         $client->respond(
3776             $e->retrieve_serial_item([
3777                 $_->{"id"}, {
3778                     "flesh" => 3,
3779                     "flesh_fields" => {
3780                         "sitem" => [qw/stream issuance unit creator editor/],
3781                         "sstr" => ["distribution"],
3782                         "sdist" => ["holding_lib"]
3783                     }
3784                 }
3785             ])
3786         );
3787         $returned++;
3788     }
3789
3790     $e->disconnect;
3791     undef;
3792 }
3793
3794 __PACKAGE__->register_method(
3795     "method" => "get_receivable_issuances",
3796     "api_name" => "open-ils.serial.issuances.receivable",
3797     "stream" => 1,
3798     "signature" => {
3799         "desc" => "Return all issuances with receivable items given " .
3800             "a subscription ID",
3801         "params" => [
3802             {"desc" => "Authtoken", "type" => "string"},
3803             {"desc" => "Subscription ID", "type" => "number"},
3804         ],
3805         "return" => {
3806             "desc" => "All issuances with receivable items " .
3807                 "(but not the items themselves)", "type" => "object"
3808         }
3809     }
3810 );
3811
3812 sub get_receivable_issuances {
3813     my ($self, $client, $auth, $sub_id) = @_;
3814
3815     my $e = new_editor("authtoken" => $auth);
3816     return $e->die_event unless $e->checkauth;
3817
3818     # XXX permissions
3819
3820     my $issuance_ids = $e->json_query({
3821         "select" => {
3822             "siss" => [
3823                 {"transform" => "distinct", "column" => "id"},
3824                 "date_published"
3825             ]
3826         },
3827         "from" => {"siss" => "sitem"},
3828         "where" => {
3829             "subscription" => $sub_id,
3830             "+sitem" => {"date_received" => undef}
3831         },
3832         "order_by" => {
3833             "siss" => {"date_published" => {"direction" => "asc"}}
3834         }
3835
3836     }) or return $e->die_event;
3837
3838     $client->respond($e->retrieve_serial_issuance($_->{"id"}))
3839         foreach (@$issuance_ids);
3840
3841     $e->disconnect;
3842     undef;
3843 }
3844
3845
3846 __PACKAGE__->register_method(
3847     "method" => "get_routing_list_users",
3848     "api_name" => "open-ils.serial.routing_list_users.fleshed_and_ordered",
3849     "stream" => 1,
3850     "signature" => {
3851         "desc" => "Return all routing list users with reader fleshed " .
3852             "(with card and home_ou) for a given stream ID, sorted by pos",
3853         "params" => [
3854             {"desc" => "Authtoken", "type" => "string"},
3855             {"desc" => "Stream ID (int or array of ints)", "type" => "mixed"},
3856         ],
3857         "return" => {
3858             "desc" => "Stream of routing list users", "type" => "object",
3859                 "class" => "srlu"
3860         }
3861     }
3862 );
3863
3864 sub get_routing_list_users {
3865     my ($self, $client, $auth, $stream_id) = @_;
3866
3867     my $e = new_editor("authtoken" => $auth);
3868     return $e->die_event unless $e->checkauth;
3869
3870     my $users = $e->search_serial_routing_list_user([
3871         {"stream" => $stream_id}, {
3872             "order_by" => {"srlu" => "pos"},
3873             "flesh" => 2,
3874             "flesh_fields" => {
3875                 "srlu" => [qw/reader stream/],
3876                 "au" => [qw/card home_ou mailing_address billing_address/],
3877                 "sstr" => ["distribution"]
3878             }
3879         }
3880     ]) or return $e->die_event;
3881
3882     return undef unless @$users;
3883
3884     # The ADMIN_SERIAL_STREAM permission is used simply to avoid the
3885     # need for any new permission.  The context OU will be the same
3886     # for every result of the above query, so we need only check once.
3887     return $e->die_event unless $e->allowed(
3888         "ADMIN_SERIAL_STREAM", $users->[0]->stream->distribution->holding_lib
3889     );
3890
3891     $e->disconnect;
3892
3893     my @users = map { $_->stream($_->stream->id); $_ } @$users;
3894     @users = sort { $a->stream cmp $b->stream } @users if
3895         ref $stream_id eq "ARRAY";
3896
3897     $client->respond($_) for @users;
3898
3899     undef;
3900 }
3901
3902
3903 __PACKAGE__->register_method(
3904     "method" => "replace_routing_list_users",
3905     "api_name" => "open-ils.serial.routing_list_users.replace",
3906     "signature" => {
3907         "desc" => "Replace all routing list users on the specified streams " .
3908             "with those in the list argument",
3909         "params" => [
3910             {"desc" => "Authtoken", "type" => "string"},
3911             {"desc" => "List of srlu objects", "type" => "array"},
3912         ],
3913         "return" => {
3914             "desc" => "event on failure, undef on success"
3915         }
3916     }
3917 );
3918
3919 sub replace_routing_list_users {
3920     my ($self, $client, $auth, $users) = @_;
3921
3922     return undef unless ref $users eq "ARRAY";
3923
3924     if (grep { ref $_ ne "Fieldmapper::serial::routing_list_user" } @$users) {
3925         return new OpenILS::Event("BAD_PARAMS", "note" => "Only srlu objects");
3926     }
3927
3928     my $e = new_editor("authtoken" => $auth, "xact" => 1);
3929     return $e->die_event unless $e->checkauth;
3930
3931     my %streams_ok = ();
3932     my $pos = 0;
3933
3934     foreach my $user (@$users) {
3935         unless (exists $streams_ok{$user->stream}) {
3936             my $stream = $e->retrieve_serial_stream([
3937                 $user->stream, {
3938                     "flesh" => 1,
3939                     "flesh_fields" => {"sstr" => ["distribution"]}
3940                 }
3941             ]) or return $e->die_event;
3942             $e->allowed(
3943                 "ADMIN_SERIAL_STREAM", $stream->distribution->holding_lib
3944             ) or return $e->die_event;
3945
3946             my $to_delete = $e->search_serial_routing_list_user(
3947                 {"stream" => $user->stream}
3948             ) or return $e->die_event;
3949
3950             $logger->info(
3951                 "Deleting srlu: [" .
3952                 join(", ", map { $_->id; } @$to_delete) .
3953                 "]"
3954             );
3955
3956             foreach (@$to_delete) {
3957                 $e->delete_serial_routing_list_user($_) or
3958                     return $e->die_event;
3959             }
3960
3961             $streams_ok{$user->stream} = 1;
3962         }
3963
3964         next if $user->isdeleted;
3965
3966         $user->clear_id;
3967         $user->pos($pos++);
3968         $e->create_serial_routing_list_user($user) or return $e->die_event;
3969     }
3970
3971     $e->commit or return $e->die_event;
3972     undef;
3973 }
3974
3975 __PACKAGE__->register_method(
3976     "method" => "get_records_with_marc_85x",
3977     "api_name"=>"open-ils.serial.caption_and_pattern.find_legacy_by_bib_record",
3978     "stream" => 1,
3979     "signature" => {
3980         "desc" => "Return the specified BRE itself and/or any related SRE ".
3981             "whenever they have 853-855 tags",
3982         "params" => [
3983             {"desc" => "Authtoken", "type" => "string"},
3984             {"desc" => "bib record ID", "type" => "number"},
3985         ],
3986         "return" => {
3987             "desc" => "objects, either bre or sre", "type" => "object"
3988         }
3989     }
3990 );
3991
3992 sub get_records_with_marc_85x { # specifically, 853-855
3993     my ($self, $client, $auth, $bre_id) = @_;
3994
3995     my $e = new_editor("authtoken" => $auth);
3996     return $e->die_event unless $e->checkauth;
3997
3998     my $bre = $e->search_biblio_record_entry([
3999         {"id" => $bre_id, "deleted" => "f"}, {
4000             "flesh" => 1,
4001             "flesh_fields" => {"bre" => [qw/creator editor owner/]}
4002         }
4003     ]) or return $e->die_event;
4004
4005     return undef unless @$bre;
4006     $bre = $bre->[0];
4007
4008     my $record = MARC::Record->new_from_xml($bre->marc);
4009     $client->respond($bre) if $record->field("85[3-5]");
4010     # XXX Is passing a regex to ->field() an abuse of MARC::Record ?
4011
4012     my $sres = $e->search_serial_record_entry([
4013         {"record" => $bre_id, "deleted" => "f"}, {
4014             "flesh" => 1,
4015             "flesh_fields" => {"sre" => [qw/creator editor owning_lib/]}
4016         }
4017     ]) or return $e->die_event;
4018
4019     $e->disconnect;
4020
4021     foreach my $sre (@$sres) {
4022         $client->respond($sre) if
4023             MARC::Record->new_from_xml($sre->marc)->field("85[3-5]");
4024     }
4025
4026     undef;
4027 }
4028
4029 __PACKAGE__->register_method(
4030     "method" => "create_scaps_from_marcxml",
4031     "api_name" => "open-ils.serial.caption_and_pattern.create_from_records",
4032     "stream" => 1,
4033     "signature" => {
4034         "desc" => "Create caption and pattern objects from 853-855 tags " .
4035             "in MARCXML documents",
4036         "params" => [
4037             {"desc" => "Authtoken", "type" => "string"},
4038             {"desc" => "Subscription ID", "type" => "number"},
4039             {"desc" => "list of MARCXML documents as strings",
4040                 "type" => "array"},
4041         ],
4042         "return" => {
4043             "desc" => "Newly created caption and pattern objects",
4044             "type" => "object", "class" => "scap"
4045         }
4046     }
4047 );
4048
4049 sub create_scaps_from_marcxml {
4050     my ($self, $client, $auth, $sub_id, $docs) = @_;
4051
4052     return undef unless ref $docs eq "ARRAY";
4053
4054     my $e = new_editor("authtoken" => $auth, "xact" => 1);
4055     return $e->die_event unless $e->checkauth;
4056
4057     # Retrieve the subscription just for perm checking (whether we can create
4058     # scaps at the owning lib).
4059     my $sub = $e->retrieve_serial_subscription($sub_id) or return $e->die_event;
4060     return $e->die_event unless
4061         $e->allowed("ADMIN_SERIAL_CAPTION_PATTERN", $sub->owning_lib);
4062
4063     foreach my $record (map { MARC::Record->new_from_xml($_) } @$docs) {
4064         foreach my $field ($record->field("85[3-5]")) {
4065             my $scap = new Fieldmapper::serial::caption_and_pattern;
4066             $scap->subscription($sub_id);
4067             $scap->type($MFHD_NAMES_BY_TAG{$field->tag});
4068             $scap->pattern_code(
4069                 OpenSRF::Utils::JSON->perl2JSON(
4070                     [ $field->indicator(1), $field->indicator(2),
4071                         map { @$_ } $field->subfields ] # flattens nested array
4072                 )
4073             );
4074             $e->create_serial_caption_and_pattern($scap) or
4075                 return $e->die_event;
4076             $client->respond($e->data);
4077         }
4078     }
4079
4080     $e->commit or return $e->die_event;
4081     undef;
4082 }
4083
4084 # All these _clone_foo() functions could possibly have been consolidated into
4085 # one clever function, but it's faster to get things working this way.
4086 sub _clone_subscription {
4087     my ($sub, $bib_id, $e) = @_;
4088
4089     # clone sub itself
4090     my $new_sub = $sub->clone;
4091     $new_sub->record_entry(int $bib_id) if $bib_id;
4092     $new_sub->clear_id;
4093     $new_sub->clear_distributions;
4094     $new_sub->clear_notes;
4095     $new_sub->clear_scaps;
4096
4097     $e->create_serial_subscription($new_sub) or return $e->die_event;
4098
4099     my $new_sub_id = $e->data->id;
4100     # clone dists
4101     foreach my $dist (@{$sub->distributions}) {
4102         my $r = _clone_distribution($dist, $new_sub_id, $e);
4103         return $r if $U->event_code($r);
4104     }
4105
4106     # clone sub notes
4107     foreach my $note (@{$sub->notes}) {
4108         my $r = _clone_subscription_note($note, $new_sub_id, $e);
4109         return $r if $U->event_code($r);
4110     }
4111
4112     # clone scaps
4113     foreach my $scap (@{$sub->scaps}) {
4114         my $r = _clone_caption_and_pattern($scap, $new_sub_id, $e);
4115         return $r if $U->event_code($r);
4116     }
4117
4118     return $new_sub_id;
4119 }
4120
4121 sub _clone_distribution {
4122     my ($dist, $sub_id, $e) = @_;
4123
4124     my $new_dist = $dist->clone;
4125     $new_dist->clear_id;
4126     $new_dist->clear_notes;
4127     $new_dist->clear_streams;
4128     $new_dist->subscription($sub_id);
4129
4130     $e->create_serial_distribution($new_dist) or return $e->die_event;
4131     my $new_dist_id = $e->data->id;
4132
4133     # clone streams
4134     foreach my $stream (@{$dist->streams}) {
4135         my $r = _clone_stream($stream, $new_dist_id, $e);
4136         return $r if $U->event_code($r);
4137     }
4138
4139     # clone distribution notes
4140     foreach my $note (@{$dist->notes}) {
4141         my $r = _clone_distribution_note($note, $new_dist_id, $e);
4142         return $r if $U->event_code($r);
4143     }
4144
4145     return $new_dist_id;
4146 }
4147
4148 sub _clone_subscription_note {
4149     my ($note, $sub_id, $e) = @_;
4150
4151     my $new_note = $note->clone;
4152     $new_note->clear_id;
4153     $new_note->creator($e->requestor->id);
4154     $new_note->create_date("now");
4155     $new_note->subscription($sub_id);
4156
4157     $e->create_serial_subscription_note($new_note) or return $e->die_event;
4158     return $e->data->id;
4159 }
4160
4161 sub _clone_caption_and_pattern {
4162     my ($scap, $sub_id, $e) = @_;
4163
4164     my $new_scap = $scap->clone;
4165     $new_scap->clear_id;
4166     $new_scap->subscription($sub_id);
4167
4168     $e->create_serial_caption_and_pattern($new_scap) or return $e->die_event;
4169     return $e->data->id;
4170 }
4171
4172 sub _clone_distribution_note {
4173     my ($note, $dist_id, $e) = @_;
4174
4175     my $new_note = $note->clone;
4176     $new_note->clear_id;
4177     $new_note->creator($e->requestor->id);
4178     $new_note->create_date("now");
4179     $new_note->distribution($dist_id);
4180
4181     $e->create_serial_distribution_note($new_note) or return $e->die_event;
4182     return $e->data->id;
4183 }
4184
4185 sub _clone_stream {
4186     my ($stream, $dist_id, $e) = @_;
4187
4188     my $new_stream = $stream->clone;
4189     $new_stream->clear_id;
4190     $new_stream->clear_routing_list_users;
4191     $new_stream->distribution($dist_id);
4192
4193     $e->create_serial_stream($new_stream) or return $e->die_event;
4194     my $new_stream_id = $e->data->id;
4195
4196     # clone routing list users
4197     foreach my $user (@{$stream->routing_list_users}) {
4198         my $r = _clone_routing_list_user($user, $new_stream_id, $e);
4199         return $r if $U->event_code($r);
4200     }
4201
4202     return $new_stream_id;
4203 }
4204
4205 sub _clone_routing_list_user {
4206     my ($user, $stream_id, $e) = @_;
4207
4208     my $new_user = $user->clone;
4209     $new_user->clear_id;
4210     $new_user->stream($stream_id);
4211
4212     $e->create_serial_routing_list_user($new_user) or return $e->die_event;
4213     return $e->data->id;
4214 }
4215
4216 __PACKAGE__->register_method(
4217     "method" => "clone_subscription",
4218     "api_name" => "open-ils.serial.subscription.clone",
4219     "signature" => {
4220         "desc" => q{Clone a subscription, including its attending distributions,
4221             streams, captions and patterns, routing list users, distribution
4222             notes and subscription notes. Do not include holdings-specific
4223             things, like issuances, items, units, summaries. Attach the
4224             clone either to the same bib record as the original, or to one
4225             specified by ID.},
4226         "params" => [
4227             {"desc" => "Authtoken", "type" => "string"},
4228             {"desc" => "Subscription ID", "type" => "number"},
4229             {"desc" => "Bib Record ID (optional)", "type" => "number"}
4230         ],
4231         "return" => {
4232             "desc" => "ID of the new subscription", "type" => "number"
4233         }
4234     }
4235 );
4236
4237 sub clone_subscription {
4238     my ($self, $client, $auth, $sub_id, $bib_id) = @_;
4239
4240     my $e = new_editor("authtoken" => $auth, "xact" => 1);
4241     return $e->die_event unless $e->checkauth;
4242
4243     my $sub = $e->retrieve_serial_subscription([
4244         int $sub_id, {
4245             "flesh" => 3,
4246             "flesh_fields" => {
4247                 "ssub" => [qw/distributions notes scaps/],
4248                 "sdist" => [qw/streams notes/],
4249                 "sstr" => ["routing_list_users"]
4250             }
4251         }
4252     ]) or return $e->die_event;
4253
4254     # ADMIN_SERIAL_SUBSCRIPTION will have to be good enough as a
4255     # catch-all permisison for this operation.
4256     return $e->die_event unless
4257         $e->allowed("ADMIN_SERIAL_SUBSCRIPTION", $sub->owning_lib);
4258
4259     my $result = _clone_subscription($sub, $bib_id, $e);
4260
4261     return $e->die_event($result) if $U->event_code($result);
4262
4263     $e->commit or return $e->die_event;
4264     return $result;
4265 }
4266
4267 __PACKAGE__->register_method(
4268     "method" => "summary_test",
4269     "api_name" => "open-ils.serial.summary_test",
4270     "stream" => 1,
4271     "api_level" => 1,
4272     "argc" => 3
4273 );
4274
4275 # This crummy little test method allows quicker reproduction of certain
4276 # failures (e.g. at item receive time) of the holdings summarization code.
4277 # Pass it an authtoken, an array of issuance IDs, and a single sdist ID
4278 sub summary_test {
4279     my ($self, $conn, $authtoken, $iss_id_list, $sdist_id) = @_;
4280
4281     my $e = new_editor(authtoken => $authtoken, xact => 1);
4282     return $e->die_event unless $e->checkauth;
4283     return $e->die_event unless $e->allowed("RECEIVE_SERIAL");
4284
4285     my @issuances;
4286     foreach my $id (@$iss_id_list) {
4287         my $iss = $e->retrieve_serial_issuance($id) or return $e->die_event;
4288         push @issuances, $iss;
4289     }
4290
4291     my $dist = $e->retrieve_serial_distribution($sdist_id) or return $e->die_event;
4292
4293     $conn->respond(_summarize_contents($e, \@issuances, $dist));
4294     $e->rollback;
4295     return;
4296 }
4297
4298 __PACKAGE__->register_method(
4299     "method" => "fetch_pattern_templates",
4300     "api_name" => "open-ils.serial.pattern_template.retrieve.at",
4301     "stream" => 1,
4302     "signature" => {
4303         "desc" => q{Return the set of pattern templates that are
4304             visible to the specified library.},
4305         "params" => [
4306             {"desc" => "Authtoken", "type" => "string"},
4307             {"desc" => "OU ID", "type" => "number"},
4308         ],
4309         return => {
4310             desc => "stream of pattern templates",
4311             type => "object", class => "spt"
4312         }
4313     }
4314 );
4315
4316 sub fetch_pattern_templates {
4317     my ($self, $client, $auth, $org_unit)  = @_;
4318
4319     my $e = new_editor("authtoken" => $auth);
4320     return $e->die_event unless $e->checkauth;
4321
4322     my $patterns = $e->json_query({
4323         from => [ 'serial.pattern_templates_visible_to' => $org_unit ]
4324     });
4325 $logger->info(Dumper($patterns)); use Data::Dumper;
4326
4327     $client->respond($e->retrieve_serial_pattern_template($_->{id}))
4328         foreach (@$patterns);
4329
4330     $e->disconnect;
4331     return undef;
4332 }
4333
4334 1;