]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/xul/staff_client/server/serial/batch_receive.js
LP#1081551 Serials batch recv. dupe barcode check
[working/Evergreen.git] / Open-ILS / xul / staff_client / server / serial / batch_receive.js
1 /* The code in this file relies on common.js */
2
3 dojo.require("dojo.cookie");
4 dojo.require("openils.Util");
5 dojo.require("openils.User");
6 dojo.require("openils.CGI");
7 dojo.require("openils.XUL");
8 dojo.require("openils.PermaCrud");
9
10 JSAN.use("util.error");
11 var _error = new util.error();
12 var batch_receiver;
13
14 function _generic_onmethoderror(r, stat, stat_text) {
15     _error.standard_unexpected_error_alert(
16         dojo.byId("commonStrings").getString("common.error"),
17         stat + ":" + stat_text
18     );
19     busy(false);
20 }
21
22 function S(k) {
23     return dojo.byId("serialStrings").getString("batch_receive." + k).
24         replace("\\n", "\n");
25 }
26
27 function F(k, args) {
28     return dojo.byId("serialStrings").
29         getFormattedString("batch_receive." + k, args).replace("\\n", "\n");
30 }
31
32 function BatchReceiver() {
33     var self = this;
34
35     this.init = function(authtoken, bib_id, sub_id) {
36         if (authtoken) {
37             this.user = new openils.User({"authtoken": authtoken});
38             this.pcrud = new openils.PermaCrud({"authtoken": authtoken});
39             this.authtoken = authtoken;
40         }
41
42         hide("batch_receive_sub");
43         hide("batch_receive_entry");
44         hide("batch_receive_bibdata_bits");
45         hide("batch_receive_sub_bits");
46         hide("batch_receive_issuance_bits");
47         hide("batch_receive_issuance");
48
49         dojo.byId("bib_lookup_submit").disabled = false;
50         dojo.byId("bib_search_term").value = "";
51
52         if (!bib_id) {
53             show("batch_receive_bib");
54             dojo.byId("bib_search_term").focus();
55         }
56
57         if (!this.entry_tbody) {
58             this.entry_tbody = dojo.byId("entry_tbody");
59             this.template = this.entry_tbody.removeChild(
60                 dojo.byId("entry_template")
61             );
62         }
63
64         this._clear_entry_batch_row();
65
66         this._call_number_cache = null;
67         this._prepared_call_number_controls = {};
68         this._location_by_lib = {};
69         this._copy_template_cache = {};
70         this._wants_print_routing = {};
71
72         /* empty the entry receiving table if we're starting over */
73         if (this.item_cache) {
74             for (var id in this.item_cache) {
75                 this.finish_receipt(this.item_cache[id]);
76                 hard_empty(this.entry_tbody);
77             }
78             /* XXX incredibly, running hard_empty() more than once seems to be
79              * good and necessary.  There's a bug under the covers somewhere,
80              * but this keeps it out of sight for the moment. */
81              hard_empty(this.entry_tbody);
82         }
83         hard_empty(this.entry_tbody);
84
85         this.rows = {};
86         this.item_cache = {};
87
88         if (bib_id)
89             this.bib_lookup(bib_id, null, true, sub_id);
90
91         busy(false);
92     };
93
94     this._clear_entry_batch_row = function() {
95         dojo.forEach(
96             dojo.byId("entry_batch_row").childNodes,
97             function(node) {
98                 if (node.nodeType == 1 &&
99                     node.getAttribute("name") != "barcode")
100                     hard_empty(node);
101             }
102         );
103     };
104
105     this._show_bibdata_bits = function() {
106         hard_empty("title_here");
107         dojo.byId("title_here").appendChild(T(this.bibdata.mvr.title()));
108         hard_empty("author_here");
109
110         if (this.bibdata.mvr.author()) {
111             dojo.byId("author_here").appendChild(T(this.bibdata.mvr.author()));
112             show("author_here_holder");
113         } else {
114             hide("author_here_holder");
115         }
116
117         show("batch_receive_bibdata_bits");
118     };
119
120     this._sub_label = function(sub) {
121         /* XXX use a formatting string from serial.properties */
122         return sub.id() + ": (" + sub.owning_lib().shortname() + ") " +
123             D(sub.start_date()) + " - " + D(sub.end_date());
124     };
125
126     this._show_sub_bits = function() {
127         hard_empty("sublabel_here");
128         dojo.place(
129             T(this._sub_label(this.sub)),
130             "sublabel_here",
131             "only"
132         );
133         hide("batch_receive_sub");
134         show("batch_receive_sub_bits");
135     };
136
137     this._show_issuance_bits = function() {
138         hide("batch_receive_issuance");
139         hard_empty("issuance_label_here");
140         dojo.place(
141             T(this.issuance.label()),
142             "issuance_label_here",
143             "only"
144         );
145         show("batch_receive_issuance_bits");
146     }
147
148     this._get_receivable_issuances = function() {
149         var issuances = [];
150
151         busy(true);
152         try {
153             fieldmapper.standardRequest(
154                 ["open-ils.serial", "open-ils.serial.issuances.receivable"], {
155                     "params": [this.authtoken, this.sub.id()],
156                     "async": false,
157                     "onresponse": function(r) {
158                         if (r = openils.Util.readResponse(r))
159                             issuances.push(r);
160                     },
161                     "onmethoderror": _generic_onmethoderror
162                 }
163             );
164         } catch (E) {
165             alert(E);
166         }
167         busy(false);
168
169         return issuances;
170     };
171
172     this._build_circ_modifier_dropdown = function() {
173         if (!this._built_circ_modifier_dropdown) {
174             var menulist = dojo.create("menulist");
175             var menupopup = dojo.create("menupopup", null, menulist, "only");
176             dojo.create(
177                 "menuitem", {"value": 0, "label": S("none")},
178                 menupopup, "first"
179             );
180
181             var mods = [];
182             fieldmapper.standardRequest(
183                 ["open-ils.circ", "open-ils.circ.circ_modifier.retrieve.all"],{
184                     "params": [{"full": true}],
185                     "async": false,
186                     "onresponse": function(r) {
187                         if (mods = openils.Util.readResponse(r)) {
188                             mods.sort(
189                                 function(a,b) {
190                                     return a.code() > b.code() ? 1 :
191                                         b.code() > a.code() ? -1 :
192                                         0;
193                                 }
194                             ).forEach(
195                                 function(mod) {
196                                     dojo.create(
197                                         "menuitem", {
198                                             "value": mod.code(),
199                                             "label": document.getElementById('commonStrings').getFormattedString('staff.circ_modifier.display',[mod.code(),mod.name(),mod.description()]) 
200                                         }, menupopup, "last"
201                                     );
202                                 }
203                             );
204                         }
205                     },
206                     "onmethoderror": _generic_onmethoderror
207                 }
208             );
209             if (!mods.length) {
210                 /* in this case, discard menulist and menupopup */
211                 this._built_circ_modifier_dropdown =
212                     dojo.create("description", {"value": "-"});
213             } else {
214                 this._built_circ_modifier_dropdown = menulist;
215             }
216         }
217
218         return dojo.clone(this._built_circ_modifier_dropdown);
219     };
220
221     this._extend_circ_modifier_for_batch = function(control) {
222         dojo.create(
223             "menuitem", {"value": -1, "label": "---"},
224             dojo.query("menupopup", control)[0],
225             "first"
226         );
227         control.value = -1;
228         return control;
229     };
230
231     this._build_location_dropdown = function(locs, add_unset_value) {
232         var menulist = dojo.create("menulist");
233         var menupopup = dojo.create("menupopup", null, menulist, "only");
234
235         if (add_unset_value) {
236             dojo.create(
237                 "menuitem", {"value": -1, "label": "---"}, menupopup, "first"
238             );
239         }
240
241         locs.forEach(
242             function(loc) {
243                 dojo.create(
244                     "menuitem", {
245                         "value": loc.id(), "label": loc.name()
246                     }, menupopup, "last"
247                 );
248             }
249         );
250
251         return menulist;
252     };
253
254     this._get_locations_for_lib = function(lib) {
255         if (!this._location_by_lib[lib]) {
256             fieldmapper.standardRequest(
257                 ["open-ils.circ", "open-ils.circ.copy_location.retrieve.all"],{
258                     "params": [lib, false, true],
259                     "async": false,
260                     "onresponse": function(r) {
261                         if (locs = openils.Util.readResponse(r))
262                             self._location_by_lib[lib] = locs;
263                     },
264                     "onmethoderror": _generic_onmethoderror
265                 }
266             );
267         }
268
269         return this._location_by_lib[lib];
270     };
271
272     this._build_call_number_control = function(item) {
273         /* In any case, give a dropdown of call numbers related to the
274          * same bre as the subscription relates to. */
275         if (!this._call_number_cache) {
276             this._call_number_cache = this.pcrud.search(
277                 "acn", {
278                     "record": this.sub.record_entry()
279                 }, {
280                     "order_by": {"acn": "label"},   /* XXX wrong sorting? */
281                 }
282             );
283         }
284
285         if (typeof item == "undefined") {
286             /* In this case, no further limiting of call numbers for now,
287              * although ideally it might be nice to limit to call numbers
288              * with owning_lib matching the holding_lib of the distribs
289              * that ultimately relate to the items. */
290
291             var menulist = dojo.create("menulist", {
292                 "editable": "true", "className": "cn"
293             });
294             var menupopup = dojo.create("menupopup", null, menulist, "only");
295
296             openils.Util.uniqueObjects(this._call_number_cache, "label").
297                 forEach(
298                     function(cn) {
299                         dojo.create(
300                             "menuitem", {
301                                 "value": cn.label(), "label": cn.label()
302                             }, menupopup, "last"
303                         );
304                     }
305                 );
306
307             return menulist;
308         } else {
309             /* In this case, limit call numbers by owning_lib matching
310              * distributions's holding_lib. */
311
312             var lib = item.stream().distribution().holding_lib().id();
313             if (!this._prepared_call_number_controls[lib]) {
314                 var menulist = dojo.create("menulist", {
315                     "editable": "true", "className": "cn"
316                 });
317                 var menupopup = dojo.create("menupopup", null, menulist,"only");
318                 this._call_number_cache.filter(
319                     function(cn) { return cn.owning_lib() == lib; }
320                 ).forEach(
321                     function(cn) {
322                         dojo.create(
323                             "menuitem", {
324                                 "value": cn.label(), "label": cn.label()
325                             }, menupopup, "last"
326                         );
327                     }
328                 );
329                 this._prepared_call_number_controls[lib] = menulist;
330             }
331             return dojo.clone(this._prepared_call_number_controls[lib]);
332         }
333     };
334
335     this._build_batch_location_dropdown = function() {
336         var menulist = dojo.create("menulist");
337         var menupopup = dojo.create("menupopup",null,menulist);
338         dojo.create("menuitem", {"value": -1, "label": "---"}, menupopup);
339
340         fieldmapper.standardRequest(
341             ["open-ils.circ",
342                 "open-ils.circ.copy_location.retrieve.distinct.atomic"],{
343                 "params": [],
344                 "async": false,
345                 "onresponse": function(r) {
346                     if (list = openils.Util.readResponse(r)) {
347                         list.forEach(
348                             function(locname) {
349                                 dojo.create(
350                                     "menuitem", {
351                                         "value": locname, "label": locname
352                                     }, menupopup
353                                 );
354                             }
355                         );
356                     }
357                 },
358                 "onmethoderror": _generic_onmethoderror
359             }
360         );
361
362         return menulist;
363     };
364
365     this._build_print_routing_toggle = function(item) {
366         var start = true;
367         var checkbox = dojo.create(
368             "checkbox", {
369                 "oncommand": function(ev) {
370                         self._print_routing(item.id(), ev.target.checked);
371                 },
372                 "checked": start.toString()
373             }
374         );
375         this._print_routing(item.id(), start);
376         return checkbox;
377     }
378
379     this._build_receive_toggle = function(item) {
380         return dojo.create(
381             "checkbox", {
382                 "oncommand": function(ev) {
383                         self._disable_row(item.id(), !ev.target.checked);
384                 },
385                 "checked": "true",
386                 "name": "receive_" + item.id()
387             }
388         );
389     }
390
391     this._disable_row = function(item_id, disabled) {
392         var row = this.rows[item_id];
393         dojo.query(
394             "textbox,menulist,checkbox:not([name^='receive_'])", row
395         ).forEach(
396             function(element) { element.disabled = disabled; }
397         );
398         this._row_disabled(row, disabled);
399     };
400
401     this._row_disabled = function(row, disabled) {
402         if (typeof(row) == "string") row = this.rows[row];
403
404         var checkbox = dojo.query("checkbox", row)[1];
405
406         if (typeof(disabled) != "undefined")
407             checkbox.checked = !disabled;
408
409         return !checkbox.checked;
410     };
411
412     this._row_print_routing_disabled = function(row, disabled) {
413         if (typeof(row) == "string") row = this.rows[row];
414
415         var checkbox = dojo.query("checkbox", row)[0];
416
417         if (typeof(disabled) != "undefined") {
418             checkbox.checked = !disabled;
419             checkbox.doCommand();
420         }
421
422         return !checkbox.checked;
423     };
424
425     this._row_field_value = function(row, field, value) {
426         if (typeof(row) == "string") row = this.rows[row];
427
428         var node = dojo.query("*", node_by_name(field, row))[0];
429
430         if (typeof(value) == "undefined") {
431             return node.selectedItem ?
432                 node.selectedItem.value : node.value;
433         } else {
434             /* XXX The new two lines /should/ each do the same thing, but
435              * apparently they don't.  With only one or the other, I get
436              * skipped fields when this is called by the code that
437              * pre-populates fields based on copy templates.  This may
438              * have something to do with Dojo and XUL not getting along
439              * completely? */
440             dojo.attr(node, "value", value);
441             node.value = value;
442         }
443     }
444
445     this._print_routing = function(id, value) {
446         this._wants_print_routing[id] = value;
447     };
448
449         this._user_wants_autogen = function() {
450         return dojo.byId("autogen_barcodes").checked;
451     };
452
453     this._get_autogen_potentials = function(item_id) {
454         var hit_a_wall = false;
455
456         return [openils.Util.objectProperties(this.rows).sort(num_sort).filter(
457             function(id) {
458                 if (hit_a_wall) {
459                     return false;
460                 } else if (id <= item_id || self._row_disabled(id)) {
461                     return false;
462                 } else if (self._row_field_value(id, "barcode")) {
463                     hit_a_wall = true;
464                     return false;
465                 } else {
466                     return true;
467                 }
468             }
469         ), hit_a_wall];
470     };
471
472     this._prepare_autogen_control = function() {
473         dojo.attr("autogen_barcodes",
474             "command", function(ev) {
475                 if (!ev.target.checked) {
476                     var list = self._have_autogen_barcodes();
477                     if (list.length && confirm(S("autogen_barcodes.remove"))) {
478                         list.forEach(
479                             function(id) {
480                                 self._row_field_value(id, "barcode", "");
481                                 self.rows[id]._has_autogen_barcode = false;
482                             }
483                         );
484                     }
485                 }
486             }
487         );
488     };
489
490     this._have_autogen_barcodes = function() {
491         var list = [];
492         for (var id in this.rows)
493             if (this.rows[id]._has_autogen_barcode) list.push(id);
494         return list;
495     };
496
497     this._cn_exists_but_not_for_lib = function(lib, value) {
498         var exists = this._call_number_cache.filter(
499             function(cn) { return cn.label() == value }
500         );
501         var for_lib = exists.filter(
502             function(cn) { return cn.owning_lib() == lib; }
503         );
504         return (exists.length && !for_lib.length);
505     };
506
507     this._call_number_confirm_for_lib = function(lib, value) {
508         if (!this._has_confirmed_cn_for)
509             this._has_confirmed_cn_for = {};
510
511         if (typeof(this._has_confirmed_cn_for[lib.id()]) == "undefined") {
512             if (this._cn_exists_but_not_for_lib(lib.id(), value)) {
513                 this._has_confirmed_cn_for[lib.id()] = confirm(
514                     F("cn_for_lib", [lib.shortname()])
515                 );
516             } else {
517                 this._has_confirmed_cn_for[lib.id()] = true;
518             }
519         }
520
521         return this._has_confirmed_cn_for[lib.id()];
522     }
523
524     this._confirm_row_field_application = function(id, key, value) {
525         if (key == "call_number") { /* XXX make a dispatch table so we can do
526                                        this for other fields too */
527             return this._call_number_confirm_for_lib(
528                 this.item_cache[id].stream().distribution().holding_lib(),
529                 value
530             );
531         } else {
532             return true;
533         }
534     };
535
536     this._location_by_name = function(id, value) {
537         var lib = this.item_cache[id].stream().distribution().
538             holding_lib().id();
539         var winners = this._location_by_lib[lib].filter(
540             function(loc) { return loc.name() == value; }
541         );
542         if (winners.length) {
543             return winners[0].id();
544         } else {
545             return null;
546         }
547     };
548
549     this._set_all_enabled_rows = function(key, value) {
550         /* do NOT do trimming here, set whitespace as is. */
551         for (var id in this.rows) {
552             if (!this._row_disabled(id)) {
553                 if (this._confirm_row_field_application(id, key, value)) {
554                     if (key == "location") /* kludge for this field */ {
555                         if (actual = this._location_by_name(id, value))
556                             this._row_field_value(id, key, actual);
557                     } else {
558                         this._row_field_value(id, key, value);
559                     }
560                 }
561             }
562         }
563     };
564
565     this.print_routing_lists = function(streams) {
566         fieldmapper.standardRequest(
567             ["open-ils.serial",
568                 "open-ils.serial.routing_list_users.fleshed_and_ordered.atomic"],{
569                 "params": [
570                     this.authtoken, streams.map(function(o) { return o.id(); })
571                 ],
572                 "async": false,
573                 "oncomplete": function(r) {
574                     if ((r = openils.Util.readResponse(r)) && r.length) {
575                         openils.XUL.newTabEasy(
576                             "SERIAL_PRINT_ROUTING_LIST_USERS",
577                             S("print_routing_list_users"), {
578                                 "show_print_button": false, /* we supply one */
579                                 "routing_list_data": {
580                                     "streams": streams, "mvr": self.bibdata.mvr,
581                                     "issuance": self.issuance, "users": r
582                                 }
583                             }, true /* wrap_in_browser */
584                         );
585                     }
586                 },
587                 "onmethoderror": _generic_onmethoderror
588             }
589         );
590     };
591
592     this.bib_lookup = function(bib_search_term, evt, is_actual_id, sub_id) {
593         if (evt && evt.keyCode != 13) return;
594
595         if (!bib_search_term) {
596             var bib_search_term = dojo.byId("bib_search_term").value.trim();
597             if (!bib_search_term.length) {
598                 alert(S("bib_lookup.empty"));
599                 return;
600             }
601         }
602
603         hide("batch_receive_sub");
604         hide("batch_receive_entry");
605
606         busy(true);
607         dojo.byId("bib_lookup_submit").disabled = true;
608         fieldmapper.standardRequest(
609             ["open-ils.serial",
610                 "open-ils.serial.biblio.record_entry.by_identifier.atomic"], {
611                 "params": [
612                     bib_search_term, {
613                         "require_subscriptions": true,
614                         "add_mvr": true,
615                         "is_actual_id": is_actual_id
616                     }
617                 ],
618                 "async": false,
619                 "oncomplete": function(r) {
620                     /* These two things better come before readResponse(),
621                      * which can throw exceptions. */
622                     busy(false);
623                     dojo.byId("bib_lookup_submit").disabled = false;
624
625                     var list = openils.Util.readResponse(r, false, true);
626                     if (list && list.length) {
627                         if (list.length > 1) {
628                             /* XXX TODO just let the user pick one from a list,
629                              * although this circumstance seems really
630                              * unlikely.  It just can't happen for TCN, and
631                              * wouldn't be likely for ISxN or UPC... ? */
632                             alert(S("bib_lookup.multiple"));
633                         } else {
634                             self.bibdata = list[0];
635                             self._show_bibdata_bits();
636                             self.choose_subscription(sub_id);
637                         }
638                     } else {
639                         alert(S("bib_lookup.not_found"));
640                         if (is_actual_id) {
641                             self.init();
642                         } else {
643                             dojo.byId("bib_search_term").reset();
644                             dojo.byId("bib_search_term").focus();
645                         }
646                     }
647                 },
648                 "onmethoderror": _generic_onmethoderror
649             }
650         );
651     };
652
653     this.choose_subscription = function(sub_id) {
654         hide("batch_receive_bib");
655         hide("batch_receive_entry");
656         hide("batch_receive_sub_bits");
657         hide("batch_receive_issuance");
658
659         var subs = this.bibdata.bre.subscriptions();
660
661         if (sub_id) {
662             this.choose_issuance(
663                 subs.filter(function(o) { return o.id() == sub_id; })[0]
664             );
665         } else if (subs.length > 1) {
666             var menulist = dojo.create("menulist", {"id": "sub_chooser"});
667             var menupopup = dojo.create("menupopup", {}, menulist, "only");
668
669             this.bibdata.bre.subscriptions().forEach(
670                 function(sub) {
671                     dojo.create(
672                         "menuitem", {
673                             "label": self._sub_label(sub),
674                             "value": sub.id()
675                         }, menupopup, "last"
676                     );
677                 }
678             );
679
680             hard_empty(dojo.byId("sub_chooser_here"));
681
682             dojo.place(menulist, dojo.byId("sub_chooser_here"), "only");
683             show("batch_receive_sub");
684         } else {
685             this.choose_issuance(subs[0]);
686         }
687     };
688
689     this.choose_issuance = function(sub) {
690         hide("batch_receive_bib");
691         hide("batch_receive_entry");
692         hide("batch_receive_sub");
693
694         if (typeof(sub) == "undefined") {   /* sub chosen from menu */
695             var sub_id = dojo.byId("sub_chooser").value;
696             this.sub = this.bibdata.bre.subscriptions().filter(
697                 function(o) { return o.id() == sub_id; }
698             )[0];
699         } else {    /* only one sub possible, passed in directly */
700             this.sub = sub;
701         }
702
703         this._show_sub_bits();
704
705         this.issuances = this._get_receivable_issuances();   /* sync */
706
707         if (this.issuances.length > 1) {
708             var menulist = dojo.create("menulist", {"id": "issuance_chooser"});
709             var menupopup = dojo.create("menupopup", {}, menulist, "only");
710
711             this.issuances.sort(
712                 function(a, b) {
713                     if (a.date_published()>b.date_published()) return 1;
714                     else if (b.date_published()>a.date_published()) return -1;
715                     else return 0;
716                 }
717             ).forEach(
718                 function(issuance) {
719                     dojo.create(
720                         "menuitem", {
721                             "label": issuance.label(),
722                             "value": issuance.id()
723                         }, menupopup, "last"
724                     );
725                 }
726             );
727
728             hard_empty("issuance_chooser_here");
729             dojo.place(menulist, dojo.byId("issuance_chooser_here"), "only");
730
731             show("batch_receive_issuance");
732         } else if (this.issuances.length) {
733             this.load_entry_form(this.issuances[0]);
734         } else {
735             alert(S("issuance_lookup.none"));
736             this.init();
737         }
738
739     };
740
741     this._update_copy_template_cache = function() {
742         var templates_needed = openils.Util.uniqueElements(
743             openils.Util.objectProperties(this.item_cache).map(
744                 function(id) {
745                     return self.item_cache[id].stream().distribution().
746                         receive_unit_template();
747                 }
748             )
749         ).filter(
750             function(id) { return !self._copy_template_cache[id]; }
751         );
752
753         if (templates_needed.length) {
754             this.pcrud.search("act", {"id": templates_needed}).forEach(
755                 function(tmpl) {
756                     self._copy_template_cache[tmpl.id()] = tmpl;
757                 }
758             );
759         }
760     }
761
762     this.apply_copy_templates = function() {
763         this._update_copy_template_cache(); /* sync */
764
765         for (var id in this.item_cache) {
766             var item = this.item_cache[id];
767             var template_id =
768                 item.stream().distribution().receive_unit_template();
769             var template = this._copy_template_cache[template_id];
770
771             var row = this.rows[id];
772
773             var tmpl_mod = template.circ_modifier();
774             var tmpl_loc = template.location();
775             var tmpl_price = template.price();
776             if (tmpl_mod != null) {
777                 this._row_field_value(
778                     row, "circ_modifier", tmpl_mod == "" ? 0 : tmpl_mod
779                 );
780             }
781             if (tmpl_loc)
782                 this._row_field_value(row, "location", tmpl_loc);
783             if (tmpl_price > 0)
784                 this._row_field_value(row, "price", tmpl_price);
785         }
786     };
787
788     this.load_entry_form = function(issuance) {
789         if (typeof(issuance) == "undefined") {
790             var issuance_id = dojo.byId("issuance_chooser").value;
791             this.issuance = this.issuances.filter(
792                 function(o) { return o.id() == issuance_id; }
793             )[0];
794         } else {
795             this.issuance = issuance;
796         }
797
798         this._show_issuance_bits();
799         this._prepare_autogen_control();
800
801         busy(true);
802
803         fieldmapper.standardRequest(
804             ["open-ils.serial",
805                 "open-ils.serial.items.receivable.by_issuance.atomic"], {
806                 "params": [this.authtoken, this.issuance.id()],
807                 "async": true,
808                 "onresponse": function(r) {
809                     busy(false);
810
811                     if (list = openils.Util.readResponse(r, false, true)) {
812                         if (list.length) {
813                             busy(true);
814                             show("form_holder");
815
816                             list.forEach(function(o) {self.add_entry_row(o);});
817
818                             self.build_batch_entry_row();
819
820                             var recv_with_units =
821                                 dojo.byId("batch_receive_with_units");
822                             recv_with_units.doCommand();
823                             if (recv_with_units.checked)
824                                 self.apply_copy_templates();
825
826                             show("batch_receive_entry");
827                             busy(false);
828                         } else {
829                             alert(S("item_lookup.none"));
830                             if (self.issuances.length) self.choose_issuance();
831                             else self.init();
832                         }
833                     }
834                 },
835                 "onmethoderror": _generic_onmethoderror
836             }
837         );
838     };
839
840     this.toggle_receive_with_units = function(ev) {
841         var head_row = dojo.byId("batch_receive_entry_thead");
842         var batch_row = dojo.byId("entry_batch_row");
843
844         var fields = [
845             "barcode", "call_number", "price", "location", "circ_modifier"
846         ];
847
848         var table_cell_func = ev.target.checked ?
849             show_table_cell : hide_table_cell;
850         fields.forEach(
851             function(key) {
852                 if (batch_row) table_cell_func(node_by_name(key, batch_row));
853                 if (head_row) table_cell_func(node_by_name(key, head_row));
854
855                 for (var id in self.rows) {
856                     table_cell_func(node_by_name(key, self.rows[id]));
857                 }
858             }
859         );
860
861         if (!ev.target.checked) {
862             /* XXX As of the time of this writing, a blank barcode field will
863              * avoid unit creation */
864             this._set_all_enabled_rows("barcode", "");
865         }
866     };
867
868     this.toggle_all_receive = function(checked) {
869         for (var id in this.rows) {
870             this._disable_row(id, !checked);
871         }
872     };
873
874     this.toggle_all_print_routing = function(checked) {
875         for (var id in this.rows) {
876             this._row_print_routing_disabled(id, !checked);
877         }
878     };
879
880     this.build_batch_entry_row = function() {
881         var row = dojo.byId("entry_batch_row");
882
883         this.batch_controls = {};
884
885         node_by_name("note", row).appendChild(
886             this.batch_controls.note = dojo.create("textbox", {"size": 20})
887         );
888
889         node_by_name("location", row).appendChild(
890             this.batch_controls.location =
891                 this._build_batch_location_dropdown()
892         );
893
894         node_by_name("circ_modifier", row).appendChild(
895             this.batch_controls.circ_modifier =
896                 this._extend_circ_modifier_for_batch(
897                     this._build_circ_modifier_dropdown() /* for all OUs */
898                 )
899         );
900
901         node_by_name("call_number", row).appendChild(
902             this.batch_controls.call_number = this._build_call_number_control()
903         );
904
905         node_by_name("price", row).appendChild(
906             this.batch_controls.price = dojo.create("textbox", {"size": 9})
907         );
908
909         node_by_name("print_routing", row).appendChild(
910             dojo.create(
911                 "checkbox", {
912                     "oncommand": function(ev) {
913                         self.toggle_all_print_routing(ev.target.checked);
914                     },
915                     "checked": "true"
916                 }
917             )
918         );
919
920         node_by_name("receive", row).appendChild(
921             dojo.create(
922                 "checkbox", {
923                     "oncommand": function(ev) {
924                         self.toggle_all_receive(ev.target.checked);
925                     },
926                     "checked": "true"
927                 }
928             )
929         );
930
931         node_by_name("apply", row).appendChild(
932             dojo.create("button", {
933                 "label": S("apply"),
934                 "oncommand": function() { self.apply_batch_values(); }
935             })
936         );
937     };
938
939     this.apply_batch_values = function() {
940         var row = dojo.byId("entry_batch_row");
941
942         for (var key in this.batch_controls) {
943             var value = this.batch_controls[key].value;
944             if (value != "" && value != -1)
945                 this._set_all_enabled_rows(key, value);
946         }
947
948         /* XXX genericize for all fields? */
949         delete this._has_confirmed_cn_for;
950     };
951
952     this.add_entry_row = function(item) {
953         this.item_cache[item.id()] = item;
954         var row = this.rows[item.id()] = dojo.clone(this.template);
955
956         function n(s) { return node_by_name(s, row); }    /* typing saver */
957
958         var stream_dist_label = item.stream().distribution().label();
959         if (item.stream().routing_label())
960             stream_dist_label += " / " + item.stream().routing_label();
961
962         n("holding_lib").appendChild(
963             dojo.create(
964                 "description", {
965                     "value": item.stream().distribution().
966                         holding_lib().shortname(),
967                     "tooltiptext": stream_dist_label
968                 }
969             )
970         );
971
972         n("barcode").appendChild(
973             dojo.create(
974                 "textbox", {
975                     "size": 15,
976                     "tabindex": 10000 + Number(item.id()), /* is this right? */
977                     "onchange": function() {
978                         self.autogen_if_appropriate(this, item.id());
979                     }
980                 }
981             )
982         );
983
984         n("location").appendChild(
985             this._build_location_dropdown(
986                 this._get_locations_for_lib(
987                     item.stream().distribution().holding_lib().id()
988                 )
989             )
990         );
991
992         n("note").appendChild(dojo.create("textbox", {"size": 20}));
993         n("circ_modifier").appendChild(this._build_circ_modifier_dropdown());
994         n("call_number").appendChild(this._build_call_number_control(item));
995         n("price").appendChild(dojo.create("textbox", {"size": 9}));
996         n("print_routing").appendChild(this._build_print_routing_toggle(item));
997         n("receive").appendChild(this._build_receive_toggle(item));
998
999         this.entry_tbody.appendChild(row);
1000     };
1001
1002     this.receive = function() {
1003         var items = [];
1004         var confirmed_missing_units = false;
1005
1006         for (var id in this.rows) {
1007             if (this._row_disabled(id))
1008                 continue;
1009
1010             var item = this.item_cache[id];
1011
1012             /* Don't trim() call_number field, as existing call numbers
1013              * are yielded by their label field, not by id, and if
1014              * they start or end in spaces, we'll unintentionally create
1015              * a new, different CN if we trim that */
1016             var cn_string = this._row_field_value(id, "call_number");
1017             var barcode = this._row_field_value(id, "barcode");
1018             if (barcode && barcode.trim) barcode = barcode.trim();
1019
1020             if (barcode && cn_string.length) {
1021                 var unit = new sunit();
1022                 unit.barcode(barcode);
1023
1024                 ["price", "location", "circ_modifier"].forEach(
1025                     function(field) {
1026                         var value = self._row_field_value(id, field);
1027                         if (value)
1028                             unit[field](value.trim ? value.trim() : value);
1029                     }
1030                 );
1031
1032                 unit.call_number(cn_string);
1033                 item.unit(unit);
1034             } else if (barcode && !cn_string.length) {
1035                 alert(S("missing_cn"));
1036                 return;
1037             } else if (!confirmed_missing_units) {
1038                 if (
1039                     (!dojo.byId("batch_receive_with_units").checked) ||
1040                     confirm(S("missing_units"))
1041                 ) {
1042                     confirmed_missing_units = true;
1043                 } else {
1044                     return;
1045                 }
1046             }
1047
1048             var note_value = this._row_field_value(id, "note").trim();
1049             if (note_value) {
1050                 var note = new sin();
1051                 note.item(id);
1052                 note.pub(false);
1053                 note.title(S("receive_time_note"));
1054                 note.value(note_value);
1055
1056                 item.notes([note]);
1057             }
1058
1059             items.push(item);
1060         }
1061
1062         busy(true);
1063         fieldmapper.standardRequest(
1064             ["open-ils.serial", "open-ils.serial.receive_items.one_unit_per"],{
1065                 "params": [this.authtoken, items, this.sub.record_entry()],
1066                 "async": true,
1067                 "oncomplete": function(r) {
1068                     try {
1069                         var streams_for_printing = [];
1070
1071                         // first check for problems encountered during
1072                         // receive and exit early if any are found.
1073                         var item_ids = [];
1074                         while (item_id = openils.Util.readResponse(r, true)) {
1075                             if (typeof item_id == 'object') { // event
1076                                 if (item_id.textcode == 
1077                                     'SERIAL_UNIT_BARCODE_COLLISION') {
1078                                     alert(F(
1079                                         'unit_barcode_collision', 
1080                                         item_id.payload.barcode
1081                                     ));
1082                                 } else {
1083                                     // unexpected event, rely on toString()
1084                                     alert(item_id); 
1085                                 }
1086                                 busy(false);
1087                                 return;
1088                             }
1089                             item_ids.push(item_id);
1090                         }
1091
1092                         while (item_id = item_ids.shift()) { 
1093                             if (self._wants_print_routing[item_id]) {
1094                                 streams_for_printing.push(
1095                                     self.item_cache[item_id].stream()
1096                                 );
1097                             }
1098                             self.finish_receipt(item_id);
1099                         }
1100                         if (streams_for_printing.length)
1101                             self.print_routing_lists(streams_for_printing);
1102                     } catch (E) {
1103                         alert(E);
1104                     }
1105                     busy(false);
1106                     try {
1107                         xulG.reload_opac();
1108                     } catch(E) {
1109                         (dump ? dump : console.log)(E);
1110                     }
1111                 },
1112                 "onmethoderror": _generic_onmethoderror
1113             }
1114         );
1115     };
1116
1117     this.finish_receipt = function(item_id) {
1118         hard_empty(this.rows[item_id]);
1119         dojo.destroy(this.rows[item_id]);
1120         delete this.rows[item_id];
1121         delete this.item_cache[item_id];
1122     };
1123
1124     this.autogen_if_appropriate = function(textbox, item_id) {
1125         if (this._user_wants_autogen() && textbox.value) {
1126             var kvlist = this._get_autogen_potentials(item_id);
1127             var list = kvlist[0];
1128             var question = kvlist[1];
1129             if (list.length) {
1130                 if (question && !confirm(S("autogen_barcodes.questionable")))
1131                     return;
1132
1133                 busy(true);
1134                 try {
1135                     fieldmapper.standardRequest(
1136                         ["open-ils.cat", "open-ils.cat.item.barcode.autogen"], {
1137                             "params": [
1138                                 this.authtoken, textbox.value, list.length
1139                             ],
1140                             "async": false,
1141                             "onresponse": function(r) {
1142                                 r = openils.Util.readResponse(r, false, true);
1143                                 if (r) {
1144                                     for (var i = 0; i < r.length; i++) {
1145                                         var row = self.rows[list[i]];
1146                                         self._row_field_value(
1147                                             row, "barcode", r[i]
1148                                         );
1149                                         row._has_autogen_barcode = true;
1150                                     }
1151                                 }
1152                             },
1153                             "onmethoderror": _generic_onmethoderror
1154                         }
1155                     );
1156                 } catch (E) {
1157                     alert(E);
1158                 }
1159                 busy(false);
1160             } /* do nothing for empty list */
1161         }
1162     };
1163
1164     this.init.apply(this, arguments);
1165 }
1166
1167 function my_init() {
1168     var cgi = new openils.CGI();
1169     var authtoken = (typeof ses == "function" ? ses() : 0) ||
1170             cgi.param("ses") || dojo.cookie("ses");
1171     if(!authtoken && openils.XUL.isXUL()) {
1172         var stash = openils.XUL.getStash();
1173         authtoken = stash.session.key;
1174     }
1175     batch_receiver = new BatchReceiver(
1176         authtoken,
1177         cgi.param("docid") || null, cgi.param("subid") || null
1178     );
1179 }