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