]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/staff/serials/services/core.js
Docs: incorporating offline circ docs
[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         if (prev_iss && prev_iss.holding_code()) { // we're predicting
320             var old_link_parts = JSON.parse(prev_iss.holding_code())[3].split('.');
321             var olink = old_link_parts[0];
322             var oseq = parseInt(old_link_parts[1]) + 1;
323             link = [olink,oseq].join('.');
324
325             if (prev_iss.holding_type())
326                 type = prev_iss.holding_type();
327
328             if (prev_iss.caption_and_pattern()) {
329                 var tmp = sub.scaps().filter(function (s) {
330                     return (s.id() == prev_iss.caption_and_pattern());
331                 });
332                 if (angular.isArray(tmp) && tmp[0]) scap = tmp[0];
333             }
334
335             date = new Date(prev_iss.date_published());
336         } else if (curr_iss) { // we're editing
337             if (curr_iss.holding_type())
338                 type = curr_iss.holding_type();
339
340             if (curr_iss.caption_and_pattern()) {
341                 var tmp = sub.scaps().filter(function (s) {
342                     return (s.id() == curr_iss.caption_and_pattern());
343                 });
344                 if (angular.isArray(tmp) && tmp[0]) scap = tmp[0];
345             }
346             if (!curr_iss.holding_code()) {
347                 adhoc = true;
348             } else {
349                 var tmp = JSON.parse(curr_iss.holding_code());
350                 for (var i = 2; i < tmp.length; i += 2) {
351                     // we're intentionally being a bit sloppy here, as
352                     // the only subfields we are about in this context
353                     // are the ones that are not repeatable
354                     current_values[tmp[i]] = tmp[i + 1];
355                 }
356             }
357
358             date = new Date(curr_iss.date_published());
359         } else {
360             // starting from scratch, so default the
361             // first publication date to the subscription start date
362             if (!date) date = new Date(sub.start_date());
363         }
364
365         args.date = date;
366
367         if (!scap) {
368             var tmp = sub.scaps().filter(function (s) {
369                 return (s.type() == type && s.active() == 't');
370             });
371             if (angular.isArray(tmp) && tmp[0]) scap = tmp[0];
372         }
373
374         if (!scap) return args;
375
376         var others = [], enums = [], chrons = [], freq = '';
377         var pat = JSON.parse(scap.pattern_code()).slice(4); // just the part we care about
378
379         var freq_index = pat.indexOf('w');
380         if (freq_index > -1) {
381             freq = pat[freq_index + 1];
382             if (prev_iss) {
383                 date = new Date(
384                     date.getTime() + service.freq_offset[freq]
385                 );
386             }
387         }
388        
389         if (!date) date = new Date();
390
391         for (var i = 0; i < pat.length; i++) {
392             sf = pat[i]; i++;
393             val = pat[i];
394
395             if (sf != 'w') {
396                 var pat_part = {
397                     subfield : sf,
398                     pattern  : val
399                 };
400
401                 var chron_part = String(val).replace(/[)(]+/g,'');
402                 if (sf in current_values) {
403                     pat_part.value = current_values[sf];
404                 } else {
405                     try {
406                         pat_part.value = service.get_chron_part[chron_part](date);
407                     } catch (e) {
408                         // not a chron part
409                         pat_part.value = '';
410                     }
411                 }
412
413                 if (sf.match(/[a-f]/)) {
414                     enums.push(pat_part);
415                 } else if (sf.match(/[i-l]/)) {
416                     chrons.push(pat_part);
417                 } else {
418                     others.push(pat_part);
419                 }
420             }
421         }
422
423         if (enums.length == 0 && chrons.length == 0) {
424             var parts = service.freq_chrons[freq];
425             if (parts.length) {
426                 angular.forEach(parts, function(p, ind) {
427                     var sf = !ind ? 'i' : !--ind ? 'j' : 'k';
428                     chrons.push({
429                         subfield : sf,
430                         value    : service.get_chron_part.year(date)
431                     });
432                 });
433             } else { 
434                 chrons = [
435                     { subfield : 'i', value : service.get_chron_part.year(date)  },
436                     { subfield : 'j', value : service.get_chron_part.month(date) },
437                     { subfield : 'k', value : service.get_chron_part.day(date)  }
438                 ];
439             }
440         }
441
442         return {
443             holding_code : ["4","1","8",link],
444             scap         : scap.id(),
445             type         : type,
446             date         : date,
447             enums        : enums,
448             chrons       : chrons,
449             others       : others,
450             freq         : freq,
451             adhoc        : adhoc
452         };
453     }
454
455     service.new_holding_code = function (options) {
456         if (options === undefined) options = {};
457         options.count = options.count || 1;
458         options.label = options.label || '';
459
460         return $uibModal.open({
461             templateUrl: './serials/t_holding_code_dialog',
462             //size: 'lg',
463             //windowClass: 'eg-wide-modal',
464             backdrop: 'static',
465             controller:
466                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
467                 $scope.focusMe = true;
468                 $scope.title = options.title;
469                 $scope.request_count = options.request_count;
470                 $scope.count = options.count;
471                 $scope.label = options.label;
472                 $scope.save_label = options.save_label;
473                 $scope.pubdate = options.date;
474                 $scope.type = options.type || 'basic';
475                 $scope.args = { adhoc : false };
476                 if (options.adhoc) $scope.args.adhoc = true;
477                 $scope.can_change_adhoc = options.can_change_adhoc;
478
479                 function refresh (n,o) {
480                     if (n && o && n !== o) {
481                         $scope.args = service.prep_new_holding_code({
482                             type : $scope.type,
483                             date : $scope.pubdate,
484                             prev_iss : options.prev_iss,
485                             curr_iss : options.curr_iss,
486                         });
487                         if (!options.can_change_adhoc && options.adhoc) $scope.args.adhoc = true;
488
489                         if ($scope.args.type && $scope.type != $scope.args.type)
490                             $scope.type = $scope.args.type;
491                         if ($scope.args.date)
492                             $scope.pubdate = $scope.args.date;
493
494                         delete options.prev_iss; // only use this once
495                         delete options.curr_iss; // only use this once
496                     }
497                 }
498
499                 $scope.$watch('count',function (n) {options.count = n});
500                 $scope.$watch('label',function (n) {options.label = n});
501                 $scope.$watch('type',refresh);
502                 $scope.$watch('pubdate',refresh);
503
504                 $scope.ok = function(args) { $uibModalInstance.close(args) }
505                 $scope.cancel = function () { $uibModalInstance.dismiss() }
506
507                 refresh(1,2); // force data loading
508             }]
509         }).result.then(function (args) {
510             if (args.enums && args.chrons) {
511                 angular.forEach(
512                     args.enums.concat(args.chrons),
513                     function (e) {
514                         args.holding_code.push(e.subfield);
515                         args.holding_code.push(e.value);
516                     }
517                 );
518             }
519             args.count = options.count;
520             args.label = options.label;
521             return $q.when(args);
522         });
523     }
524
525     function update_flat_mfhd_list() {
526         var list = [];
527         angular.forEach(service.mfhdList, function(sre) {
528             var mfhdHash = egCore.idl.toHash(sre);
529             var rec = new MARC21.Record({ marcxml : mfhdHash.marc });
530             var _mfhd = {
531                 'id'                   : mfhdHash.id,
532                 'owning_lib.name'      : mfhdHash.owning_lib.name,
533                 'owning_lib.id'        : mfhdHash.owning_lib.id,
534                 'marc'                 : rec.toBreaker(),
535                 'marc_xml'             : mfhdHash.marc,
536                 'svr'                  : null,
537                 'basic_holdings'       : null,
538                 'index_holdings'       : null,
539                 'supplement_holdings'  : null
540             }
541             list.push(_mfhd);
542             egCore.net.request(
543                 'open-ils.search',
544                 'open-ils.search.serial.record.mfhd.retrieve',
545                 mfhdHash.id
546             ).then(function(svr) {
547                 _mfhd.svr = egCore.idl.toTypedHash(svr);
548                 _mfhd.basic_holdings = _mfhd.svr.basic_holdings.join("; ");
549                 _mfhd.index_holdings = _mfhd.svr.index_holdings.join("; ");
550                 _mfhd.supplement_holdings = _mfhd.svr.supplement_holdings.join("; ");
551             })
552         });
553         service.flatMfhdList.length = 0;
554         angular.extend(service.flatMfhdList, list);
555     }
556
557     // create/update a flat version of the subscription/distribution/stream
558     // tree for feeding to the distribution and stream grid
559     function update_flat_sdist_sstr_list() {
560
561         // flatten the structure...
562         var list = [];
563         angular.forEach(service.subTree, function(ssub) {
564             var ssubHash = egCore.idl.toHash(ssub);
565
566             var _ssub = {
567                 'id'                   : ssubHash.id,
568                 'owning_lib.name'      : ssubHash.owning_lib.name,
569                 'owning_lib.id'        : ssubHash.owning_lib.id,
570                 'start_date'           : ssubHash.start_date,
571                 'end_date'             : ssubHash.end_date,
572                 'expected_date_offset' : ssubHash.expected_date_offset
573             };
574             // insert and escape if we have no distributions
575             if (ssubHash.distributions.length == 0) {
576                 list.push(_ssub);
577                 return;
578             }
579
580             angular.forEach(ssubHash.distributions, function(sdist) {
581                 var _sdist = {};
582                 angular.forEach([
583                     'id',
584                     'summary_method',
585                     'record_entry',
586                     'label',
587                     'display_grouping',
588                     'unit_label_prefix',
589                     'unit_label_suffix',
590                 ], function(fld) {
591                     _sdist['sdist.' + fld] = sdist[fld];
592                 });
593                 _sdist['sdist.holding_lib.name'] = sdist.holding_lib.name;
594                 _sdist['sdist.holding_lib.id'] = sdist.holding_lib.id;
595                 _sdist['sdist.receive_call_number.label'] = 
596                     sdist.receive_call_number ? sdist.receive_call_number.label : null;
597                 _sdist['sdist.receive_unit_template.name'] =
598                     sdist.receive_unit_template ? sdist.receive_unit_template.name : null;
599                 _sdist['sdist.bind_call_number.label'] =
600                     sdist.bind_call_number ? sdist.bind_call_number.label : null;
601                 _sdist['sdist.bind_unit_template.name'] =
602                     sdist.bind_unit_template ? sdist.bind_unit_template.name : null;
603                 // if we have no streams, add to the list and escape
604                 if (sdist.streams.length == 0) {
605                     var row = {};
606                     angular.extend(row, _ssub, _sdist);
607                     list.push(row);
608                     return;
609                 }
610
611                 angular.forEach(sdist.streams, function(sstr) {
612                     var _sstr = {
613                         'sstr.id'                 : sstr.id,
614                         'sstr.routing_label'      : sstr.routing_label,
615                         'sstr.additional_routing' : ((sstr.routing_list_users.length > 0) ? true : false)
616                     };
617                     var row = {};
618                     angular.extend(row, _ssub, _sdist, _sstr);
619                     list.push(row);
620                 });
621             });
622         });
623
624         // ... then sort it
625         service.subList.length = 0;
626         angular.extend(service.subList,
627             orderByFilter(list, ['"owning_lib.name"', '"start_date"', '"end_date"',
628                                  '"holding_lib.name"', '"sdist.id"', '"sstr.id"'])
629         );
630
631         // ... then remove duplication of owning library, distribution library,
632         // and distribution labels
633         var sub_lib = null;
634         var dist_lib = null;
635         var dist_label = null;
636         var index = 0;
637         angular.forEach(service.subList, function(row) {
638             row['index'] = index++;
639             if (sub_lib == row['owning_lib.name']) {
640                 row['owning_lib.name'] = null;
641             } else {
642                 sub_lib = row['owning_lib.name'];
643                 dist_lib = row['sdist.holding_lib.name'];
644                 dist_label = row['sdist.label'];
645                 return;
646             }
647             if (dist_lib == row['sdist.holding_lib.name']) {
648                 row['sdist.holding_lib.name'] = null;
649             } else {
650                 dist_lib = row['sdist.holding_lib.name'];
651             }
652             if (dist_label == row['sdist.label']) {
653                 row['sdist.label'] = null;
654             } else {
655                 dist_label = row['sdist.label'];
656             }
657         });
658     }
659
660     // verify that a subscription ID and bib ID are actually
661     // associated with each other
662     service.verify_subscription_id = function(bibId, ssubId) {
663         var deferred = $q.defer();
664         egCore.pcrud.search('ssub', {
665                 record_entry : bibId,
666                 id           : ssubId
667         }, {}, { atomic : true, idlist : true }
668         ).then(function(list) {
669             if (list.length == 1) {
670                 deferred.resolve(true);
671             } else {
672                 deferred.resolve(false);
673             }
674         });
675         return deferred.promise;
676     }
677
678     service.get_ssub = function(ssubId) {
679         if (!ssubId) return;
680         for (var i = 0; i <= service.subTree.length; i++) {
681             if (service.subTree[i].id() == ssubId) {
682                 return service.subTree[i];
683             }
684         }
685     }
686
687     service.fetch_spt = function() {
688         return egCore.net.request(
689             'open-ils.serial',
690             'open-ils.serial.pattern_template.retrieve.at.atomic',
691             egCore.auth.token(),
692             egCore.auth.user().ws_ou()
693         ).then(function(list) {
694             service.sptList.length = 0;
695             angular.extend(service.sptList, list);
696         });
697     }
698
699     service.fetch_templates = function(org) {
700         return egCore.pcrud.search('act',
701             {owning_lib : egCore.org.fullPath(org, true)},
702             {order_by : { act : 'name' }}, {atomic : true}
703         );
704     };
705
706     service.print_routing_lists = function (bibId, items, check, force, print_rl) {
707         if (!check && !print_rl && !force) return $q.when();
708
709         return egCore.net.request(
710             'open-ils.search',
711             'open-ils.search.biblio.record.mods_slim.retrieve',
712             bibId
713         ).then(function(mvr) {
714
715             var by_issuance = {};
716             angular.forEach(items, function (i) {
717                 if (check && !i._print_routing_list) return;
718                 if (!by_issuance[i.issuance().id()])
719                     by_issuance[i.issuance().id()] = [];
720                 by_issuance[i.issuance().id()].push(i);
721             });
722
723             var issuance_matrix = [];
724             angular.forEach(by_issuance, function (list) {
725                 issuance_matrix.push(list);
726             });
727
728             var deferred = $q.defer();
729             var promise = deferred.promise;
730
731             angular.forEach(issuance_matrix, function(item_list, index) {
732
733                 promise = promise.then(function(){
734                     return $uibModal.open({
735                         templateUrl: './serials/t_print_routing_list',
736                         size: 'lg',
737                         windowClass: 'eg-wide-modal',
738                         backdrop: 'static',
739                         controller:
740                         ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
741                             var all_users = [];
742                             var all_streams = [];
743
744                             angular.forEach(item_list, function(i){
745                                 all_streams.push(i.stream());
746                                 all_users = all_users.concat(i.stream().routing_list_users());
747                             });
748
749                             $scope.xulg = {
750                                 show_print_button: true,
751                                 routing_list_data: {
752                                     streams : all_streams,
753                                     mvr     : mvr,
754                                     issuance: item_list[0].issuance(),
755                                     users   : orderByFilter(all_users, 'pos')
756                                 }
757                             };
758
759                             $scope.url = '/eg/serial/print_routing_list_users?ses=' + egCore.auth.token();
760                             $scope.last = index == issuance_matrix.length - 1 ? true : false; 
761                             $scope.ok = function() { $uibModalInstance.close() }
762                         }]
763                     }).result;
764                 });
765
766             });
767
768             return deferred.resolve();
769         });
770
771     }
772
773     service.set_item_status = function(newStatus, bibId, list, callback) {
774         if (!callback) callback = function () { return $q.when() }
775         if (!list.length) return $q.reject();
776
777         return egConfirmDialog.open(
778             egCore.strings.CONFIRM_CHANGE_ITEMS.status,
779             egCore.strings.CONFIRM_CHANGE_ITEMS_MESSAGE.status,
780             {items : list.length}
781         ).result.then(function () {
782             var promises = [$q.when()];
783             angular.forEach(list, function(item) {
784                 item.status(newStatus);
785                 promises.push(
786                     egCore.net.request(
787                         'open-ils.serial',
788                         'open-ils.serial.item.update',
789                         egCore.auth.token(),
790                         item
791                     ).then(function(res) {
792                         return $q.when();
793                     })
794                 );
795             });
796             $q.all(promises).then(function() {
797                 callback();
798             });
799         });
800     }
801     
802     service.process_items = function (mode, bibId, list, do_barcode, bind, print_rl, callback) {
803         if (!callback) callback = function () { return $q.when() }
804         if (!list.length) return $q.reject();
805
806         // deal with locations and circ mods for *NEW* units
807         var copy_locations = {};
808         var circ_mods = {};
809
810         // deal with barcodes and call numbers for *NEW* units
811         var barcodes = {};
812         var call_numbers = {};
813         var call_numbers_by_siss_and_sdist = {};
814
815         var deferred = $q.defer();
816         var current_promise = deferred.promise;
817         var last_promise;
818
819         var sitem_alerts = [];
820         var sdist_alerts = [];
821         var ssub_alerts = list[0].stream().distribution().subscription().notes().filter(function(n){
822             return n.alert() == 't';
823         })
824
825         var dist_seen = {};
826         angular.forEach(list, function(i) {
827             sitem_alerts = sitem_alerts.concat(
828                 i.notes().filter(function(n){
829                     return n.alert() == 't';
830                 })
831             );
832             var sdist = '_'+i.stream().distribution().id();
833             if (!dist_seen[sdist]) {
834                 dist_seen[sdist] = 1;
835                 sdist_alerts = sdist_alerts.concat(
836                     i.stream().distribution().notes().filter(function(n){
837                         return n.alert() == 't';
838                     })
839                 );
840             }
841         });
842
843         if (do_barcode || bind) {
844
845             last_promise = current_promise.then(function(){ return $uibModal.open({
846                 templateUrl: './serials/t_batch_receive',
847                 size: 'lg',
848                 windowClass: 'eg-wide-modal',
849                 backdrop: 'static',
850                 controller:
851                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
852
853                     $scope.print_routing_lists = print_rl;
854                     $scope.barcode_items = do_barcode;
855                     $scope.force_bind = bind;
856                     $scope.bind = bind;
857                     $scope.items = list;
858                     $scope.ssub_alerts = ssub_alerts;
859                     $scope.sdist_alerts = sdist_alerts;
860                     $scope.sitem_alerts = sitem_alerts;
861                     $scope.acn_list = [];
862                     $scope.acnp_labels = [];
863                     $scope.acns_labels = [];
864                     $scope.acpl_list = [];
865
866                     $scope.cannot_print = function (index) {
867                         return $scope.items[index].stream().routing_list_users().length == 0 || ($scope.bind && index > 0);
868                     }
869
870                     $scope.bind_or_none = function (index) {
871                         return !$scope.barcode_items || ($scope.bind && index > 0);
872                     }
873
874                     $scope.focus_next_barcode = function (index) {
875                         index++;
876                         $('#item_barcode_'+index).focus().select();
877                     }
878
879                     $scope.fullCNLabel = function (cn) {
880                         var label = [cn.prefix.label,cn.label,cn.suffix.label].join(' ');
881                         return label;
882                     }
883
884                     $scope.apply_template_overrides = function (e) {
885                         if ($scope.selected_call_number) {
886                             angular.forEach($scope.items, function (i) {
887                                 i._call_number = $scope.selected_call_number.label;
888                                 i._cn_prefix = $scope.selected_call_number.prefix.label;
889                                 i._cn_suffix = $scope.selected_call_number.suffix.label;
890                             });
891                         }
892                         if ($scope.selected_circ_mod) {
893                             angular.forEach($scope.items, function (i) {
894                                 i._circ_mod = $scope.selected_circ_mod;
895                             });
896                         }
897                         if ($scope.selected_copy_location) {
898                             angular.forEach($scope.items, function (i) {
899                                 i._copy_location = $scope.selected_copy_location;
900                             });
901                         }
902                     }
903
904                     $scope.ok = function(items) { $uibModalInstance.close(items) }
905                     $scope.cancel = function () { $uibModalInstance.dismiss() }
906
907                     var dist_libs = {};
908                     var pile_o_promises = [$q.when()];
909
910                     // let's gather what we need...
911                     angular.forEach(list, function (i, index) {
912                         var dlib = i.stream().distribution().holding_lib().id();
913                         dist_libs[dlib] = egCore.org.fullPath(dlib, true);
914                         if (i.unit()) {
915                             i._barcode = i.unit().barcode();
916                             pile_o_promises.push(
917                                 egCore.pcrud.retrieve(
918                                     'acn', i.unit().call_number(),
919                                     {flesh : 1, flesh_fields : {acn : ['prefix','suffix']}}
920                                 ).then(function(cn){
921                                     if (cn.deleted() == 'f') {
922                                         i._call_number = cn.label();
923                                         i._cn_prefix = cn.prefix().label();
924                                         i._cn_suffix = cn.suffix().label();
925                                     }
926                                 })
927                             );
928                         } else {
929                             if (i.stream().distribution()[mode + '_call_number']() && 
930                                 i.stream().distribution()[mode + '_call_number']().deleted() == 'f'
931                             ) {
932                                 i._call_number = i.stream().distribution()[mode + '_call_number']().label();
933                             } else {
934                                 pile_o_promises.push(
935                                     service.fetchLastCallnumber(
936                                         i.stream().distribution().holding_lib().id()
937                                     ).then(function(cn){
938                                         if (cn) {
939                                             i._call_number = cn.label();
940                                             i._cn_prefix = cn.prefix().label();
941                                             i._cn_suffix = cn.suffix().label();
942                                         }
943                                     })
944                                 );
945                             }
946                         }
947
948                         if (i.stream().distribution()[mode + '_unit_template']()) {
949                             i._copy_location = i.stream().distribution()[mode + '_unit_template']().location();
950                             i._circ_mod = i.stream().distribution()[mode + '_unit_template']().circ_modifier();
951                         }
952
953                         if ($scope.print_routing_lists && !$scope.cannot_print(index))
954                             i._print_routing_list = true;
955
956                         i._receive = true;
957                     });
958
959                     // build unique list of orgs from distribution.holding_lib fullPaths
960                     var dist_lib_list = [];
961                     angular.forEach(dist_libs, function (l) {
962                         dist_lib_list = dist_lib_list.concat(l);
963                     });
964                     dist_lib_list = dist_lib_list.filter(function(v,i,s){
965                         return s.indexOf(v) == i;
966                     });
967
968                     // Copy locations only come from the workstation location, same as XUL
969                     pile_o_promises.push(egCore.pcrud.search(
970                         'acpl',
971                         {owning_lib : egCore.org.fullPath(egCore.auth.user().ws_ou(), true)},
972                         {},{ atomic : true }
973                     ).then(function (list) {
974                         $scope.acpl_list = list.map(function(i){return egCore.idl.toHash(i)});
975                         return $q.when();
976                     }));
977
978                     // Call numbers, however, come from anywhere the distributions might live
979                     pile_o_promises.push(egCore.pcrud.search(
980                         'acn',
981                         {deleted : 'f', record : bibId, owning_lib : dist_lib_list},
982                         {flesh : 1, flesh_fields : {acn : ['prefix','suffix']}},{ atomic : true }
983                     ).then(function (list) {
984                         $scope.acn_list = list.map(function(i){return egCore.idl.toHash(i)});
985                         return $q.when();
986                     }));
987
988                     // Likewise for prefix and suffix, for combo box
989                     angular.forEach(['acnp','acns'], function (cl) {
990                         pile_o_promises.push(egCore.pcrud.search(
991                             cl,
992                             {owning_lib : dist_lib_list},
993                             {},{ atomic : true }
994                         ).then(function (list) {
995                             $scope[cl+'_labels'] = list.map(function(i){return i.label()});
996                             return $q.when();
997                         }));
998                     });
999
1000                     pile_o_promises.push(egCore.pcrud.retrieveAll(
1001                         'ccm', {}, { atomic : true }
1002                     ).then(function (list) {
1003                         $scope.ccm_list = list.map(function(i){return egCore.idl.toHash(i)});
1004                         return $q.when();
1005                     }));
1006
1007                     $q.all(pile_o_promises).then(function() {
1008                         console.log('receive data collected');
1009                     });
1010
1011                     $scope.$watch('barcode_items', function (n,o) {
1012                         if (n === undefined || n == o) return;
1013                         do_barcode = n;
1014                     });
1015
1016                     $scope.$watch('bind', function (n,o) {
1017                         if (n === undefined || n == o) return;
1018                         bind = n;
1019                         if (bind) {
1020                             angular.forEach($scope.items, function (i,index) {
1021                                 if (index > 0) i._print_routing_list = false;
1022                             });
1023                         }
1024                     });
1025                         
1026                     $scope.$watch('auto_barcodes', function (n,o) {
1027                         if (n === undefined || n == o) return;
1028
1029                         var bc = '@@AUTO';
1030                         if (!n) bc = '';
1031
1032                         angular.forEach($scope.items, function (i) {
1033                             if (!i.stream().distribution().receive_unit_template()) return;
1034                             var _barcode = i._barcode;
1035                             i._barcode = bc || i._old_barcode;
1036                             i._old_barcode = _barcode;
1037                         });
1038                     });
1039
1040                     $scope.$watch('print_routing_lists', function (n,o) {
1041                         if (n === undefined || n == o) return;
1042
1043                         angular.forEach($scope.items, function(i, index) {
1044                             if (!$scope.cannot_print(index)) {
1045                                 i._print_routing_list = n;
1046                             } else {
1047                                 i._print_routing_list = false;
1048                             }
1049                         });
1050                     });
1051                 }]
1052             }).result});
1053         } else {
1054             last_promise = current_promise.then(function(){ return $uibModal.open({
1055                 templateUrl: './serials/t_receive_alerts',
1056                 backdrop: 'static',
1057                 controller:
1058                 ['$scope', '$uibModalInstance', function($scope, $uibModalInstance) {
1059                     $scope.title = egCore.strings.CONFIRM_CHANGE_ITEMS[mode];
1060                     $scope.items = list.length;
1061                     $scope.list = list;
1062                     $scope.mode = mode;
1063                     $scope.ssub_alerts = ssub_alerts;
1064                     $scope.sdist_alerts = sdist_alerts;
1065                     $scope.sitem_alerts = sitem_alerts;
1066
1067                     $scope.ok = function(items) { $uibModalInstance.close(items) }
1068                     $scope.cancel = function () { $uibModalInstance.dismiss() }
1069                 }]
1070             }).result.then(
1071                 function(items) {
1072                     angular.forEach(list, function (i, index) {
1073                         i._receive = true;
1074                     });
1075                     return $q.when(list);
1076                 })
1077             });
1078         }
1079
1080         last_promise.then(function (items) {
1081
1082             var method;
1083             if (mode == 'receive') {
1084                 method = 'open-ils.serial.receive_items';
1085                 items = items.filter(function(i){return i._receive});
1086             } else if ( mode == 'bind') {
1087                 method = 'open-ils.serial.bind_items';
1088                 items = items.filter(function(i){return i._receive});
1089             } else if ( mode == 'reset') {
1090                 method = 'open-ils.serial.reset_items';
1091             } 
1092
1093             if (!items.length) return $q.reject();
1094
1095             var donor_unit_ids = {};
1096             angular.forEach(items, function(i, index) {
1097                 if (i.unit()) donor_unit_ids[i.unit().id()] = 1;
1098                 if (do_barcode) i.unit(-1);
1099                 if (bind) i.unit(-2);
1100                 copy_locations[i.id()] = i._copy_location;
1101                 circ_mods[i.id()] = i._circ_mod;
1102                 call_numbers[i.id()] = [i._cn_prefix, i._call_number, i._cn_suffix] || 'DEFAULT';
1103                 barcodes[i.id()] = i._barcode || '@@AUTO';
1104                 if (bind && index > 0) barcodes[i.id()] = items[0]._barcode;
1105             });
1106
1107             return egCore.net.request(
1108                 'open-ils.serial', method,
1109                 egCore.auth.token(), items, barcodes, call_numbers, donor_unit_ids,
1110                     {circ_mods:circ_mods, copy_locations : copy_locations}
1111             ).then(
1112                 function(resp) {
1113                     var evt = egCore.evt.parse(resp);
1114                     if (evt) {
1115                         ngToast.danger(egCore.strings.SERIALS_ISSUANCE_FAIL_SAVE);
1116                     } else {
1117                         ngToast.success(egCore.strings.SERIALS_ISSUANCE_SUCCESS_SAVE);
1118                         return service.print_routing_lists(bibId, items, do_barcode || bind, false, print_rl)
1119                             .finally(callback);
1120                     }
1121                 },
1122                 function(resp) {
1123                     ngToast.danger(egCore.strings.SERIALS_ISSUANCE_FAIL_SAVE);
1124                 }
1125             );
1126         });
1127
1128         return deferred.resolve();
1129     }
1130
1131     service.add_issuances = function (mySsubId) {
1132         if (!mySsubId && service.subId) mySsubId = service.subId;
1133         if (!mySsubId) return $q.reject('fetchItemsForSub: no subscription id');
1134
1135         var sub = service.get_ssub(mySsubId);
1136         if (!sub) return $q.reject('fetchItemsForSub: unknown subscription id');
1137
1138         var streams = [];
1139         angular.forEach(sub.distributions(), function(dist) {
1140             angular.forEach(
1141                 dist.streams().map(
1142                     function (stream) { return stream.id() }
1143                 ),
1144                 function (sid) { streams.push(sid) }
1145             );
1146         });
1147
1148         var options = { 
1149             order_by : [{class:'sitem',field:'date_expected',direction:'desc'}], // best aprox of pub date
1150             limit : 1,
1151             flesh : 1,
1152             flesh_fields : { sitem : ['issuance'] }
1153         };
1154
1155         return egCore.pcrud.search(
1156             'sitem', {stream:streams},
1157             {   order_by : [{class:'sitem',field:'date_expected',direction:'desc'}], // best aprox of pub date
1158                 limit : 1,
1159                 flesh : 1,
1160                 flesh_fields : { sitem : ['issuance'] }
1161             },
1162             { atomic : true }
1163         ).then(function(list) {
1164             var lastItem = list[0];
1165     
1166             if (lastItem) lastItem = lastItem.issuance();
1167     
1168             return service.new_holding_code({
1169                 title : egCore.strings.SERIALS_ISSUANCE_PREDICT,
1170                 request_count : true,
1171                 prev_iss : lastItem,
1172                 allow_adhoc : false
1173             }).then(function(hc) {
1174     
1175                 var base_iss;
1176                 if (!lastItem) {
1177                     base_iss = new egCore.idl.siss();
1178                     base_iss.creator( egCore.auth.user().id() );
1179                     base_iss.editor( egCore.auth.user().id() );
1180                     base_iss.date_published( hc.date.toISOString() );
1181                     base_iss.subscription( mySsubId );
1182                     base_iss.caption_and_pattern( hc.scap );
1183                     base_iss.holding_code( JSON.stringify(hc.holding_code) );
1184                     base_iss.holding_type( hc.type );
1185                 }
1186     
1187                 // if we're predicting without a preexisting holding, reduce the count
1188                 if (!lastItem) hc.count--;
1189     
1190                 return egCore.net.request(
1191                     'open-ils.serial',
1192                     'open-ils.serial.make_predictions',
1193                     egCore.auth.token(),
1194                     { ssub_id : mySsubId,
1195                       include_base_issuance : lastItem ? 0 : 1,
1196                       num_to_predict : hc.count,
1197                       base_issuance : base_iss || lastItem
1198                     }
1199                 ).then(
1200                     function(resp) {
1201                         var evt = egCore.evt.parse(resp);
1202                         if (evt) {
1203                             ngToast.danger(egCore.strings.SERIALS_ISSUANCE_FAIL_SAVE);
1204                         } else {
1205                             ngToast.success(egCore.strings.SERIALS_ISSUANCE_SUCCESS_SAVE);
1206                         }
1207                     },
1208                     function(resp) {
1209                         ngToast.danger(egCore.strings.SERIALS_ISSUANCE_FAIL_SAVE);
1210                     }
1211                 );
1212             });
1213         });
1214     }
1215
1216     return service;
1217 }]);
1218