]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/xul/staff_client/server/serial/batch_receive.js
Serials: an alternative batch receiving interface, to support certain
[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("openils.Util");
5 dojo.require("openils.CGI");
6
7 var authtoken;
8 var batch_receiver;
9
10 String.prototype.trim = function() {return this.replace(/^\s*(.+)\s*$/,"$1");}
11
12 /**
13  * hard_empty() is needed because dojo.empty() doesn't seem to work on
14  * XUL nodes. This also means that dojo.place() with a position argument of
15  * "only" doesn't do what it should, but calling hard_empty() on the refnode
16  * first will do the trick.
17  */
18 function hard_empty(node) {
19     if (typeof(node) == "string")
20         node = dojo.byId(node);
21     if (node)
22         dojo.forEach(node.childNodes, dojo.destroy);
23 }
24
25 function hide(e) {
26     if (typeof(e) == "string") e = dojo.byId(e);
27     openils.Util.addCSSClass(e, "hideme");
28 }
29
30 function show(e) {
31     if (typeof(e) == "string") e = dojo.byId(e);
32     openils.Util.removeCSSClass(e, "hideme");
33 }
34
35 function busy(on) {
36     if (typeof(busy._window) == "undefined")
37         busy._window = dojo.query("window")[0];
38     busy._window.style.cursor = on ? "wait" : "auto";
39 }
40
41 function S(k) {
42     return dojo.byId("serialStrings").getString("batch_receive." + k).
43         replace("\\n", "\n");
44 }
45
46 function T(s) { return document.createTextNode(s); }
47 function D(s) {return s ? openils.Util.timeStamp(s, {"selector":"date"}) : "";}
48 function node_by_name(s, ctx) {return dojo.query("[name='" + s + "']", ctx)[0];}
49
50 function num_sort(a, b) {
51     [a, b] = [Number(a), Number(b)];
52     return a > b ? 1 : (a < b ? -1 : 0);
53 }
54
55 function BatchReceiver() {
56     var self = this;
57
58     this._init = function(bib_id) {
59         hide("batch_receive_sub");
60         hide("batch_receive_entry");
61         hide("batch_receive_bibdata_bits");
62         hide("batch_receive_sub_bits");
63         hide("batch_receive_issuance_bits");
64         hide("batch_receive_issuance");
65
66         dojo.byId("bib_lookup_submit").disabled = false;
67         dojo.byId("bib_search_term").value = "";
68
69         if (!bib_id) {
70             show("batch_receive_bib");
71             dojo.byId("bib_search_term").focus();
72         }
73
74         if (!this.entry_tbody) {
75             this.entry_tbody = dojo.byId("entry_tbody");
76             this.template = this.entry_tbody.removeChild(
77                 dojo.byId("entry_template")
78             );
79         }
80
81         this._clear_entry_batch_row();
82
83         this._copy_loc_by_lib = {};
84
85         /* empty the entry receiving table if we're starting over */
86         if (this.item_cache) {
87             for (var id in this.item_cache)
88                 this.finish_receipt(this.item_cache[id]);
89         }
90
91         this.rows = {};
92         this.item_cache = {};
93
94         if (bib_id)
95             this.bib_lookup(bib_id, null, true);
96
97         busy(false);
98     };
99
100     this._clear_entry_batch_row = function() {
101         dojo.forEach(
102             dojo.byId("entry_batch_row").childNodes,
103             function(node) {
104                 if (node.nodeType == 1 &&
105                     node.getAttribute("name") != "barcode")
106                     hard_empty(node);
107             }
108         );
109     };
110
111     this._show_bibdata_bits = function() {
112         hard_empty("title_here");
113         dojo.byId("title_here").appendChild(T(this.bibdata.mvr.title()));
114         hard_empty("author_here");
115
116         if (this.bibdata.mvr.author()) {
117             dojo.byId("author_here").appendChild(T(this.bibdata.mvr.author()));
118             show("author_here_holder");
119         } else {
120             hide("author_here_holder");
121         }
122
123         show("batch_receive_bibdata_bits");
124     };
125
126     this._sub_label = function(sub) {
127         /* XXX use a formatting string from serial.properties */
128         return sub.id() + ": (" + sub.owning_lib().shortname() + ") " +
129             D(sub.start_date()) + " - " + D(sub.end_date());
130     };
131
132     this._show_sub_bits = function() {
133         hard_empty("sublabel_here");
134         dojo.place(
135             T(this._sub_label(this.sub)),
136             "sublabel_here",
137             "only"
138         );
139         hide("batch_receive_sub");
140         show("batch_receive_sub_bits");
141     };
142
143     this._show_issuance_bits = function() {
144         hide("batch_receive_issuance");
145         hard_empty("issuance_label_here");
146         dojo.place(
147             T(this.issuance.label()),
148             "issuance_label_here",
149             "only"
150         );
151         show("batch_receive_issuance_bits");
152     }
153
154     this._get_receivable_issuances = function() {
155         var issuances = [];
156
157         busy(true);
158         try {
159             fieldmapper.standardRequest(
160                 ["open-ils.serial", "open-ils.serial.issuances.receivable"], {
161                     "params": [authtoken, this.sub.id()],
162                     "async": false,
163                     "onresponse": function(r) {
164                         if (r = openils.Util.readResponse(r))
165                             issuances.push(r);
166                     }
167                 }
168             );
169         } catch (E) {
170             alert(E);
171         }
172         busy(false);
173
174         return issuances;
175     };
176
177     this._build_circ_mod_dropdown = function() {
178         if (!this._built_circ_mod_dropdown) {
179             var menulist = dojo.create("menulist");
180             var menupopup = dojo.create("menupopup", null, menulist, "only");
181             dojo.create(
182                 "menuitem", {"value": 0, "label": S("none")},
183                 menupopup, "first"
184             );
185
186             var mods = [];
187             fieldmapper.standardRequest(
188                 ["open-ils.circ", "open-ils.circ.circ_modifier.retrieve.all"], {
189                     "params": [],
190                     "async": false,
191                     "onresponse": function(r) {
192                         if (mods = openils.Util.readResponse(r)) {
193                             mods.forEach(
194                                 function(mod) {
195                                     dojo.create(
196                                         "menuitem", {
197                                             "value": mod, "label": mod
198                                         }, menupopup, "last"
199                                     );
200                                 }
201                             );
202                         }
203                     }
204                 }
205             );
206             if (!mods.length) {
207                 /* in this case, discard menulist and menupopup */
208                 this._built_circ_mod_dropdown =
209                     dojo.create("description", {"value": "-"});
210             } else {
211                 this._built_circ_mod_dropdown = menulist;
212             }
213         }
214
215         return dojo.clone(this._built_circ_mod_dropdown);
216     };
217
218     this._extend_circ_mod_for_batch = function(control) {
219         dojo.create(
220             "menuitem", {"value": -1, "label": "---"},
221             dojo.query("menupopup", control)[0],
222             "first"
223         );
224         return control;
225     };
226
227     this._build_copy_loc_dropdown = function(locs, add_unset_value) {
228         var menulist = dojo.create("menulist");
229         var menupopup = dojo.create("menupopup", null, menulist, "only");
230
231         if (add_unset_value) {
232             dojo.create(
233                 "menuitem", {"value": -1, "label": "---"}, menupopup, "first"
234             );
235         }
236
237         locs.forEach(
238             function(loc) {
239                 dojo.create(
240                     "menuitem", {
241                         "value": loc.id(),
242                         "label": "(" + loc.owning_lib().shortname() + ") " +
243                             loc.name() /* XXX i18n */
244                     }, menupopup, "last"
245                 );
246             }
247         );
248
249         return menulist;
250     };
251
252     this._get_copy_locs_for_lib = function(lib) {
253         if (!this._copy_loc_by_lib[lib]) {
254             fieldmapper.standardRequest(
255                 ["open-ils.circ", "open-ils.circ.copy_location.retrieve.all"], {
256                     "params": [lib, false, true],
257                     "async": false,
258                     "onresponse": function(r) {
259                         if (locs = openils.Util.readResponse(r))
260                             self._copy_loc_by_lib[lib] = locs;
261                     }
262                 }
263             );
264         }
265
266         return this._copy_loc_by_lib[lib];
267     };
268
269     this._build_receive_toggle = function(item) {
270         return dojo.create(
271             "checkbox", {
272                 "oncommand": function(ev) {
273                         self._disable_row(item.id(), !ev.target.checked);
274                 },
275                 "checked": "true"
276             }
277         );
278     }
279
280     this._disable_row = function(item_id, disabled) {
281         var row = this.rows[item_id];
282         dojo.query("textbox,menulist", row).forEach(
283             function(element) { element.disabled = disabled; }
284         );
285     };
286
287     this._row_disabled = function(row) {
288         if (typeof(row) == "string") row = this.rows[row];
289         return !dojo.query("checkbox", row)[0].checked;
290     };
291
292     this._row_field_value = function(row, field, value) {
293         if (typeof(row) == "string") row = this.rows[row];
294
295         var node = dojo.query("*", node_by_name(field, row))[0];
296
297         if (typeof(value) == "undefined")
298             return node.value;
299         else
300             node.value = value;
301     }
302
303         this._user_wants_autogen = function() {
304         return dojo.byId("autogen_barcodes").checked;
305     };
306
307     this._get_autogen_potentials = function(item_id) {
308         var hit_a_wall = false;
309
310         return [openils.Util.objectProperties(this.rows).sort(num_sort).filter(
311             function(id) {
312                 if (hit_a_wall) {
313                     return false;
314                 } else if (id <= item_id || self._row_disabled(id)) {
315                     return false;
316                 } else if (self._row_field_value(id, "barcode")) {
317                     hit_a_wall = true;
318                     return false;
319                 } else {
320                     return true;
321                 }
322             }
323         ), hit_a_wall];
324     };
325
326     this._prepare_autogen_control = function() {
327         dojo.attr("autogen_barcodes",
328             "command", function(ev) {
329                 if (!ev.target.checked) {
330                     var list = self._have_autogen_barcodes();
331                     if (list.length && confirm(S("autogen_barcodes.remove"))) {
332                         list.forEach(
333                             function(id) {
334                                 self._row_field_value(id, "barcode", "");
335                                 self.rows[id]._has_autogen_barcode = false;
336                             }
337                         );
338                     }
339                 }
340             }
341         );
342     };
343
344     this._have_autogen_barcodes = function() {
345         var list = [];
346         for (var id in this.rows)
347             if (this.rows[id]._has_autogen_barcode) list.push(id);
348         return list;
349     };
350
351     this._set_all_enabled_rows = function(key, value) {
352         /* do NOT do trimming here, set whitespace as is. */
353         for (var id in this.rows) {
354             if (!this._row_disabled(id))
355                 this._row_field_value(id, key, value);
356         }
357     };
358
359     this.bib_lookup = function(bib_search_term, evt, is_actual_id) {
360         if (evt && evt.keyCode != 13) return;
361
362         if (!bib_search_term) {
363             var bib_search_term = dojo.byId("bib_search_term").value.trim();
364             if (!bib_search_term.length) {
365                 alert(S("bib_lookup.empty"));
366                 return;
367             }
368         }
369
370         hide("batch_receive_sub");
371         hide("batch_receive_entry");
372
373         busy(true);
374         dojo.byId("bib_lookup_submit").disabled = true;
375         fieldmapper.standardRequest(
376             ["open-ils.serial",
377                 "open-ils.serial.biblio.record_entry.by_identifier.atomic"], {
378                 "params": [
379                     bib_search_term, {
380                         "require_subscriptions": true,
381                         "add_mvr": true,
382                         "is_actual_id": is_actual_id
383                     }
384                 ],
385                 "async": false,
386                 "oncomplete": function(r) {
387                     /* These two things better come before readResponse(), which
388                      * can throw exceptions. */
389                     busy(false);
390                     dojo.byId("bib_lookup_submit").disabled = false;
391
392                     var list = openils.Util.readResponse(r, false, true);
393                     if (list && list.length) {
394                         if (list.length > 1) {
395                             /* XXX TODO just let the user pick one from a list,
396                              * although this circumstance seems really
397                              * unlikely.  It just can't happen for TCN, and
398                              * wouldn't be likely for ISxN or UPC... ? */
399                             alert(S("bib_lookup.multiple"));
400                         } else {
401                             self.bibdata = list[0];
402                             self._show_bibdata_bits();
403                             self.choose_subscription();
404                         }
405                     } else {
406                         alert(S("bib_lookup.not_found"));
407                         if (is_actual_id) {
408                             self._init();
409                         } else {
410                             dojo.byId("bib_search_term").reset();
411                             dojo.byId("bib_search_term").focus();
412                         }
413                     }
414                 }
415             }
416         );
417     };
418
419     this.choose_subscription = function() {
420         hide("batch_receive_bib");
421         hide("batch_receive_entry");
422         hide("batch_receive_sub_bits");
423         hide("batch_receive_issuance");
424
425         var subs = this.bibdata.bre.subscriptions();
426
427         if (subs.length > 1) {
428             var menulist = dojo.create("menulist", {"id": "sub_chooser"});
429             var menupopup = dojo.create("menupopup", {}, menulist, "only");
430
431             this.bibdata.bre.subscriptions().forEach(
432                 function(sub) {
433                     dojo.create(
434                         "menuitem", {
435                             "label": self._sub_label(sub),
436                             "value": sub.id()
437                         }, menupopup, "last"
438                     );
439                 }
440             );
441
442             hard_empty(dojo.byId("sub_chooser_here"));
443
444             dojo.place(menulist, dojo.byId("sub_chooser_here"), "only");
445             show("batch_receive_sub");
446         } else {
447             this.choose_issuance(subs[0]);
448         }
449     };
450
451     this.choose_issuance = function(sub) {
452         hide("batch_receive_bib");
453         hide("batch_receive_entry");
454         hide("batch_receive_sub");
455
456         if (typeof(sub) == "undefined") {   /* sub chosen from menu */
457             var sub_id = dojo.byId("sub_chooser").value;
458             this.sub = this.bibdata.bre.subscriptions().filter(
459                 function(o) { return o.id() == sub_id; }
460             )[0];
461         } else {    /* only one sub possible, passed in directly */
462             this.sub = sub;
463         }
464
465         this._show_sub_bits();
466
467         this.issuances = this._get_receivable_issuances();   /* sync */
468
469         if (this.issuances.length > 1) {
470             var menulist = dojo.create("menulist", {"id": "issuance_chooser"});
471             var menupopup = dojo.create("menupopup", {}, menulist, "only");
472
473             this.issuances.sort(
474                 function(a, b) {
475                     if (a.date_published() > b.date_published()) return 1;
476                     else if (b.date_published() > a.date_published()) return -1;
477                     else return 0;
478                 }
479             ).forEach(
480                 function(issuance) {
481                     dojo.create(
482                         "menuitem", {
483                             "label": issuance.label(),
484                             "value": issuance.id()
485                         }, menupopup, "last"
486                     );
487                 }
488             );
489
490             hard_empty("issuance_chooser_here");
491             dojo.place(menulist, dojo.byId("issuance_chooser_here"), "only");
492
493             show("batch_receive_issuance");
494         } else if (this.issuances.length) {
495             this.load_entry_form(this.issuances[0]);
496         } else {
497             alert(S("issuance_lookup.none"));
498             this._init();
499         }
500
501     };
502
503     this.load_entry_form = function(issuance) {
504         if (typeof(issuance) == "undefined") {
505             var issuance_id = dojo.byId("issuance_chooser").value;
506             this.issuance = this.issuances.filter(
507                 function(o) { return o.id() == issuance_id; }
508             )[0];
509         } else {
510             this.issuance = issuance;
511         }
512
513         this._show_issuance_bits();
514         this._prepare_autogen_control();
515
516         busy(true);
517
518         fieldmapper.standardRequest(
519             ["open-ils.serial",
520                 "open-ils.serial.items.receivable.by_issuance.atomic"], {
521                 "params": [authtoken, this.issuance.id()],
522                 "async": true,
523                 "onresponse": function(r) {
524                     busy(false);
525
526                     if (list = openils.Util.readResponse(r, false, true)) {
527
528                         if (list.length) {
529                             busy(true);
530                             show("form_holder");
531
532                             list.forEach(function(o) {self.add_entry_row(o);});
533                             if (list.length > 1) {
534                                 self.build_batch_entry_row();
535                                 show("batch_receive_entry");
536                             }
537
538                             busy(false);
539                         } else {
540                             alert(S("item_lookup.none"));
541                             if (self.issuances.length) self.choose_issuance();
542                             else self._init();
543                         }
544                     }
545                 }
546             }
547         );
548
549     };
550
551     this.build_batch_entry_row = function() {
552         var row = dojo.byId("entry_batch_row");
553
554         this.batch_controls = {};
555
556         node_by_name("note", row).appendChild(
557             this.batch_controls.note = dojo.create("textbox", {"size": 20})
558         );
559
560         node_by_name("copy_loc", row).appendChild(
561             this.batch_controls.copy_loc = this._build_copy_loc_dropdown(
562                 /* XXX is 1 really the right value below? */
563                 this._get_copy_locs_for_lib(1),
564                 true /* add_unset_value */
565             )
566         );
567
568         node_by_name("circ_mod", row).appendChild(
569             this.batch_controls.circ_mod = this._extend_circ_mod_for_batch(
570                 this._build_circ_mod_dropdown()
571             )
572         );
573
574         node_by_name("price", row).appendChild(
575             this.batch_controls.price = dojo.create("textbox", {"size": 9})
576         );
577
578         node_by_name("apply", row).appendChild(
579             dojo.create("button", {
580                 "label": S("apply"),
581                 "oncommand": function() { self.apply_batch_values(); }
582             })
583         );
584     };
585
586     this.apply_batch_values = function() {
587         var row = dojo.byId("entry_batch_row");
588
589         for (var key in this.batch_controls) {
590             var value = this.batch_controls[key].value;
591             if (value != "" && value != -1)
592                 this._set_all_enabled_rows(key, value);
593         }
594     };
595
596     this.add_entry_row = function(item) {
597         this.item_cache[item.id()] = item;
598         var row = this.rows[item.id()] = dojo.clone(this.template);
599
600         function n(s) { return node_by_name(s, row); }    /* typing saver */
601
602         n("holding_lib").appendChild(
603             T(item.stream().distribution().holding_lib().shortname())
604         );
605
606         n("barcode").appendChild(
607             dojo.create(
608                 "textbox", {
609                     "size": 15,
610                     "tabindex": 10000 + Number(item.id()), /* is this right? */
611                     "onchange": function() {
612                         self.autogen_if_appropriate(this, item.id());
613                     }
614                 }
615             )
616         );
617
618         n("copy_loc").appendChild(
619             this._build_copy_loc_dropdown(
620                 this._get_copy_locs_for_lib(
621                     item.stream().distribution().holding_lib().id()
622                 )
623             )
624         );
625
626         n("note").appendChild(dojo.create("textbox", {"size": 20}));
627         n("circ_mod").appendChild(this._build_circ_mod_dropdown());
628         n("price").appendChild(dojo.create("textbox", {"size": 9}));
629         n("receive").appendChild(this._build_receive_toggle(item));
630
631         this.entry_tbody.appendChild(row);
632     };
633
634     this.receive = function() {
635         var recv_ids = [];
636         for (var id in this.rows) {
637             /* XXX TODO: get field values, send to ML,
638              * and yes do trimming here. */
639             if (!this._row_disabled(id)) recv_ids.push(id);
640         }
641
642         busy(true);
643         fieldmapper.standardRequest(
644             ["open-ils.serial", "open-ils.serial.items.receive_by_id"], {
645                 "params": [authtoken, recv_ids],
646                 "async": true,
647                 "oncomplete": function(r) {
648                     try {
649                         while (item = openils.Util.readResponse(r))
650                             self.finish_receipt(item);
651                     } catch (E) {
652                         alert(E);
653                     }
654                     busy(false);
655                 }
656             }
657         );
658     };
659
660     this.finish_receipt = function(item) {
661         dojo.destroy(this.rows[item.id()]);
662         delete this.rows[item.id()];
663         delete this.item_cache[item.id()];
664     };
665
666     this.autogen_if_appropriate = function(textbox, item_id) {
667         if (this._user_wants_autogen() && textbox.value) {
668             var [list, question] = this._get_autogen_potentials(item_id);
669             if (list.length) {
670                 if (question && !confirm(S("autogen_barcodes.questionable")))
671                     return;
672
673                 busy(true);
674                 try {
675                     fieldmapper.standardRequest(
676                         ["open-ils.cat", "open-ils.cat.item.barcode.autogen"], {
677                             "params": [authtoken, textbox.value, list.length],
678                             "async": false,
679                             "onresponse": function(r) {
680                                 r = openils.Util.readResponse(r, false, true);
681                                 if (r) {
682                                     for (var i = 0; i < r.length; i++) {
683                                         var row = self.rows[list[i]];
684                                         self._row_field_value(
685                                             row, "barcode", r[i]
686                                         );
687                                         row._has_autogen_barcode = true;
688                                     }
689                                 }
690                             }
691                         }
692                     );
693                 } catch (E) {
694                     alert(E);
695                 }
696                 busy(false);
697             } /* do nothing for empty list */
698         }
699     };
700
701     this._init.apply(this, arguments);
702 }
703
704 function my_init() {
705     var cgi = new openils.CGI();
706
707     authtoken = (typeof ses == "function" ? ses() : 0) ||
708         cgi.param("ses") || dojo.cookie("ses");
709
710     batch_receiver = new BatchReceiver(cgi.param("docid") || null);
711 }