]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/serials/services/core.js
LP#1745427: account for change in prediction patterns
[Evergreen.git] / Open-ILS / web / js / ui / default / staff / serials / services / core.js
1 angular.module('egSerialsMod', ['egCoreMod'])
2 .factory('egSerialsCoreSvc',
3        ['egCore','orderByFilter','$q','$filter','$uibModal','ngToast','egConfirmDialog',
4 function(egCore , orderByFilter , $q , $filter , $uibModal , ngToast , egConfirmDialog) {
5     var DAY = 86400000;
6     var service = {
7         bibId : null,
8         subId : null,
9         subTree : [],
10         subList : [],
11         sptList : [],
12         mfhdList : [],
13         potentialPatternList : [],
14         flatMfhdList : [],
15         itemMap : {},
16         itemTree : [],
17         itemList : [],
18         freq_offset : {
19             a : 365 * DAY,
20             b : 62 * DAY,
21             c : 4 * DAY,
22             d : DAY,
23             e : 14 * DAY,
24             f : 186 * DAY,
25             g : 2 * 365 * DAY,
26             h : 3 * 365 * DAY,
27             i : 2 * DAY,
28             j : 10 * DAY,
29             k : DAY,
30             m : 31 * DAY,
31             q : 93 * DAY,
32             s : 14 * DAY,
33             t : 124 * DAY,
34             w : 7 * DAY,
35             x : 0
36         },
37         freq_chrons : {
38             a : ['year'],
39             b : ['year','month'],
40             c : ['year','month'],
41             d : ['year','month','day'],
42             e : ['year','month','day'],
43             f : ['year','month'],
44             g : ['year'],
45             h : ['year','month'],
46             i : ['year','month','day'],
47             j : ['year','month','day'],
48             k : ['year','month','day'],
49             m : ['year','month'],
50             q : ['year','season'],
51             s : ['year','month'],
52             t : ['year','month','day'],
53             w : ['year','month','day'],
54             x : ['year','month','day']
55         },
56         get_chron_part : {
57             year  : function(d) { return d.getFullYear() },
58             season: function(d) { return _loose_season(d) },
59             month : function(d) { return ('00' + (d.getMonth() + 1)).slice(-2) },
60             week  : function(d) { return $filter('date')(d, 'ww') },
61             day   : function(d) { return ('00'+d.getDate()).slice(-2) },
62             hour  : function(d) { return ('00'+d.getHours()).slice(-2) }
63         },
64         item_status_list : [
65             'Expected',
66             'Received',
67             'Claimed',
68             'Bindery',
69             'Bound',
70             'Discarded',
71             'Not Held',
72             'Not Published'
73         ],
74         item_status_i18n : []
75     };
76
77     angular.forEach(service.item_status_list, function(status) {
78         service.item_status_i18n.push({
79             name  : status,
80             label : egCore.strings.SERIALS_ITEM_STATUS[status]
81         });
82     });
83
84     function _loose_season(D) {
85         var m = D.getMonth() + 1;
86         var d = D.getDate();
87
88         if (
89             (m == 1 || m == 2) || (m == 12 && d >= 21) || (m == 3 && d < 20)
90         ) {
91             return 24;  /* MFHD winter */
92         } else if (
93             (m == 4 || m == 5) || (m == 3 && d >= 20) || (m == 6 && d < 21)
94         ) {
95             return 21;  /* spring */
96         } else if (
97             (m == 7 || m == 8) || (m == 6 && d >= 21) || (m == 9 && d < 22)
98         ) {
99             return 22;  /* summer */
100         } else {
101             return 23;  /* autumn */
102         }
103     }
104
105     service.fetch_mfhds = function(bibId, contextOrg) {
106         // TODO filter by contextOrg
107         return egCore.pcrud.search('sre', {
108                 record       : bibId,
109                 deleted      : 'f',
110                 active       : 't'
111             }, {
112                 flesh : 3,
113                 flesh_fields : {
114                     'sre' : ['owning_lib']
115                 }
116             },
117             { atomic : true }
118         ).then(function(list) {
119             service.bibId = bibId;
120             service.mfhdList = list;
121             update_flat_mfhd_list();
122         });
123     }
124
125     service.fetch_patterns_from_bibs_mfhds = function(bibId) {
126         return egCore.net.request(
127             'open-ils.serial',
128             'open-ils.serial.caption_and_pattern.find_legacy_by_bib_record.atomic',
129             egCore.auth.token(),
130             bibId
131         ).then(function(list) {
132             service.potentialPatternList = egCore.idl.toTypedHash(list);
133             angular.forEach(service.potentialPatternList, function(pot) {
134                 var rec = new MARC21.Record({ marcxml : pot.marc });
135                 var pattern_fields = rec.fields.filter(function(f) {
136                     return (f.tag == '853' || f.tag == '854' || f.tag == '855');
137                 });
138                 pot.desc = '';
139                 if (pattern_fields.length > 0) {
140                     // just take the first one
141                     var fld = pattern_fields[0];
142                     pot.desc = fld.tag + ' ' + fld.ind1 + fld.ind2 +
143                                fld.subfields.map(function(sf) { 
144                                  return '$' + sf[0] + sf[1]
145                                }).join('');
146                 }
147             });
148         })
149     }
150
151     // fetch subscription, distributions, streams, captions,
152     // and notes associated with the indicated bib
153     service.fetch = function(bibId, contextOrg) {
154
155         var filter = { record_entry : bibId };
156         if (contextOrg) filter.owning_lib = egCore.org.descendants(contextOrg, true);
157         return egCore.pcrud.search('ssub', filter,
158             {
159                 flesh : 5,
160                 flesh_fields : {
161                     'ssub'  : ['owning_lib','distributions', 'scaps', 'notes'],
162                     'sdist' : [ 'record_entry','holding_lib',
163                                 'receive_call_number',
164                                 'receive_unit_template',
165                                 'bind_call_number',
166                                 'bind_unit_template',
167                                 'streams','notes'],
168                     'sstr'  : ['routing_list_users'],
169                     'srlu'  : ['reader'],
170                     'au'    : ['card','home_ou','mailing_address','billing_address']
171                 }
172             },
173             { atomic : true }
174         ).then(function(list) {
175             service.bibId = bibId;
176             service.subTree = list;
177             update_flat_sdist_sstr_list();
178             return $q.when(list);
179         });
180     }
181
182     // fetch subscription, distributions, streams, captions,
183     // and notes associated with the indicated bib
184     service.fetchLastCallnumber = function(contextOrg) {
185         return egCore.pcrud.search('acn', {
186                 record : service.bibId,
187                 owning_lib : contextOrg,
188                 deleted : 'f'
189             }, { flesh : 1,
190                  flesh_fields : {acn : ['prefix','suffix']},
191                  order_by : [{class:'acn',field:'create_date',direction:'desc'}],
192                  limit : 1
193             }, { atomic : true }
194         ).then(function(list) {
195             return $q.when(list[0]);
196         });
197     }
198
199     service.fetchItemsForSubPaged = function(subId,filter,offset,limit,sort) {
200         return service.fetchItemsForSub(
201             subId,
202             filter,
203             { limit : limit, offset : offset, paging : true },
204             sort
205         );
206     }
207
208     // Creates an inverted tree from item to sub
209     service.fetchItemsForSub = function(subId,filter,options,sort) {
210         var deferred = $q.defer(); // side-effects only, otherwise the grid is wonky
211
212         if (!filter) filter = {};
213         if (!options) options = { limit : 100 }; // only used during full refresh
214
215         if (!subId && service.subId) subId = service.subId;
216         if (!subId) return $q.reject('fetchItemsForSub: no subscription id');
217
218         var sub = service.get_ssub(subId);
219         if (!sub) return $q.reject('fetchItemsForSub: unknown subscription id');
220
221         var streams = [];
222         angular.forEach(sub.distributions(), function(dist) {
223             angular.forEach(
224                 dist.streams().map(
225                     function (stream) { return stream.id() }
226                 ),
227                 function (sid) { streams.push(sid) }
228             );
229         });
230
231         angular.extend(filter, {stream:streams});
232         angular.extend(options, { 
233             order_by : [{class:'sitem',field:'date_expected'}], // best aprox of pub date
234             flesh : 1,
235             flesh_fields : {
236                 sitem : ['notes','issuance','editor','creator','unit','url']
237             }
238         });
239         if (sort) {
240             angular.extend(options, {
241                 order_by : [sort]
242             });
243         }
244
245         egCore.pcrud.search(
246             'sitem', filter, options,
247             { atomic : true }
248         ).then(function(list) {
249             service.subId = subId;
250             if (!options.paging) { // not paged
251                 service.itemTree = list;
252                 service.itemMap = {};
253             } else { // paged
254                 angular.forEach(list, function (item) {
255                     var exists = service.itemTree.filter(function (i) {
256                         return i.id() == item.id()
257                     }).length;
258                     if (!exists) service.itemTree.push(item);
259                 });
260             }
261
262             // map items by stream for faster lookup
263             var tmp = {};
264             angular.forEach(list, function(item) {
265                 if (!tmp[item.stream()]) tmp[item.stream()] = [];
266                 tmp[item.stream()].push(item);
267                 service.itemMap[item.id()] = item;
268             });
269
270             angular.forEach(sub.distributions(), function(dist) {
271                 angular.forEach(dist.streams(), function(stream) {
272                     angular.forEach(tmp[stream.id()], function (item) {
273                         var routing_list = egCore.idl.Clone(stream.routing_list_users());
274                         var st = egCore.idl.Clone(stream,1);
275                         st.routing_list_users(routing_list);
276                         var d = egCore.idl.Clone(dist,1);
277                         var ss = egCore.idl.Clone(sub,1);
278                         ss.distributions([]);
279                         d.subscription(ss);
280                         d.streams([]);
281                         st.distribution(d);
282                         item.stream(st);
283                     });
284                 });
285             });
286
287             var hashList = egCore.idl.toHash(service.itemTree);
288             angular.forEach(hashList, function (item) {
289                 item['issuance.date_published'] = item.issuance.date_published;
290                 item['stream.distribution.holding_lib.name'] = item.stream.distribution.holding_lib.name;
291             });
292
293             // ... then sort it
294             if (sort) {
295                 service.itemList = hashList;
296             } else {
297                 service.itemList = orderByFilter(hashList, ['"issuance.date_published"', '"stream.distribution.holding_lib.name"', '"id"']);
298             }
299             deferred.resolve();
300         });
301
302         return deferred.promise;
303     }
304
305     service.prep_new_holding_code = function (args) {
306
307         var type = args.type;
308         var date = args.date;
309         var prev_iss = args.prev_iss;
310         var curr_iss = args.curr_iss;
311         var adhoc = false;
312         var link = '1.1';
313         var current_values = {};
314
315         var sub = service.get_ssub(service.subId);
316         if (!sub) return args;
317
318         var scap;
319         var pattern_changed = false;
320         if (prev_iss && prev_iss.holding_code()) { // we're predicting
321             var old_link_parts = JSON.parse(prev_iss.holding_code())[3].split('.');
322             var olink = old_link_parts[0];
323             var oseq = parseInt(old_link_parts[1]) + 1;
324             link = [olink,oseq].join('.');
325
326             if (prev_iss.holding_type())
327                 type = prev_iss.holding_type();
328
329             if (prev_iss.caption_and_pattern()) {
330                 var tmp = sub.scaps().filter(function (s) {
331                     return (s.id() == prev_iss.caption_and_pattern() && s.active() == 't');
332                 });
333                 if (angular.isArray(tmp) && tmp[0]) {
334                     scap = tmp[0];
335                 } else {
336                     // pattern associated with last issue must no longer be active
337                     pattern_changed = true;
338                 }
339             }
340
341             date = new Date(prev_iss.date_published());
342         } else if (curr_iss) { // we're editing
343             if (curr_iss.holding_type())
344                 type = curr_iss.holding_type();
345
346             if (curr_iss.caption_and_pattern()) {
347                 var tmp = sub.scaps().filter(function (s) {
348                     return (s.id() == curr_iss.caption_and_pattern());
349                 });
350                 if (angular.isArray(tmp) && tmp[0]) scap = tmp[0];
351             }
352             if (!curr_iss.holding_code()) {
353                 adhoc = true;
354             } else {
355                 var tmp = JSON.parse(curr_iss.holding_code());
356                 for (var i = 2; i < tmp.length; i += 2) {
357                     // we're intentionally being a bit sloppy here, as
358                     // the only subfields we are about in this context
359                     // are the ones that are not repeatable
360                     current_values[tmp[i]] = tmp[i + 1];
361                 }
362             }
363
364             date = new Date(curr_iss.date_published());
365         } else {
366             // starting from scratch, so default the
367             // first publication date to the subscription start date
368             if (!date) date = new Date(sub.start_date());
369         }
370
371         args.date = date;
372
373         if (!scap) {
374             var tmp = sub.scaps().filter(function (s) {
375                 return (s.type() == type && s.active() == 't');
376             });
377             if (angular.isArray(tmp) && tmp[0]) scap = tmp[0];
378         }
379
380         if (!scap) return args;
381
382         var others = [], enums = [], chrons = [], freq = '';
383         var pat = JSON.parse(scap.pattern_code()).slice(4); // just the part we care about
384
385         var freq_index = pat.indexOf('w');
386         if (freq_index > -1) {
387             freq = pat[freq_index + 1];
388             if (prev_iss && !args.pattern_changed) {
389                 date = new Date(
390                     date.getTime() + service.freq_offset[freq]
391                 );
392             }
393         }
394        
395         if (!date) date = new Date();
396
397         for (var i = 0; i < pat.length; i++) {
398             sf = pat[i]; i++;
399             val = pat[i];
400
401             if (sf != 'w') {
402                 var pat_part = {
403                     subfield : sf,
404                     pattern  : val
405                 };
406
407                 var chron_part = String(val).replace(/[)(]+/g,'');
408                 if (sf in current_values) {
409                     pat_part.value = current_values[sf];
410                 } else {
411                     try {
412                         pat_part.value = service.get_chron_part[chron_part](date);
413                     } catch (e) {
414                         // not a chron part
415                         pat_part.value = '';
416                     }
417                 }
418
419                 if (sf.match(/[a-f]/)) {
420                     enums.push(pat_part);
421                 } else if (sf.match(/[i-l]/)) {
422                     chrons.push(pat_part);
423                 } else {
424                     others.push(pat_part);
425                 }
426             }
427         }
428
429         if (enums.length == 0 && chrons.length == 0) {
430             var parts = service.freq_chrons[freq];
431             if (parts.length) {
432                 angular.forEach(parts, function(p, ind) {
433                     var sf = !ind ? 'i' : !--ind ? 'j' : 'k';
434                     chrons.push({
435                         subfield : sf,
436                         value    : service.get_chron_part.year(date)
437                     });
438                 });
439             } else { 
440                 chrons = [
441                     { subfield : 'i', value : service.get_chron_part.year(date)  },
442                     { subfield : 'j', value : service.get_chron_part.month(date) },
443                     { subfield : 'k', value : service.get_chron_part.day(date)  }
444                 ];
445             }
446         }
447
448         return {
449             holding_code : ["4","1","8",link],
450             scap         : scap.id(),
451             type         : type,
452             date         : date,
453             enums        : enums,
454             chrons       : chrons,
455             others       : others,
456             freq         : freq,
457             adhoc        : adhoc,
458             pattern_changed : pattern_changed
459         };
460     }
461
462     service.new_holding_code = function (options) {
463         if (options === undefined) options = {};
464         options.count = options.count || 1;
465         options.label = options.label || '';
466
467         return $uibModal.open({
468             templateUrl: './serials/t_holding_code_dialog',
469             //size: 'lg',
470             //windowClass: 'eg-wide-modal',
471             backdrop: 'static',
472             controller:
473                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
474                 $scope.focusMe = true;
475                 $scope.title = options.title;
476                 $scope.request_count = options.request_count;
477                 $scope.count = options.count;
478                 $scope.label = options.label;
479                 $scope.save_label = options.save_label;
480                 $scope.pubdate = options.date;
481                 $scope.type = options.type || 'basic';
482                 $scope.args = { adhoc : false };
483                 if (options.adhoc) $scope.args.adhoc = true;
484                 $scope.can_change_adhoc = options.can_change_adhoc;
485
486                 function refresh (n,o) {
487                     if (n && o && n !== o) {
488                         $scope.args = service.prep_new_holding_code({
489                             type : $scope.type,
490                             date : $scope.pubdate,
491                             prev_iss : options.prev_iss,
492                             curr_iss : options.curr_iss,
493                         });
494                         if (!options.can_change_adhoc && options.adhoc) $scope.args.adhoc = true;
495
496                         if ($scope.args.type && $scope.type != $scope.args.type)
497                             $scope.type = $scope.args.type;
498                         if ($scope.args.date)
499                             $scope.pubdate = $scope.args.date;
500
501                         delete options.prev_iss; // only use this once
502                         delete options.curr_iss; // only use this once
503                     }
504                 }
505
506                 $scope.$watch('count',function (n) {options.count = n});
507                 $scope.$watch('label',function (n) {options.label = n});
508                 $scope.$watch('type',refresh);
509                 $scope.$watch('pubdate',refresh);
510
511                 $scope.ok = function(args) { $uibModalInstance.close(args) }
512                 $scope.cancel = function () { $uibModalInstance.dismiss() }
513
514                 refresh(1,2); // force data loading
515             }]
516         }).result.then(function (args) {
517             if (args.enums && args.chrons) {
518                 angular.forEach(
519                     args.enums.concat(args.chrons),
520                     function (e) {
521                         args.holding_code.push(e.subfield);
522                         args.holding_code.push(e.value);
523                     }
524                 );
525             }
526             args.count = options.count;
527             args.label = options.label;
528             return $q.when(args);
529         });
530     }
531
532     function update_flat_mfhd_list() {
533         var list = [];
534         angular.forEach(service.mfhdList, function(sre) {
535             var mfhdHash = egCore.idl.toHash(sre);
536             var rec = new MARC21.Record({ marcxml : mfhdHash.marc });
537             var _mfhd = {
538                 'id'                   : mfhdHash.id,
539                 'owning_lib.name'      : mfhdHash.owning_lib.name,
540                 'owning_lib.id'        : mfhdHash.owning_lib.id,
541                 'marc'                 : rec.toBreaker(),
542                 'marc_xml'             : mfhdHash.marc,
543                 'svr'                  : null,
544                 'basic_holdings'       : null,
545                 'index_holdings'       : null,
546                 'supplement_holdings'  : null
547             }
548             list.push(_mfhd);
549             egCore.net.request(
550                 'open-ils.search',
551                 'open-ils.search.serial.record.mfhd.retrieve',
552                 mfhdHash.id
553             ).then(function(svr) {
554                 _mfhd.svr = egCore.idl.toTypedHash(svr);
555                 _mfhd.basic_holdings = _mfhd.svr.basic_holdings.join("; ");
556                 _mfhd.index_holdings = _mfhd.svr.index_holdings.join("; ");
557                 _mfhd.supplement_holdings = _mfhd.svr.supplement_holdings.join("; ");
558             })
559         });
560         service.flatMfhdList.length = 0;
561         angular.extend(service.flatMfhdList, list);
562     }
563
564     // create/update a flat version of the subscription/distribution/stream
565     // tree for feeding to the distribution and stream grid
566     function update_flat_sdist_sstr_list() {
567
568         // flatten the structure...
569         var list = [];
570         angular.forEach(service.subTree, function(ssub) {
571             var ssubHash = egCore.idl.toHash(ssub);
572
573             var _ssub = {
574                 'id'                   : ssubHash.id,
575                 'owning_lib.name'      : ssubHash.owning_lib.name,
576                 'owning_lib.id'        : ssubHash.owning_lib.id,
577                 'start_date'           : ssubHash.start_date,
578                 'end_date'             : ssubHash.end_date,
579                 'expected_date_offset' : ssubHash.expected_date_offset
580             };
581             // insert and escape if we have no distributions
582             if (ssubHash.distributions.length == 0) {
583                 list.push(_ssub);
584                 return;
585             }
586
587             angular.forEach(ssubHash.distributions, function(sdist) {
588                 var _sdist = {};
589                 angular.forEach([
590                     'id',
591                     'summary_method',
592                     'record_entry',
593                     'label',
594                     'display_grouping',
595                     'unit_label_prefix',
596                     'unit_label_suffix',
597                 ], function(fld) {
598                     _sdist['sdist.' + fld] = sdist[fld];
599                 });
600                 _sdist['sdist.holding_lib.name'] = sdist.holding_lib.name;
601                 _sdist['sdist.holding_lib.id'] = sdist.holding_lib.id;
602                 _sdist['sdist.receive_call_number.label'] = 
603                     sdist.receive_call_number ? sdist.receive_call_number.label : null;
604                 _sdist['sdist.receive_unit_template.name'] =
605                     sdist.receive_unit_template ? sdist.receive_unit_template.name : null;
606                 _sdist['sdist.bind_call_number.label'] =
607                     sdist.bind_call_number ? sdist.bind_call_number.label : null;
608                 _sdist['sdist.bind_unit_template.name'] =
609                     sdist.bind_unit_template ? sdist.bind_unit_template.name : null;
610                 // if we have no streams, add to the list and escape
611                 if (sdist.streams.length == 0) {
612                     var row = {};
613                     angular.extend(row, _ssub, _sdist);
614                     list.push(row);
615                     return;
616                 }
617
618                 angular.forEach(sdist.streams, function(sstr) {
619                     var _sstr = {
620                         'sstr.id'                 : sstr.id,
621                         'sstr.routing_label'      : sstr.routing_label,
622                         'sstr.additional_routing' : ((sstr.routing_list_users.length > 0) ? true : false)
623                     };
624                     var row = {};
625                     angular.extend(row, _ssub, _sdist, _sstr);
626                     list.push(row);
627                 });
628             });
629         });
630
631         // ... then sort it
632         service.subList.length = 0;
633         angular.extend(service.subList,
634             orderByFilter(list, ['"owning_lib.name"', '"start_date"', '"end_date"',
635                                  '"holding_lib.name"', '"sdist.id"', '"sstr.id"'])
636         );
637
638         // ... then remove duplication of owning library, distribution library,
639         // and distribution labels
640         var sub_lib = null;
641         var dist_lib = null;
642         var dist_label = null;
643         var index = 0;
644         angular.forEach(service.subList, function(row) {
645             row['index'] = index++;
646             if (sub_lib == row['owning_lib.name']) {
647                 row['owning_lib.name'] = null;
648             } else {
649                 sub_lib = row['owning_lib.name'];
650                 dist_lib = row['sdist.holding_lib.name'];
651                 dist_label = row['sdist.label'];
652                 return;
653             }
654             if (dist_lib == row['sdist.holding_lib.name']) {
655                 row['sdist.holding_lib.name'] = null;
656             } else {
657                 dist_lib = row['sdist.holding_lib.name'];
658             }
659             if (dist_label == row['sdist.label']) {
660                 row['sdist.label'] = null;
661             } else {
662                 dist_label = row['sdist.label'];
663             }
664         });
665     }
666
667     // verify that a subscription ID and bib ID are actually
668     // associated with each other
669     service.verify_subscription_id = function(bibId, ssubId) {
670         var deferred = $q.defer();
671         egCore.pcrud.search('ssub', {
672                 record_entry : bibId,
673                 id           : ssubId
674         }, {}, { atomic : true, idlist : true }
675         ).then(function(list) {
676             if (list.length == 1) {
677                 deferred.resolve(true);
678             } else {
679                 deferred.resolve(false);
680             }
681         });
682         return deferred.promise;
683     }
684
685     service.get_ssub = function(ssubId) {
686         if (!ssubId) return;
687         for (var i = 0; i <= service.subTree.length; i++) {
688             if (service.subTree[i].id() == ssubId) {
689                 return service.subTree[i];
690             }
691         }
692     }
693
694     service.fetch_spt = function() {
695         return egCore.net.request(
696             'open-ils.serial',
697             'open-ils.serial.pattern_template.retrieve.at.atomic',
698             egCore.auth.token(),
699             egCore.auth.user().ws_ou()
700         ).then(function(list) {
701             service.sptList.length = 0;
702             angular.extend(service.sptList, list);
703         });
704     }
705
706     service.fetch_templates = function(org) {
707         return egCore.pcrud.search('act',
708             {owning_lib : egCore.org.fullPath(org, true)},
709             {order_by : { act : 'name' }}, {atomic : true}
710         );
711     };
712
713     service.print_routing_lists = function (bibId, items, check, force, print_rl) {
714         if (!check && !print_rl && !force) return $q.when();
715
716         return egCore.net.request(
717             'open-ils.search',
718             'open-ils.search.biblio.record.mods_slim.retrieve',
719             bibId
720         ).then(function(mvr) {
721
722             var by_issuance = {};
723             angular.forEach(items, function (i) {
724                 if (check && !i._print_routing_list) return;
725                 if (!by_issuance[i.issuance().id()])
726                     by_issuance[i.issuance().id()] = [];
727                 by_issuance[i.issuance().id()].push(i);
728             });
729
730             var issuance_matrix = [];
731             angular.forEach(by_issuance, function (list) {
732                 issuance_matrix.push(list);
733             });
734
735             var deferred = $q.defer();
736             var promise = deferred.promise;
737
738             angular.forEach(issuance_matrix, function(item_list, index) {
739
740                 promise = promise.then(function(){
741                     return $uibModal.open({
742                         templateUrl: './serials/t_print_routing_list',
743                         size: 'lg',
744                         windowClass: 'eg-wide-modal',
745                         backdrop: 'static',
746                         controller:
747                         ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
748                             var all_users = [];
749                             var all_streams = [];
750
751                             angular.forEach(item_list, function(i){
752                                 all_streams.push(i.stream());
753                                 all_users = all_users.concat(i.stream().routing_list_users());
754                             });
755
756                             $scope.xulg = {
757                                 show_print_button: true,
758                                 routing_list_data: {
759                                     streams : all_streams,
760                                     mvr     : mvr,
761                                     issuance: item_list[0].issuance(),
762                                     users   : orderByFilter(all_users, 'pos')
763                                 }
764                             };
765
766                             $scope.url = '/eg/serial/print_routing_list_users?ses=' + egCore.auth.token();
767                             $scope.last = index == issuance_matrix.length - 1 ? true : false; 
768                             $scope.ok = function() { $uibModalInstance.close() }
769                         }]
770                     }).result;
771                 });
772
773             });
774
775             return deferred.resolve();
776         });
777
778     }
779
780     service.set_item_status = function(newStatus, bibId, list, callback) {
781         if (!callback) callback = function () { return $q.when() }
782         if (!list.length) return $q.reject();
783
784         return egConfirmDialog.open(
785             egCore.strings.CONFIRM_CHANGE_ITEMS.status,
786             egCore.strings.CONFIRM_CHANGE_ITEMS_MESSAGE.status,
787             {items : list.length}
788         ).result.then(function () {
789             var promises = [$q.when()];
790             angular.forEach(list, function(item) {
791                 item.status(newStatus);
792                 promises.push(
793                     egCore.net.request(
794                         'open-ils.serial',
795                         'open-ils.serial.item.update',
796                         egCore.auth.token(),
797                         item
798                     ).then(function(res) {
799                         return $q.when();
800                     })
801                 );
802             });
803             $q.all(promises).then(function() {
804                 callback();
805             });
806         });
807     }
808     
809     service.process_items = function (mode, bibId, list, do_barcode, bind, print_rl, callback) {
810         if (!callback) callback = function () { return $q.when() }
811         if (!list.length) return $q.reject();
812
813         // deal with locations and circ mods for *NEW* units
814         var copy_locations = {};
815         var circ_mods = {};
816
817         // deal with barcodes and call numbers for *NEW* units
818         var barcodes = {};
819         var call_numbers = {};
820         var call_numbers_by_siss_and_sdist = {};
821
822         var deferred = $q.defer();
823         var current_promise = deferred.promise;
824         var last_promise;
825
826         var sitem_alerts = [];
827         var sdist_alerts = [];
828         var ssub_alerts = list[0].stream().distribution().subscription().notes().filter(function(n){
829             return n.alert() == 't';
830         })
831
832         var dist_seen = {};
833         angular.forEach(list, function(i) {
834             sitem_alerts = sitem_alerts.concat(
835                 i.notes().filter(function(n){
836                     return n.alert() == 't';
837                 })
838             );
839             var sdist = '_'+i.stream().distribution().id();
840             if (!dist_seen[sdist]) {
841                 dist_seen[sdist] = 1;
842                 sdist_alerts = sdist_alerts.concat(
843                     i.stream().distribution().notes().filter(function(n){
844                         return n.alert() == 't';
845                     })
846                 );
847             }
848         });
849
850         if (do_barcode || bind) {
851
852             last_promise = current_promise.then(function(){ return $uibModal.open({
853                 templateUrl: './serials/t_batch_receive',
854                 size: 'lg',
855                 windowClass: 'eg-wide-modal',
856                 backdrop: 'static',
857                 controller:
858                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
859
860                     $scope.print_routing_lists = print_rl;
861                     $scope.barcode_items = do_barcode;
862                     $scope.force_bind = bind;
863                     $scope.bind = bind;
864                     $scope.items = list;
865                     $scope.ssub_alerts = ssub_alerts;
866                     $scope.sdist_alerts = sdist_alerts;
867                     $scope.sitem_alerts = sitem_alerts;
868                     $scope.acn_list = [];
869                     $scope.acnp_labels = [];
870                     $scope.acns_labels = [];
871                     $scope.acpl_list = [];
872
873                     $scope.cannot_print = function (index) {
874                         return $scope.items[index].stream().routing_list_users().length == 0 || ($scope.bind && index > 0);
875                     }
876
877                     $scope.bind_or_none = function (index) {
878                         return !$scope.barcode_items || ($scope.bind && index > 0);
879                     }
880
881                     $scope.focus_next_barcode = function (index) {
882                         index++;
883                         $('#item_barcode_'+index).focus().select();
884                     }
885
886                     $scope.fullCNLabel = function (cn) {
887                         var label = [cn.prefix.label,cn.label,cn.suffix.label].join(' ');
888                         return label;
889                     }
890
891                     $scope.apply_template_overrides = function (e) {
892                         if ($scope.selected_call_number) {
893                             angular.forEach($scope.items, function (i) {
894                                 i._call_number = $scope.selected_call_number.label;
895                                 i._cn_prefix = $scope.selected_call_number.prefix.label;
896                                 i._cn_suffix = $scope.selected_call_number.suffix.label;
897                             });
898                         }
899                         if ($scope.selected_circ_mod) {
900                             angular.forEach($scope.items, function (i) {
901                                 i._circ_mod = $scope.selected_circ_mod;
902                             });
903                         }
904                         if ($scope.selected_copy_location) {
905                             angular.forEach($scope.items, function (i) {
906                                 i._copy_location = $scope.selected_copy_location;
907                             });
908                         }
909                     }
910
911                     $scope.ok = function(items) { $uibModalInstance.close(items) }
912                     $scope.cancel = function () { $uibModalInstance.dismiss() }
913
914                     var dist_libs = {};
915                     var pile_o_promises = [$q.when()];
916
917                     // let's gather what we need...
918                     angular.forEach(list, function (i, index) {
919                         var dlib = i.stream().distribution().holding_lib().id();
920                         dist_libs[dlib] = egCore.org.fullPath(dlib, true);
921                         if (i.unit()) {
922                             i._barcode = i.unit().barcode();
923                             pile_o_promises.push(
924                                 egCore.pcrud.retrieve(
925                                     'acn', i.unit().call_number(),
926                                     {flesh : 1, flesh_fields : {acn : ['prefix','suffix']}}
927                                 ).then(function(cn){
928                                     if (cn.deleted() == 'f') {
929                                         i._call_number = cn.label();
930                                         i._cn_prefix = cn.prefix().label();
931                                         i._cn_suffix = cn.suffix().label();
932                                     }
933                                 })
934                             );
935                         } else {
936                             if (i.stream().distribution()[mode + '_call_number']() && 
937                                 i.stream().distribution()[mode + '_call_number']().deleted() == 'f'
938                             ) {
939                                 i._call_number = i.stream().distribution()[mode + '_call_number']().label();
940                             } else {
941                                 pile_o_promises.push(
942                                     service.fetchLastCallnumber(
943                                         i.stream().distribution().holding_lib().id()
944                                     ).then(function(cn){
945                                         if (cn) {
946                                             i._call_number = cn.label();
947                                             i._cn_prefix = cn.prefix().label();
948                                             i._cn_suffix = cn.suffix().label();
949                                         }
950                                     })
951                                 );
952                             }
953                         }
954
955                         if (i.stream().distribution()[mode + '_unit_template']()) {
956                             i._copy_location = i.stream().distribution()[mode + '_unit_template']().location();
957                             i._circ_mod = i.stream().distribution()[mode + '_unit_template']().circ_modifier();
958                         }
959
960                         if ($scope.print_routing_lists && !$scope.cannot_print(index))
961                             i._print_routing_list = true;
962
963                         i._receive = true;
964                     });
965
966                     // build unique list of orgs from distribution.holding_lib fullPaths
967                     var dist_lib_list = [];
968                     angular.forEach(dist_libs, function (l) {
969                         dist_lib_list = dist_lib_list.concat(l);
970                     });
971                     dist_lib_list = dist_lib_list.filter(function(v,i,s){
972                         return s.indexOf(v) == i;
973                     });
974
975                     // Copy locations only come from the workstation location, same as XUL
976                     pile_o_promises.push(egCore.pcrud.search(
977                         'acpl',
978                         {owning_lib : egCore.org.fullPath(egCore.auth.user().ws_ou(), true)},
979                         {},{ atomic : true }
980                     ).then(function (list) {
981                         $scope.acpl_list = list.map(function(i){return egCore.idl.toHash(i)});
982                         return $q.when();
983                     }));
984
985                     // Call numbers, however, come from anywhere the distributions might live
986                     pile_o_promises.push(egCore.pcrud.search(
987                         'acn',
988                         {deleted : 'f', record : bibId, owning_lib : dist_lib_list},
989                         {flesh : 1, flesh_fields : {acn : ['prefix','suffix']}},{ atomic : true }
990                     ).then(function (list) {
991                         $scope.acn_list = list.map(function(i){return egCore.idl.toHash(i)});
992                         return $q.when();
993                     }));
994
995                     // Likewise for prefix and suffix, for combo box
996                     angular.forEach(['acnp','acns'], function (cl) {
997                         pile_o_promises.push(egCore.pcrud.search(
998                             cl,
999                             {owning_lib : dist_lib_list},
1000                             {},{ atomic : true }
1001                         ).then(function (list) {
1002                             $scope[cl+'_labels'] = list.map(function(i){return i.label()});
1003                             return $q.when();
1004                         }));
1005                     });
1006
1007                     pile_o_promises.push(egCore.pcrud.retrieveAll(
1008                         'ccm', {}, { atomic : true }
1009                     ).then(function (list) {
1010                         $scope.ccm_list = list.map(function(i){return egCore.idl.toHash(i)});
1011                         return $q.when();
1012                     }));
1013
1014                     $q.all(pile_o_promises).then(function() {
1015                         console.log('receive data collected');
1016                     });
1017
1018                     $scope.$watch('barcode_items', function (n,o) {
1019                         if (n === undefined || n == o) return;
1020                         do_barcode = n;
1021                     });
1022
1023                     $scope.$watch('bind', function (n,o) {
1024                         if (n === undefined || n == o) return;
1025                         bind = n;
1026                         if (bind) {
1027                             angular.forEach($scope.items, function (i,index) {
1028                                 if (index > 0) i._print_routing_list = false;
1029                             });
1030                         }
1031                     });
1032                         
1033                     $scope.$watch('auto_barcodes', function (n,o) {
1034                         if (n === undefined || n == o) return;
1035
1036                         var bc = '@@AUTO';
1037                         if (!n) bc = '';
1038
1039                         angular.forEach($scope.items, function (i) {
1040                             if (!i.stream().distribution().receive_unit_template()) return;
1041                             var _barcode = i._barcode;
1042                             i._barcode = bc || i._old_barcode;
1043                             i._old_barcode = _barcode;
1044                         });
1045                     });
1046
1047                     $scope.$watch('print_routing_lists', function (n,o) {
1048                         if (n === undefined || n == o) return;
1049
1050                         angular.forEach($scope.items, function(i, index) {
1051                             if (!$scope.cannot_print(index)) {
1052                                 i._print_routing_list = n;
1053                             } else {
1054                                 i._print_routing_list = false;
1055                             }
1056                         });
1057                     });
1058                 }]
1059             }).result});
1060         } else {
1061             last_promise = current_promise.then(function(){ return $uibModal.open({
1062                 templateUrl: './serials/t_receive_alerts',
1063                 backdrop: 'static',
1064                 controller:
1065                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
1066                     $scope.title = egCore.strings.CONFIRM_CHANGE_ITEMS[mode];
1067                     $scope.items = list.length;
1068                     $scope.list = list;
1069                     $scope.mode = mode;
1070                     $scope.ssub_alerts = ssub_alerts;
1071                     $scope.sdist_alerts = sdist_alerts;
1072                     $scope.sitem_alerts = sitem_alerts;
1073
1074                     $scope.ok = function(items) { $uibModalInstance.close(items) }
1075                     $scope.cancel = function () { $uibModalInstance.dismiss() }
1076                 }]
1077             }).result.then(
1078                 function(items) {
1079                     angular.forEach(list, function (i, index) {
1080                         i._receive = true;
1081                     });
1082                     return $q.when(list);
1083                 })
1084             });
1085         }
1086
1087         last_promise.then(function (items) {
1088
1089             var method;
1090             if (mode == 'receive') {
1091                 method = 'open-ils.serial.receive_items';
1092                 items = items.filter(function(i){return i._receive});
1093             } else if ( mode == 'bind') {
1094                 method = 'open-ils.serial.bind_items';
1095                 items = items.filter(function(i){return i._receive});
1096             } else if ( mode == 'reset') {
1097                 method = 'open-ils.serial.reset_items';
1098             } 
1099
1100             if (!items.length) return $q.reject();
1101
1102             var donor_unit_ids = {};
1103             angular.forEach(items, function(i, index) {
1104                 if (i.unit()) donor_unit_ids[i.unit().id()] = 1;
1105                 if (do_barcode) i.unit(-1);
1106                 if (bind) i.unit(-2);
1107                 copy_locations[i.id()] = i._copy_location;
1108                 circ_mods[i.id()] = i._circ_mod;
1109                 call_numbers[i.id()] = [i._cn_prefix, i._call_number, i._cn_suffix] || 'DEFAULT';
1110                 barcodes[i.id()] = i._barcode || '@@AUTO';
1111                 if (bind && index > 0) barcodes[i.id()] = items[0]._barcode;
1112             });
1113
1114             return egCore.net.request(
1115                 'open-ils.serial', method,
1116                 egCore.auth.token(), items, barcodes, call_numbers, donor_unit_ids,
1117                     {circ_mods:circ_mods, copy_locations : copy_locations}
1118             ).then(
1119                 function(resp) {
1120                     var evt = egCore.evt.parse(resp);
1121                     if (evt) {
1122                         ngToast.danger(egCore.strings.SERIALS_ISSUANCE_FAIL_SAVE);
1123                     } else {
1124                         ngToast.success(egCore.strings.SERIALS_ISSUANCE_SUCCESS_SAVE);
1125                         return service.print_routing_lists(bibId, items, do_barcode || bind, false, print_rl)
1126                             .finally(callback);
1127                     }
1128                 },
1129                 function(resp) {
1130                     ngToast.danger(egCore.strings.SERIALS_ISSUANCE_FAIL_SAVE);
1131                 }
1132             );
1133         });
1134
1135         return deferred.resolve();
1136     }
1137
1138     service.add_issuances = function (mySsubId) {
1139         if (!mySsubId && service.subId) mySsubId = service.subId;
1140         if (!mySsubId) return $q.reject('fetchItemsForSub: no subscription id');
1141
1142         var sub = service.get_ssub(mySsubId);
1143         if (!sub) return $q.reject('fetchItemsForSub: unknown subscription id');
1144
1145         var streams = [];
1146         angular.forEach(sub.distributions(), function(dist) {
1147             angular.forEach(
1148                 dist.streams().map(
1149                     function (stream) { return stream.id() }
1150                 ),
1151                 function (sid) { streams.push(sid) }
1152             );
1153         });
1154
1155         var options = { 
1156             order_by : [{class:'sitem',field:'date_expected',direction:'desc'}], // best aprox of pub date
1157             limit : 1,
1158             flesh : 1,
1159             flesh_fields : { sitem : ['issuance'] }
1160         };
1161
1162         return egCore.pcrud.search(
1163             'sitem', {stream:streams},
1164             {   order_by : [{class:'sitem',field:'date_expected',direction:'desc'}], // best aprox of pub date
1165                 limit : 1,
1166                 flesh : 1,
1167                 flesh_fields : { sitem : ['issuance'] }
1168             },
1169             { atomic : true }
1170         ).then(function(list) {
1171             var lastItem = list[0];
1172     
1173             if (lastItem) lastItem = lastItem.issuance();
1174     
1175             return service.new_holding_code({
1176                 title : egCore.strings.SERIALS_ISSUANCE_PREDICT,
1177                 request_count : true,
1178                 prev_iss : lastItem,
1179                 allow_adhoc : false
1180             }).then(function(hc) {
1181     
1182                 var base_iss;
1183                 var include_base_iss = 0;
1184                 if (!lastItem || hc.pattern_changed) {
1185                     include_base_iss = 1;
1186                     base_iss = new egCore.idl.siss();
1187                     base_iss.creator( egCore.auth.user().id() );
1188                     base_iss.editor( egCore.auth.user().id() );
1189                     base_iss.date_published( hc.date.toISOString() );
1190                     base_iss.subscription( mySsubId );
1191                     base_iss.caption_and_pattern( hc.scap );
1192                     base_iss.holding_code( JSON.stringify(hc.holding_code) );
1193                     base_iss.holding_type( hc.type );
1194                 }
1195
1196                 // if we're predicting without a preexisting holding, reduce the count
1197                 if (!lastItem) hc.count--;
1198     
1199                 return egCore.net.request(
1200                     'open-ils.serial',
1201                     'open-ils.serial.make_predictions',
1202                     egCore.auth.token(),
1203                     { ssub_id : mySsubId,
1204                       include_base_issuance : include_base_iss,
1205                       num_to_predict : hc.count,
1206                       base_issuance : base_iss || lastItem
1207                     }
1208                 ).then(
1209                     function(resp) {
1210                         var evt = egCore.evt.parse(resp);
1211                         if (evt) {
1212                             ngToast.danger(egCore.strings.SERIALS_ISSUANCE_FAIL_SAVE);
1213                         } else {
1214                             ngToast.success(egCore.strings.SERIALS_ISSUANCE_SUCCESS_SAVE);
1215                         }
1216                     },
1217                     function(resp) {
1218                         ngToast.danger(egCore.strings.SERIALS_ISSUANCE_FAIL_SAVE);
1219                     }
1220                 );
1221             });
1222         });
1223     }
1224
1225     return service;
1226 }]);
1227