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