]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/xul/staff_client/server/serial/batch_receive.js
Serials: closer to full working receiving in the batch receive interface
[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._location_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                 hard_empty(this.entry_tbody);
90             }
91             /* XXX incredibly, running hard_empty() more than once seems to be
92              * good and necessary.  There's a bug under the covers somewhere,
93              * but this keeps it out of sight for the moment. */
94              hard_empty(this.entry_tbody);
95         }
96         hard_empty(this.entry_tbody);
97
98         this.rows = {};
99         this.item_cache = {};
100
101         if (bib_id)
102             this.bib_lookup(bib_id, null, true);
103
104         busy(false);
105     };
106
107     this._clear_entry_batch_row = function() {
108         dojo.forEach(
109             dojo.byId("entry_batch_row").childNodes,
110             function(node) {
111                 if (node.nodeType == 1 &&
112                     node.getAttribute("name") != "barcode")
113                     hard_empty(node);
114             }
115         );
116     };
117
118     this._show_bibdata_bits = function() {
119         hard_empty("title_here");
120         dojo.byId("title_here").appendChild(T(this.bibdata.mvr.title()));
121         hard_empty("author_here");
122
123         if (this.bibdata.mvr.author()) {
124             dojo.byId("author_here").appendChild(T(this.bibdata.mvr.author()));
125             show("author_here_holder");
126         } else {
127             hide("author_here_holder");
128         }
129
130         show("batch_receive_bibdata_bits");
131     };
132
133     this._sub_label = function(sub) {
134         /* XXX use a formatting string from serial.properties */
135         return sub.id() + ": (" + sub.owning_lib().shortname() + ") " +
136             D(sub.start_date()) + " - " + D(sub.end_date());
137     };
138
139     this._show_sub_bits = function() {
140         hard_empty("sublabel_here");
141         dojo.place(
142             T(this._sub_label(this.sub)),
143             "sublabel_here",
144             "only"
145         );
146         hide("batch_receive_sub");
147         show("batch_receive_sub_bits");
148     };
149
150     this._show_issuance_bits = function() {
151         hide("batch_receive_issuance");
152         hard_empty("issuance_label_here");
153         dojo.place(
154             T(this.issuance.label()),
155             "issuance_label_here",
156             "only"
157         );
158         show("batch_receive_issuance_bits");
159     }
160
161     this._get_receivable_issuances = function() {
162         var issuances = [];
163
164         busy(true);
165         try {
166             fieldmapper.standardRequest(
167                 ["open-ils.serial", "open-ils.serial.issuances.receivable"], {
168                     "params": [authtoken, this.sub.id()],
169                     "async": false,
170                     "onresponse": function(r) {
171                         if (r = openils.Util.readResponse(r))
172                             issuances.push(r);
173                     }
174                 }
175             );
176         } catch (E) {
177             alert(E);
178         }
179         busy(false);
180
181         return issuances;
182     };
183
184     this._build_circ_modifier_dropdown = function() {
185         if (!this._built_circ_modifier_dropdown) {
186             var menulist = dojo.create("menulist");
187             var menupopup = dojo.create("menupopup", null, menulist, "only");
188             dojo.create(
189                 "menuitem", {"value": 0, "label": S("none")},
190                 menupopup, "first"
191             );
192
193             var mods = [];
194             fieldmapper.standardRequest(
195                 ["open-ils.circ", "open-ils.circ.circ_modifier.retrieve.all"],{
196                     "params": [{"full": true}],
197                     "async": false,
198                     "onresponse": function(r) {
199                         if (mods = openils.Util.readResponse(r)) {
200                             mods.sort(
201                                 function(a,b) {
202                                     return a.code() > b.code() ? 1 :
203                                         b.code() > a.code() ? -1 :
204                                         0;
205                                 }
206                             ).forEach(
207                                 function(mod) {
208                                     dojo.create(
209                                         "menuitem", {
210                                             "value": mod.code(),
211                                             /* XXX use format string */
212                                             "label": mod.code()+" "+mod.name()
213                                         }, menupopup, "last"
214                                     );
215                                 }
216                             );
217                         }
218                     }
219                 }
220             );
221             if (!mods.length) {
222                 /* in this case, discard menulist and menupopup */
223                 this._built_circ_modifier_dropdown =
224                     dojo.create("description", {"value": "-"});
225             } else {
226                 this._built_circ_modifier_dropdown = menulist;
227             }
228         }
229
230         return dojo.clone(this._built_circ_modifier_dropdown);
231     };
232
233     this._extend_circ_modifier_for_batch = function(control) {
234         dojo.create(
235             "menuitem", {"value": -1, "label": "---"},
236             dojo.query("menupopup", control)[0],
237             "first"
238         );
239         return control;
240     };
241
242     this._build_location_dropdown = function(locs, add_unset_value) {
243         var menulist = dojo.create("menulist");
244         var menupopup = dojo.create("menupopup", null, menulist, "only");
245
246         if (add_unset_value) {
247             dojo.create(
248                 "menuitem", {"value": -1, "label": "---"}, menupopup, "first"
249             );
250         }
251
252         locs.forEach(
253             function(loc) {
254                 dojo.create(
255                     "menuitem", {
256                         "value": loc.id(),
257                         "label": "(" + loc.owning_lib().shortname() + ") " +
258                             loc.name() /* XXX i18n */
259                     }, menupopup, "last"
260                 );
261             }
262         );
263
264         return menulist;
265     };
266
267     this._get_locations_for_lib = function(lib) {
268         if (!this._location_by_lib[lib]) {
269             fieldmapper.standardRequest(
270                 ["open-ils.circ", "open-ils.circ.copy_location.retrieve.all"],{
271                     "params": [lib, false, true],
272                     "async": false,
273                     "onresponse": function(r) {
274                         if (locs = openils.Util.readResponse(r))
275                             self._location_by_lib[lib] = locs;
276                     }
277                 }
278             );
279         }
280
281         return this._location_by_lib[lib];
282     };
283
284     this._build_receive_toggle = function(item) {
285         return dojo.create(
286             "checkbox", {
287                 "oncommand": function(ev) {
288                         self._disable_row(item.id(), !ev.target.checked);
289                 },
290                 "checked": "true"
291             }
292         );
293     }
294
295     this._disable_row = function(item_id, disabled) {
296         var row = this.rows[item_id];
297         dojo.query("textbox,menulist", row).forEach(
298             function(element) { element.disabled = disabled; }
299         );
300     };
301
302     this._row_disabled = function(row) {
303         if (typeof(row) == "string") row = this.rows[row];
304         return !dojo.query("checkbox", row)[0].checked;
305     };
306
307     this._row_field_value = function(row, field, value) {
308         if (typeof(row) == "string") row = this.rows[row];
309
310         var node = dojo.query("*", node_by_name(field, row))[0];
311
312         if (typeof(value) == "undefined")
313             return node.value;
314         else
315             node.value = value;
316     }
317
318         this._user_wants_autogen = function() {
319         return dojo.byId("autogen_barcodes").checked;
320     };
321
322     this._get_autogen_potentials = function(item_id) {
323         var hit_a_wall = false;
324
325         return [openils.Util.objectProperties(this.rows).sort(num_sort).filter(
326             function(id) {
327                 if (hit_a_wall) {
328                     return false;
329                 } else if (id <= item_id || self._row_disabled(id)) {
330                     return false;
331                 } else if (self._row_field_value(id, "barcode")) {
332                     hit_a_wall = true;
333                     return false;
334                 } else {
335                     return true;
336                 }
337             }
338         ), hit_a_wall];
339     };
340
341     this._prepare_autogen_control = function() {
342         dojo.attr("autogen_barcodes",
343             "command", function(ev) {
344                 if (!ev.target.checked) {
345                     var list = self._have_autogen_barcodes();
346                     if (list.length && confirm(S("autogen_barcodes.remove"))) {
347                         list.forEach(
348                             function(id) {
349                                 self._row_field_value(id, "barcode", "");
350                                 self.rows[id]._has_autogen_barcode = false;
351                             }
352                         );
353                     }
354                 }
355             }
356         );
357     };
358
359     this._have_autogen_barcodes = function() {
360         var list = [];
361         for (var id in this.rows)
362             if (this.rows[id]._has_autogen_barcode) list.push(id);
363         return list;
364     };
365
366     this._set_all_enabled_rows = function(key, value) {
367         /* do NOT do trimming here, set whitespace as is. */
368         for (var id in this.rows) {
369             if (!this._row_disabled(id))
370                 this._row_field_value(id, key, value);
371         }
372     };
373
374     this.bib_lookup = function(bib_search_term, evt, is_actual_id) {
375         if (evt && evt.keyCode != 13) return;
376
377         if (!bib_search_term) {
378             var bib_search_term = dojo.byId("bib_search_term").value.trim();
379             if (!bib_search_term.length) {
380                 alert(S("bib_lookup.empty"));
381                 return;
382             }
383         }
384
385         hide("batch_receive_sub");
386         hide("batch_receive_entry");
387
388         busy(true);
389         dojo.byId("bib_lookup_submit").disabled = true;
390         fieldmapper.standardRequest(
391             ["open-ils.serial",
392                 "open-ils.serial.biblio.record_entry.by_identifier.atomic"], {
393                 "params": [
394                     bib_search_term, {
395                         "require_subscriptions": true,
396                         "add_mvr": true,
397                         "is_actual_id": is_actual_id
398                     }
399                 ],
400                 "async": false,
401                 "oncomplete": function(r) {
402                     /* These two things better come before readResponse(),
403                      * which can throw exceptions. */
404                     busy(false);
405                     dojo.byId("bib_lookup_submit").disabled = false;
406
407                     var list = openils.Util.readResponse(r, false, true);
408                     if (list && list.length) {
409                         if (list.length > 1) {
410                             /* XXX TODO just let the user pick one from a list,
411                              * although this circumstance seems really
412                              * unlikely.  It just can't happen for TCN, and
413                              * wouldn't be likely for ISxN or UPC... ? */
414                             alert(S("bib_lookup.multiple"));
415                         } else {
416                             self.bibdata = list[0];
417                             self._show_bibdata_bits();
418                             self.choose_subscription();
419                         }
420                     } else {
421                         alert(S("bib_lookup.not_found"));
422                         if (is_actual_id) {
423                             self._init();
424                         } else {
425                             dojo.byId("bib_search_term").reset();
426                             dojo.byId("bib_search_term").focus();
427                         }
428                     }
429                 }
430             }
431         );
432     };
433
434     this.choose_subscription = function() {
435         hide("batch_receive_bib");
436         hide("batch_receive_entry");
437         hide("batch_receive_sub_bits");
438         hide("batch_receive_issuance");
439
440         var subs = this.bibdata.bre.subscriptions();
441
442         if (subs.length > 1) {
443             var menulist = dojo.create("menulist", {"id": "sub_chooser"});
444             var menupopup = dojo.create("menupopup", {}, menulist, "only");
445
446             this.bibdata.bre.subscriptions().forEach(
447                 function(sub) {
448                     dojo.create(
449                         "menuitem", {
450                             "label": self._sub_label(sub),
451                             "value": sub.id()
452                         }, menupopup, "last"
453                     );
454                 }
455             );
456
457             hard_empty(dojo.byId("sub_chooser_here"));
458
459             dojo.place(menulist, dojo.byId("sub_chooser_here"), "only");
460             show("batch_receive_sub");
461         } else {
462             this.choose_issuance(subs[0]);
463         }
464     };
465
466     this.choose_issuance = function(sub) {
467         hide("batch_receive_bib");
468         hide("batch_receive_entry");
469         hide("batch_receive_sub");
470
471         if (typeof(sub) == "undefined") {   /* sub chosen from menu */
472             var sub_id = dojo.byId("sub_chooser").value;
473             this.sub = this.bibdata.bre.subscriptions().filter(
474                 function(o) { return o.id() == sub_id; }
475             )[0];
476         } else {    /* only one sub possible, passed in directly */
477             this.sub = sub;
478         }
479
480         this._show_sub_bits();
481
482         this.issuances = this._get_receivable_issuances();   /* sync */
483
484         if (this.issuances.length > 1) {
485             var menulist = dojo.create("menulist", {"id": "issuance_chooser"});
486             var menupopup = dojo.create("menupopup", {}, menulist, "only");
487
488             this.issuances.sort(
489                 function(a, b) {
490                     if (a.date_published()>b.date_published()) return 1;
491                     else if (b.date_published()>a.date_published()) return -1;
492                     else return 0;
493                 }
494             ).forEach(
495                 function(issuance) {
496                     dojo.create(
497                         "menuitem", {
498                             "label": issuance.label(),
499                             "value": issuance.id()
500                         }, menupopup, "last"
501                     );
502                 }
503             );
504
505             hard_empty("issuance_chooser_here");
506             dojo.place(menulist, dojo.byId("issuance_chooser_here"), "only");
507
508             show("batch_receive_issuance");
509         } else if (this.issuances.length) {
510             this.load_entry_form(this.issuances[0]);
511         } else {
512             alert(S("issuance_lookup.none"));
513             this._init();
514         }
515
516     };
517
518     this.load_entry_form = function(issuance) {
519         if (typeof(issuance) == "undefined") {
520             var issuance_id = dojo.byId("issuance_chooser").value;
521             this.issuance = this.issuances.filter(
522                 function(o) { return o.id() == issuance_id; }
523             )[0];
524         } else {
525             this.issuance = issuance;
526         }
527
528         this._show_issuance_bits();
529         this._prepare_autogen_control();
530
531         busy(true);
532
533         fieldmapper.standardRequest(
534             ["open-ils.serial",
535                 "open-ils.serial.items.receivable.by_issuance.atomic"], {
536                 "params": [authtoken, this.issuance.id()],
537                 "async": true,
538                 "onresponse": function(r) {
539                     busy(false);
540
541                     if (list = openils.Util.readResponse(r, false, true)) {
542
543                         if (list.length) {
544                             busy(true);
545                             show("form_holder");
546
547                             list.forEach(function(o) {self.add_entry_row(o);});
548                             if (list.length > 1) {
549                                 self.build_batch_entry_row();
550                                 show("batch_receive_entry");
551                             }
552
553                             busy(false);
554                         } else {
555                             alert(S("item_lookup.none"));
556                             if (self.issuances.length) self.choose_issuance();
557                             else self._init();
558                         }
559                     }
560                 }
561             }
562         );
563
564     };
565
566     this.build_batch_entry_row = function() {
567         var row = dojo.byId("entry_batch_row");
568
569         this.batch_controls = {};
570
571         node_by_name("note", row).appendChild(
572             this.batch_controls.note = dojo.create("textbox", {"size": 20})
573         );
574
575         node_by_name("location", row).appendChild(
576             this.batch_controls.location = this._build_location_dropdown(
577                 /* XXX TODO build a smarter list. rather than all copy locs
578                  * under OU #1, try building a list of copy locs available to
579                  * all OUs represented in actual items */
580                 this._get_locations_for_lib(1),
581                 true /* add_unset_value */
582             )
583         );
584
585         node_by_name("circ_modifier", row).appendChild(
586             this.batch_controls.circ_modifier =
587                 this._extend_circ_modifier_for_batch(
588                     this._build_circ_modifier_dropdown()
589                 )
590         );
591
592         node_by_name("price", row).appendChild(
593             this.batch_controls.price = dojo.create("textbox", {"size": 9})
594         );
595
596         node_by_name("apply", row).appendChild(
597             dojo.create("button", {
598                 "label": S("apply"),
599                 "oncommand": function() { self.apply_batch_values(); }
600             })
601         );
602     };
603
604     this.apply_batch_values = function() {
605         var row = dojo.byId("entry_batch_row");
606
607         for (var key in this.batch_controls) {
608             var value = this.batch_controls[key].value;
609             if (value != "" && value != -1)
610                 this._set_all_enabled_rows(key, value);
611         }
612     };
613
614     this.add_entry_row = function(item) {
615         this.item_cache[item.id()] = item;
616         var row = this.rows[item.id()] = dojo.clone(this.template);
617
618         function n(s) { return node_by_name(s, row); }    /* typing saver */
619
620         n("holding_lib").appendChild(
621             T(item.stream().distribution().holding_lib().shortname())
622         );
623
624         n("barcode").appendChild(
625             dojo.create(
626                 "textbox", {
627                     "size": 15,
628                     "tabindex": 10000 + Number(item.id()), /* is this right? */
629                     "onchange": function() {
630                         self.autogen_if_appropriate(this, item.id());
631                     }
632                 }
633             )
634         );
635
636         n("location").appendChild(
637             this._build_location_dropdown(
638                 this._get_locations_for_lib(
639                     item.stream().distribution().holding_lib().id()
640                 )
641             )
642         );
643
644         n("note").appendChild(dojo.create("textbox", {"size": 20}));
645         n("circ_modifier").appendChild(this._build_circ_modifier_dropdown());
646         n("price").appendChild(dojo.create("textbox", {"size": 9}));
647         n("receive").appendChild(this._build_receive_toggle(item));
648
649         this.entry_tbody.appendChild(row);
650     };
651
652     this.receive = function() {
653         var items = [];
654         for (var id in this.rows) {
655             if (this._row_disabled(id)) 
656                 continue;
657
658             var item = this.item_cache[id];
659
660             var barcode = this._row_field_value(id, "barcode");
661             if (barcode) {
662                 var unit = new sunit();
663                 unit.barcode(barcode);
664
665                 ["price", "location", "circ_modifier"].forEach(
666                     function(field) {
667                         var value = self._row_field_value(id, field).trim();
668                         if (value) unit[field](value);
669                     }
670                 );
671
672
673                 item.unit(unit);
674             }
675
676             var note_value = this._row_field_value(id, "note").trim();
677             if (note_value) {
678                 var note = new sin();
679                 note.item(id);
680                 note.pub(false);
681                 note.title(S("receive_time_note"));
682                 note.value(note_value);
683
684                 item.notes([note]);
685             }
686
687             items.push(item);
688         }
689
690         busy(true);
691         fieldmapper.standardRequest(
692             ["open-ils.serial", "open-ils.serial.receive_items.one_unit_per"],{
693                 "params": [authtoken, items],
694                 "async": true,
695                 "oncomplete": function(r) {
696                     try {
697                         while (item_id = openils.Util.readResponse(r))
698                             self.finish_receipt(item_id);
699                     } catch (E) {
700                         alert(E);
701                     }
702                     busy(false);
703                 }
704             }
705         );
706     };
707
708     this.finish_receipt = function(item_id) {
709         hard_empty(this.rows[item_id]);
710         dojo.destroy(this.rows[item_id]);
711         delete this.rows[item_id];
712         delete this.item_cache[item_id];
713     };
714
715     this.autogen_if_appropriate = function(textbox, item_id) {
716         if (this._user_wants_autogen() && textbox.value) {
717             var [list, question] = this._get_autogen_potentials(item_id);
718             if (list.length) {
719                 if (question && !confirm(S("autogen_barcodes.questionable")))
720                     return;
721
722                 busy(true);
723                 try {
724                     fieldmapper.standardRequest(
725                         ["open-ils.cat", "open-ils.cat.item.barcode.autogen"], {
726                             "params": [authtoken, textbox.value, list.length],
727                             "async": false,
728                             "onresponse": function(r) {
729                                 r = openils.Util.readResponse(r, false, true);
730                                 if (r) {
731                                     for (var i = 0; i < r.length; i++) {
732                                         var row = self.rows[list[i]];
733                                         self._row_field_value(
734                                             row, "barcode", r[i]
735                                         );
736                                         row._has_autogen_barcode = true;
737                                     }
738                                 }
739                             }
740                         }
741                     );
742                 } catch (E) {
743                     alert(E);
744                 }
745                 busy(false);
746             } /* do nothing for empty list */
747         }
748     };
749
750     this._init.apply(this, arguments);
751 }
752
753 function my_init() {
754     var cgi = new openils.CGI();
755
756     authtoken = (typeof ses == "function" ? ses() : 0) ||
757         cgi.param("ses") || dojo.cookie("ses");
758
759     batch_receiver = new BatchReceiver(cgi.param("docid") || null);
760 }