]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/acq/common/li_table.js
LP1187016 Prevent new copies on activated POs
[working/Evergreen.git] / Open-ILS / web / js / ui / default / acq / common / li_table.js
1 dojo.require('dojo.date.locale');
2 dojo.require('dojo.date.stamp');
3 dojo.require('dijit.form.Button');
4 dojo.require('dijit.form.TextBox');
5 dojo.require('dijit.form.FilteringSelect');
6 dojo.require('dijit.form.Textarea');
7 dojo.require('dijit.Tooltip');
8 dojo.require('dijit.ProgressBar');
9 dojo.require('dojox.timing.doLater');
10 dojo.require('openils.acq.Lineitem');
11 dojo.require('openils.acq.PO');
12 dojo.require('openils.acq.Picklist');
13 dojo.require('openils.widget.AutoFieldWidget');
14 dojo.require('dojo.data.ItemFileReadStore');
15 dojo.require('openils.widget.ProgressDialog');
16 dojo.require('openils.PermaCrud');
17 dojo.require("openils.widget.PCrudAutocompleteBox");
18 dojo.require('dijit.form.ComboBox');
19 dojo.require('openils.CGI');
20
21 if (!localeStrings) {   /* we can do this because javascript doesn't have block scope */
22     dojo.requireLocalization('openils.acq', 'acq');
23     var localeStrings = dojo.i18n.getLocalization('openils.acq', 'acq');
24 }
25 const XUL_OPAC_WRAPPER = 'chrome://open_ils_staff_client/content/cat/opac.xul';
26 var li_exportable_attrs = ["issn", "isbn", "upc"];
27
28 var fundLabelFormat = [
29     '<span class="fund_${0}">${1} (${2})</span>', 'id', 'code', 'year'
30 ];
31 var fundSearchFormat = ['${0} (${1})', 'code', 'year'];
32
33 function nodeByName(name, context) {
34     return dojo.query('[name='+name+']', context)[0];
35 }
36
37 // for caching linked users.  e.g. lineitem_detail.receiver
38 var userCache = {};
39
40 var liDetailBatchFields = ['fund', 'owning_lib', 'location', 'collection_code', 'circ_modifier', 'cn_label'];
41 var liDetailFields = liDetailBatchFields.concat(['barcode', 'note']);
42 var fundStyles = {
43     "stop": "color: #c00; font-weight: bold;",
44     "warning": "color: #c93;"
45 };
46
47 function AcqLiTable() {
48
49     var self = this;
50     this.liCache = {};
51     this.plCache = {};
52     this.poCache = {};
53     this.relCache = {};
54     this.haveFundClass = {}
55     this.fundBalanceState = {};
56     this.realDfaCache = {};
57     this.virtDfaCounts = {};
58     this.virtDfaId = -1;
59     this.dfeOffset = 0;
60     this.claimEligibleLidByLi = {};
61     this.claimEligibleLid = {};
62     this.toggleState = false;
63     this.tbody = dojo.byId('acq-lit-tbody');
64     this.selectors = [];
65     this.noteAcks = {};
66     this.authtoken = openils.User.authtoken;
67     this.pcrud = new openils.PermaCrud();
68     this.rowTemplate = this.tbody.removeChild(dojo.byId('acq-lit-row'));
69     this.copyTbody = dojo.byId('acq-lit-li-details-tbody');
70     this.copyRow = this.copyTbody.removeChild(dojo.byId('acq-lit-li-details-row'));
71     this.copyBatchRow = dojo.byId('acq-lit-li-details-batch-row');
72     this.copyBatchWidgets = {};
73     this.liNotesTbody = dojo.byId('acq-lit-notes-tbody');
74     this.liNotesRow = this.liNotesTbody.removeChild(dojo.byId('acq-lit-notes-row'));
75     this.realCopiesTbody = dojo.byId('acq-lit-real-copies-tbody');
76     this.realCopiesRow = this.realCopiesTbody.removeChild(dojo.byId('acq-lit-real-copies-row'));
77     this._copy_fields_for_acqdf = ['owning_lib', 'location'];
78     this.skipInitialEligibilityCheck = false;
79     this.claimDialog = new ClaimDialogManager(
80         liClaimDialog, finalClaimDialog, this.claimEligibleLidByLi,
81         function(li) {    /* callback that fires when claims are made */
82             self.fetchClaimInfo(li.id(), /* force update */ true);
83         }
84     );
85     this.vlAgent = new VLAgent();
86
87     if (dojo.byId('acq-lit-apply-idents')) {
88         dojo.byId('acq-lit-apply-idents').onclick = function() {
89             self.applyOrderIdentValues();
90         };
91     }
92
93     this.focusLineitem = new openils.CGI().param('focus_li');
94
95     // capture the inline copy display wrapper and row template
96     this.inlineCopyContainer = 
97         this.tbody.removeChild(dojo.byId('acq-inline-copies-row'));
98     var tb = dojo.query(
99         '[name=acq-li-inline-copies-tbody]', this.inlineCopyContainer)[0];
100     this.inlineCopyTemplate = tb.removeChild(
101         dojo.query('[name=acq-li-inline-copies-template]', tb)[0]);
102     this.inlineNoCopies = tb.removeChild(
103         dojo.query('[name=acq-li-inline-copies-none]', tb)[0]);
104
105     // list of LI IDs that should be refreshed at next display time
106     this.inlineCopiesNeedingRefresh = []; 
107
108     dojo.byId("acq-lit-li-actions-selector").onchange = function() { 
109         self.applySelectedLiAction(this.options[this.selectedIndex].value);
110         this.selectedIndex = 0;
111     };
112
113     acqLitCreatePoSubmit.onClick = function() {
114         if (!self.createPoProviderSelector.attr("value") ||
115                 !self.createPoAgencySelector.attr("value")) {
116             alert(localeStrings.CREATE_PO_INVALID);
117             return false;
118         } else if (self._confirmPoPrepaySituation()) {
119             acqLitPoCreateDialog.hide();
120             self._createPO(acqLitPoCreateDialog.getValues());
121         } else {
122             return false;
123         }
124     }
125
126     acqLitSavePlButton.onClick = function() {
127         acqLitSavePlDialog.hide();
128         self._savePl(acqLitSavePlDialog.getValues());
129     }
130
131     acqLitCancelLiStateButton.onClick = function() {
132         acqLitChangeLiStateDialog.hide();
133     }
134     acqLitSaveLiStateButton.onClick = function() {
135         acqLitChangeLiStateDialog.hide();
136         self._updateLiState(acqLitChangeLiStateDialog.getValues(), acqLitChangeLiStateDialog.attr('state'));
137     }
138
139
140     dojo.byId('acq-lit-select-toggle').onclick = function(){self.toggleSelect()};
141     dojo.byId('acq-inline-copies-toggle').onclick = function(){self.toggleInlineCopies()};
142     dojo.byId('acq-lit-info-back-button').onclick = function(){self.show('list')};
143     dojo.byId('acq-lit-copies-back-button').onclick = function(){self.show('list')};
144     dojo.byId('acq-lit-notes-back-button').onclick = function(){self.show('list')};
145     dojo.byId('acq-lit-real-copies-back-button').onclick = function(){self.show('list')};
146
147     this.afwCopyFieldArgs = function(field, perms) {
148         return {
149                 "fmField" : field,
150                 "fmClass": 'acqlid',
151                 "labelFormat": (field == 'fund') ? fundLabelFormat : null,
152                 "searchFormat": (field == 'fund') ? fundSearchFormat : null,
153                 "searchFilter": (field == 'fund') ? {"active": "t"} : null,
154                 "orgLimitPerms": [perms],
155                 "dijitArgs": {
156                     "required": false,
157                     "labelType": (field == "fund") ? "html" : null
158                 },
159                 "noCache": (field == "fund"),
160                 "forceSync": true
161             };
162     };
163
164     /* This is the "new" batch updater that sits atop all lineitems. It does
165      * use this.afwCopyFieldArgs() to borrow a little common code  from the
166      * "old" batch updater atop the copy details view. */
167     this.initBatchUpdater = function(disabled_fields) {
168         openils.Util.show("acq-batch-update", "table");
169
170         if (!dojo.isArray(disabled_fields)) disabled_fields = [];
171
172         /* Note that this will directly contain dijits, not the AutoWidget
173          * wrapper object. */
174         this.batchUpdateWidgets = {};
175
176         this.batchUpdateWidgets.item_count = new dijit.form.TextBox(
177             {
178                 "style": {"width": "3em"},
179                 "disabled": Boolean(
180                     dojo.indexOf(disabled_fields, "item_count") != -1
181                 )
182             },
183             "acq-bu-item_count"
184         );
185
186         (new openils.widget.AutoFieldWidget({
187             "fmClass": "acqdf",
188             "selfReference": true,
189             "dijitArgs": { "required": false },
190             "forceSync": true,
191             "parentNode": "acq-bu-distribution_formula"
192         })).build(
193             function(w) {
194                 dojo.style(w.domNode, {"width": "12em"});
195                 /* dijitArgs to AutoFieldWidget won't work for 'disabled' */
196                 w.attr(
197                     "disabled",
198                     dojo.indexOf(disabled_fields, "distribution_formula") != -1
199                 );
200                 self.batchUpdateWidgets.distribution_formula = w;
201             }
202         );
203
204         dojo.forEach(
205             ["owning_lib","location","collection_code","circ_modifier","fund"],
206             function(field) {
207                 var args = self.afwCopyFieldArgs(field,"CREATE_PURCHASE_ORDER");
208                 args.parentNode = dojo.byId("acq-bu-" + field);
209
210                 (new openils.widget.AutoFieldWidget(args)).build(
211                     function(w, aw) {
212                         if (field == "fund") {
213                             dojo.connect(
214                                 w, "onChange", function(val) {
215                                     self._updateFundSelectorStyle(aw, val);
216                                 }
217                             );
218                             if (w.store)
219                                 self._ensureCSSFundClasses(w.store);
220                         }
221
222                         dojo.style(w.domNode, {"width": "10em"});
223                         w.attr(
224                             "disabled",
225                             dojo.indexOf(disabled_fields, field) != -1
226                         );
227                         self.batchUpdateWidgets[field] = w;
228                     }
229                 );
230             }
231         );
232
233         acqBatchUpdateApply.onClick = function() {
234             var li_id_list = self.getSelected(false, null, true /* id list */);
235             if (!li_id_list.length) {
236                 alert(localeStrings.NO_LI_TO_UPDATE);
237                 return;
238             }
239
240             progressDialog.show(true);
241             progressDialog.attr("title", localeStrings.LI_BATCH_UPDATE);
242             progressDialog.update({"maximum": li_id_list.length,"progress": 0});
243
244             var count = 0;
245
246             var params = [ self.authtoken, {"lineitems": li_id_list},
247                         self.batchUpdateChanges(), self.batchUpdateFormula() ];
248             console.log("batch update params: " + dojo.toJson(params));
249
250             fieldmapper.standardRequest(
251                 ["open-ils.acq", "open-ils.acq.lineitem.batch_update"], {
252                     "async": true,
253                     "params": params,
254                     "onresponse": function(r) {
255                         if ((r = openils.Util.readResponse(r))) { // assignment
256                             progressDialog.update({"progress": ++count});
257                         } else {
258                             progressDialog.hide();
259                             progressDialog.attr("title", "");
260                         }
261                     },
262                     "oncomplete": function() {
263                         /* XXX Is the last call to onresponse guaranteed to
264                          * finish before oncomplete is fired? */
265                         if (count != li_id_list.length) {
266                             console.error("lineitem batch update operation failed");
267                             progressDialog.hide();
268                             progressDialog.attr("title", "");
269                         } else {
270                             location.href = location.href;
271                         }
272                     }
273                 }
274             );
275         };
276     };
277
278     this.batchUpdateChanges = function() {
279         var o = {};
280
281         dojo.forEach(
282             openils.Util.objectProperties(this.batchUpdateWidgets),
283             function(k) {
284                 if (k == "distribution_formula") return; /* handled elsewhere */
285                 if (self.batchUpdateWidgets[k].attr("disabled")) return;
286
287                 /* It's important that a value of "" should mean that a field
288                  * doesn't get used in the arguments to the batch updater API,
289                  * but 0 should mean an actual 0. */
290                 var value = self.batchUpdateWidgets[k].attr("value");
291                 if (value !== "")
292                     o[k] = value;
293             }
294         );
295
296         return o;
297     };
298
299     this.batchUpdateFormula = function() {
300         if (this.batchUpdateWidgets.distribution_formula.attr("disabled")) {
301             return null;
302         } else {
303             return (
304                 this.batchUpdateWidgets.distribution_formula.attr("value") ||
305                 null
306             );
307         }
308     };
309
310     this.reset = function(keep_selectors) {
311         while(self.tbody.childNodes[0])
312             self.tbody.removeChild(self.tbody.childNodes[0]);
313         self.noteAcks = {};
314         self.relCache = {};
315
316         if (!keep_selectors)
317             self.selectors = [];
318     };
319     
320     this.setNext = function(handler) {
321         var link = dojo.byId('acq-lit-next');
322         if(handler) {
323             dojo.style(link, 'visibility', 'visible');
324             link.onclick = handler;
325         } else {
326             dojo.style(link, 'visibility', 'hidden');
327         }
328     };
329
330     this.setPrev = function(handler) {
331         var link = dojo.byId('acq-lit-prev');
332         if(handler) {
333             dojo.style(link, 'visibility', 'visible'); 
334             link.onclick = handler; 
335         } else {
336             dojo.style(link, 'visibility', 'hidden');
337         }
338     };
339
340     this.enableActionsDropdownOptions = function(mask) {
341         /* 'mask' is probably a misnomer the way I'm using it, but it needs to
342          * be one of pl,po,ao,gs,vp, or fs. */
343         dojo.query("option", "acq-lit-li-actions-selector").forEach(
344             function(option) {
345                 var opt_mask = dojo.attr(option, "mask");
346
347                 /* For each <option> element, an empty or non-existent mask
348                  * attribute, a mask attribute of "*", or a mask attribute that
349                  * matches this method's argument should result in that
350                  * option's being enabled. */
351                 dojo.attr(
352                     option, "disabled", !(
353                         !opt_mask ||
354                         opt_mask == "*" ||
355                         opt_mask.search(mask) != -1
356                     )
357                 );
358             }
359         );
360     };
361
362     /*
363      * Ensures this.focusLineitem is in view and causes a brief 
364      * border around the lineitem to come to life then fade.
365      */
366     this.focusLi = function() {
367         if (!this.focusLineitem) return;
368
369         // set during addLineitem()
370         var node = dojo.byId('li-title-ref-' + this.focusLineitem);
371
372         // LI may not yet be rendered
373         if (!node) return; 
374
375         // prevent numerous re-focuses
376         this.focusLineitem = null; 
377         
378         // causes the full row to be visible
379         dijit.scrollIntoView(node);
380
381         // may as well..
382         dojo.query('[attr=title]', node)[0].focus();
383
384         dojo.require('dojox.fx');
385
386         setTimeout(
387             function() {
388                 dojox.fx.highlight({color : '#BB4433', node : node, duration : 2000}).play();
389             }, 
390         100);
391     };
392
393     this.show = function(div) {
394         openils.Util.hide('acq-lit-table-div');
395         openils.Util.hide('acq-lit-info-div');
396         openils.Util.hide('acq-lit-li-details');
397         openils.Util.hide('acq-lit-notes-div');
398         openils.Util.hide('acq-lit-real-copies-div');
399         openils.Util.hide('acq-lit-asset-creator');
400         switch(div) {
401             case 'list':
402                 openils.Util.show('acq-lit-table-div');
403                 this.focusLi();
404                 this.refreshInlineCopies();
405                 break;
406             case 'info':
407                 openils.Util.show('acq-lit-info-div');
408                 break;
409             case 'copies':
410                 openils.Util.show('acq-lit-li-details');
411                 break;
412             case 'real-copies':
413                 openils.Util.show('acq-lit-real-copies-div');
414                 break;
415             case 'notes':
416                 openils.Util.show('acq-lit-notes-div');
417                 break;
418             case 'asset-creator':
419                 openils.Util.show('acq-lit-asset-creator');
420                 break;
421             default:
422                 if(div) 
423                     openils.Util.show(div);
424         }
425     }
426
427     this.hide = function() {
428         this.show(null);
429     }
430
431     this.toggleSelect = function() {
432         if(self.toggleState) 
433             dojo.forEach(self.selectors, function(i){i.checked = false});
434         else 
435             dojo.forEach(self.selectors, function(i){i.checked = true});
436         self.toggleState = !self.toggleState;
437     };
438
439
440     this.getAll = function(callback, id_only) {
441         /* For some uses of the li table, we may not really know about "all"
442          * the lineitems that the user thinks we know about. If we're a paged
443          * picklist, for example, we only know about the lineitems we've
444          * displayed, but not necessarily all the lineitems on the picklist.
445          * So we reach out to pcrud to inform us.
446          */
447
448         var oncomplete = function(r) {
449             var id_list = openils.Util.readResponse(r);
450             if (id_only)
451                 callback(id_list);
452             else
453                 self.fetchLineitemsById(id_list, callback);
454         };
455
456         if (this.isPL) {
457             this.pcrud.search(
458                 "jub", {"picklist": this.isPL}, {
459                     "id_list": true,    /* sic, even if id_only */
460                     "async": true,
461                     "oncomplete": oncomplete
462                 }
463             );
464             return;
465         } else if (this.isPO) {
466             this.pcrud.search(
467                 "jub", {"purchase_order": this.isPO}, {
468                     "id_list": true,
469                     "async": true,
470                     "oncomplete": oncomplete
471                 }
472             );
473             return;
474         } else if (this.isUni && this.pager) {
475             this.pager.getAllLineitemIDs(oncomplete);
476             return;
477         }
478
479         /* If execution reaches this point, we don't need or can't perform
480          * any special tricks to find out the "real" list of "all" lineitems
481          * in this context, so we fall back to the old method.
482          */
483         callback(this.getSelected(true, null, id_only));
484     };
485
486     /** @param all If true, assume all are selected */
487     this.getSelected = function(
488         all,
489         callback /* If you want a "good" idea of "all" lineitems, you must
490         provide a callback that accepts an array parameter, rather than
491         relying on the return value of this method itself. */,
492         id_only
493     ) {
494         if (all && callback)
495             return this.getAll(callback, id_only);
496
497         var indices = {};   /* use to uniqify. needed in paging situations. */
498         dojo.forEach(this.selectors,
499             function(i) { 
500                 if(i.checked || all)
501                     indices[i.parentNode.parentNode.getAttribute('li')] = true;
502             }
503         );
504
505         var result = openils.Util.objectProperties(indices);
506
507         if (!id_only)
508             result = result.map(function(liId) { return self.liCache[liId]; });
509
510         if (callback)
511             callback(result);
512         else
513             return result;
514     };
515
516     this.setRowAttr = function(td, liWrapper, field, type) {
517         var val = liWrapper.findAttr(field, type || 'lineitem_marc_attr_definition') || '';
518         td.appendChild(document.createTextNode(val));
519     };
520
521     this.setClaimPolicyControl = function(li, row) {
522         if (!self._claimPolicyPickerLoading) {
523             self._claimPolicyPickerLoading = true;
524
525             new openils.widget.AutoFieldWidget({
526                 "parentNode": "acq-lit-li-claim-policy",
527                 "fmClass": "acqclp",
528                 "selfReference": true,
529                 "dijitArgs": {"required": true}
530             }).build(
531                 function(w) { self.claimPolicyPicker = w; }
532             );
533         }
534
535         /* dojox.timing.doLater() is the best thing ever. Resource not yet
536          * ready? Just repeat my whole method when it is. */
537         if (dojox.timing.doLater(self.claimPolicyPicker)) {
538             return;
539         } else {
540             if (!row)
541                 row = self._findLiRow(li);
542
543             if (li.claim_policy()) {
544                 /* This Dojo data dance is necessary to get a whole fieldmapper
545                  * object based on a claim policy ID, since we alreay have the
546                  * widget thing loaded with all that data, and can thereby
547                  * avoid another request to the server. */
548                 self.claimPolicyPicker.store.fetchItemByIdentity({
549                     "identity": li.claim_policy(),
550                     "onItem": function(a) {
551                         var policy = (new acqclp()).fromStoreItem(a);
552                         var span = nodeByName("claim_policy", row);
553                         var inner = nodeByName("claim_policy_name", row);
554
555                         openils.Util.show(span, "inline");
556                         inner.innerHTML = policy.name();
557                     },
558                     "onError": function(e) {
559                         console.error(e);
560                     }
561                 });
562             } else {
563                 openils.Util.hide(nodeByName("claim_policy", row));
564                 nodeByName("claim_policy_name", row).innerHTML = "";
565             }
566         }
567     };
568
569     this.fetchClaimInfo = function(liId, force, callback, row) {
570         this._fetchLineitem(
571             liId, function(full) {
572                 self.liCache[full.id()] = full;
573                 self.checkClaimEligibility(full, callback, row);
574             }, force
575         );
576     }
577
578     // fetch an updated copy of the lineitem 
579     // and add it back to the lineitem table
580     this.refreshLineitem = function(li, focus) {
581         var self = this;
582         this._fetchLineitem(li.id(), 
583             function(newLi) {
584                 if (focus) {
585                     self.focusLineitem = li.id();
586                 } else {
587                     self.focusLineitem = null;
588                 }
589                 var row = dojo.query('[li='+li.id()+']', self.tbody)[0];
590                 var nextSibling = row.nextSibling;
591                 self.tbody.removeChild(row);
592                 self.addLineitem(newLi, false, nextSibling);
593             }, true
594         );
595     }
596
597     /**
598      * Inserts a single lineitem into the growing table of lineitems
599      * @param {Object} li The lineitem object to insert
600      */
601     this.addLineitem = function(li, skip_final_placement, nextSibling) {
602         this.liCache[li.id()] = li;
603
604         // insert the row right away so that final order isn't
605         // dependent on how long subsequent async request take
606         // for a given line item
607         var row = self.rowTemplate.cloneNode(true);
608         if (!skip_final_placement) {
609             if (!nextSibling) {
610                 // either no nextSibling was provided or it was null
611                 // meaning the row was already at the end of the table
612                 self.tbody.appendChild(row);
613             } else {
614                 self.tbody.insertBefore(row, nextSibling);
615             }
616         }
617
618         self.selectors.push(dojo.query('[name=selectbox]', row)[0]);
619
620         // sort the lineitem notes on edit_time
621         if(!li.lineitem_notes()) li.lineitem_notes([]);
622
623         var liWrapper = new openils.acq.Lineitem({lineitem:li});
624         row.setAttribute('li', li.id());
625         var tds = dojo.query('[attr]', row);
626         dojo.forEach(tds, function(td) {self.setRowAttr(td, liWrapper, td.getAttribute('attr'), td.getAttribute('attr_type'));});
627         dojo.query('[name=source_label]', row)[0].appendChild(document.createTextNode(li.source_label()));
628
629         // so we can scroll to it later
630         dojo.query('[name=bib-info-cell]', row)[0].id = 'li-title-ref-' + li.id();
631
632         var identifier =
633             liWrapper.findAttr("isbn", "lineitem_marc_attr_definition") ||
634             liWrapper.findAttr("upc", "lineitem_marc_attr_definition");
635
636         // XXX media prefix for added content
637         if (identifier) {
638             nodeByName("jacket", row).setAttribute(
639                 "src", "/opac/extras/ac/jacket/small/" + identifier
640             );
641         }
642
643         nodeByName("liid", row).innerHTML += li.id();
644
645         if(li.eg_bib_id()) {
646             openils.Util.show(nodeByName('catalog', row), 'inline');
647             nodeByName("catalog_link", row).onclick = this.generateMakeRecTab(li.eg_bib_id());
648         } else {
649             openils.Util.show(nodeByName('link_to_catalog', row), 'inline');
650             nodeByName("link_to_catalog_link", row).onclick = function() { self.drawBibFinder(li) };
651         }
652
653         if (li.queued_record()) {
654             this.pcrud.retrieve('vqbr', li.queued_record(),
655                 {   async : true, 
656                     oncomplete : function(r) {
657                         var qrec = openils.Util.readResponse(r);
658                         openils.Util.show(nodeByName('queue', row), 'inline');
659                         var link = nodeByName("queue_link", row);
660                         link.onclick = function() { 
661                             // open a new tab to the vandelay queue for this record
662                             openils.XUL.newTabEasy(
663                                 oilsBasePath + '/vandelay/vandelay?qtype=bib&qid=' + qrec.queue()
664                             );
665                         }
666                     }
667                 }
668             );
669         }
670
671         nodeByName("worksheet_link", row).href =
672             oilsBasePath + "/acq/lineitem/worksheet/" + li.id() + 
673             '?source=' + encodeURIComponent(location.pathname + location.search)
674
675         nodeByName("show_requests_link", row).href =
676             oilsBasePath + "/acq/picklist/user_request?lineitem=" + li.id() + 
677             '?source=' + encodeURIComponent(location.pathname + location.search)
678
679         dojo.query('[attr=title]', row)[0].onclick = function() {self.drawInfo(li.id())};
680         dojo.query('[name=copieslink]', row)[0].onclick = function() {self.drawCopies(li.id())};
681         dojo.query('[name=noteslink]', row)[0].onclick = function() {self.drawLiNotes(li)};
682         dojo.query('[name=expand_inline_copies]', row)[0].onclick = 
683             function() {self.drawInlineCopies(li.id())};
684
685         this.drawOrderIdentSelector(li, row);
686
687         if (!this.skipInitialEligibilityCheck)
688             this.fetchClaimInfo(
689                 li.id(),
690                 false,
691                 function(full) { self.setClaimPolicyControl(full, row) },
692                 row
693             );
694
695         this.updateLiNotesCount(li, row);
696
697         // show which PO this lineitem is a member of
698         if(li.purchase_order() && !this.isPO) {
699             var po = 
700                 this.poCache[li.purchase_order()] =
701                 this.poCache[li.purchase_order()] ||
702                 fieldmapper.standardRequest(
703                     ['open-ils.acq', 'open-ils.acq.purchase_order.retrieve'],
704                     {params: [
705                         this.authtoken, li.purchase_order(), {
706                             "flesh_price_summary": true,
707                             "flesh_provider" : true,
708                             "flesh_lineitem_count": true
709                         }
710                     ]});
711             if(po && !this.isMeta) {
712                 openils.Util.show(nodeByName('po', row), 'inline');
713                 var link = nodeByName('po_link', row);
714                 link.setAttribute('href', oilsBasePath +
715                     '/acq/po/view/' + li.purchase_order() +
716                     '?focus_li=' + li.id() +
717                     '&source=' + encodeURIComponent(location.pathname + location.search)
718                 );
719                 link.innerHTML += po.name();
720
721                 openils.Util.show(nodeByName('pro', row), 'inline');
722                 link = nodeByName('pro_link', row);
723                 link.setAttribute('href', oilsBasePath + '/conify/global/acq/provider/' + po.provider().id())
724                 link.innerHTML += po.provider().code();
725             }
726         }
727
728         // show which picklist this lineitem is a member of
729         if(li.picklist() && (this.isPO || this.isMeta || this.isUni)) {
730             var pl = 
731                 this.plCache[li.picklist()] = 
732                 this.plCache[li.picklist()] || 
733                 fieldmapper.standardRequest(
734                     ['open-ils.acq', 'open-ils.acq.picklist.retrieve.authoritative'],
735                     {params: [this.authtoken, li.picklist()]});
736             if (pl) {
737                 if (pl.name() == "") {
738                     openils.Util.show(nodeByName("bib_origin", row), "inline");
739
740                 } else {
741
742                     openils.Util.show(nodeByName('pl', row), 'inline');
743                     var link = nodeByName('pl_link', row);
744                     link.setAttribute('href', oilsBasePath +
745                         '/acq/picklist/view/' + li.picklist() +
746                         '?focus_li=' + li.id() +
747                         '&source=' + encodeURIComponent(location.pathname + location.search)
748                     );
749                     link.innerHTML += pl.name();
750                 }
751             }
752         }
753
754         var countNode = nodeByName('count', row);
755         var count = li.item_count() || 0;
756         if (typeof(this._copy_count_cb) == "function") {
757             this._copy_count_cb(li.id(), count);
758         }
759         countNode.innerHTML = count;
760         countNode.id = 'acq-lit-copy-count-label-' + li.id();
761
762         // lineitem price
763         var priceInput = dojo.query('[name=price]', row)[0];
764         priceInput.value = li.estimated_unit_price() || '';
765         priceInput.onchange = function() { self.updateLiPrice(priceInput, li) };
766
767         // show either "mark received" or "unreceive" as appropriate
768         this.updateLiState(li, row);
769
770         if (skip_final_placement) {
771             return row;
772         }
773
774         // the last LI may be rendered after the call to show('list'),
775         // so make sure it's focused if necessary.
776         if (this.focusLineitem == li.id())
777             this.focusLi();
778     };
779
780     this._liCountClaims = function(li) {
781         var total = 0;
782         for (var i = 0; i < li.lineitem_details().length; i++)
783             total += li.lineitem_details()[i].claims().length;
784         return total;
785     };
786
787     this._findLiRow = function(li) {
788         return dojo.query('tr[li="' + li.id() + '"]', "acq-lit-tbody")[0];
789     };
790
791     this.reconsiderClaimControl = function(li, row) {
792         if (!row) row = this._findLiRow(li);
793         var option = nodeByName("action_manage_claims", row);
794         var eligible = this.claimEligibleLidByLi[li.id()].length;
795         var count = this._liCountClaims(li);
796
797         option.disabled = !(count || eligible);
798         option.innerHTML =
799             dojo.string.substitute(localeStrings.NUM_CLAIMS_EXISTING, [count]);
800         option.onclick = function() { self.claimDialog.show(li); };
801     };
802
803     this.clearEligibility = function(li) {
804         this.claimEligibleLidByLi[li.id()] = [];
805
806         if (li.lineitem_details()) {
807             li.lineitem_details().forEach(
808                 function(lid) { delete self.claimEligibleLid[lid.id()]; }
809             );
810         }
811
812         if (this.copyCache) {
813             var to_del = [];
814             for (var k in this.copyCache) {
815                 if (this.copyCache[k].lineitem() == li.id())
816                     to_del.push(k);
817             }
818             to_del.forEach(
819                 function(k) { delete self.claimEligibleLid[k]; }
820             );
821         }
822     };
823
824     this.applyOrderIdentValues = function() {
825         this._identValuesInFlight = 
826             openils.Util.objectProperties(this.liCache).length;
827         for (var liId in this.liCache) {
828             this._applyOrderIdentValue(this.liCache[liId]);
829         }
830     };
831
832     // returns true if request was sent
833     this._applyOrderIdentValue = function(li, oncomplete) {
834         var self = this;
835
836         console.log('applying ident value for lineitem ' + li.id());
837
838         // main row
839         var row = dojo.query('[li=' + li.id() + ']')[0];
840
841         // find the selected ident value
842         var typeSel = dojo.query('[name=order_ident_type]', row)[0];
843         var valueSel = dojo.query('[name=order_ident_value]', row)[0];
844         var name = typeSel.options[typeSel.selectedIndex].value;
845         var val = typeSel._cbox.attr('value');
846
847         console.log("selected ident is " + val);
848
849         // it differs from the existing ident value, update it
850         var oldIdent = self.getLiOrderIdent(li);
851         if (oldIdent && 
852             oldIdent.attr_name() == name &&
853             oldIdent.attr_value() == val) {
854                 console.log('selected ident attr matches existing attr');
855                 if (--this._identValuesInFlight == 0) {
856                     if (oncomplete) oncomplete(li);
857                     else location.href = location.href;
858                 }
859                 return false;
860         }
861
862         // see if the selected ident value is represented
863         // by an existing lineitem attr
864
865         var args = {};
866         typeSel._cbox.store.fetch({
867             query : {attr_value : val},
868             onItem : function(item) {                                                       
869                 console.log('found existing attr for ident value');
870                 args.source_attr_id = li.attributes().filter(
871                     function(attr) { return attr.id() == item.id[0] }
872                 )[0];
873             }                                                                      
874         }); 
875
876
877         if (!args.source_attr_id) {
878             // user entered new text in the combobox
879             // so we need to create a new attr
880             console.log('creating new ident attr');
881             args.lineitem_id = li.id();
882             args.attr_name = name;
883             args.attr_value = val;
884         }
885
886         fieldmapper.standardRequest(
887             ['open-ils.acq', 'open-ils.acq.lineitem.order_identifier.set'],
888             {   async : true,
889                 params : [openils.User.authtoken, args],
890                 oncomplete : function() {
891                     console.log('order_ident oncomplete');
892                     if (--self._identValuesInFlight == 0) {
893                         if (oncomplete) oncomplete(li);
894                         else location.href = location.href;
895                     }
896                     console.log(self._identValuesInFlight + ' still in flight');
897                 }
898             }
899         );
900
901         return true;
902     };
903
904     this.getLiOrderIdent = function(li) {
905         var attrs = li.attributes();
906         if (!attrs) return null;
907         return attrs.filter(
908             function(attr) {
909                 return (
910                     attr.attr_type() == 'lineitem_local_attr_definition' &&
911                     openils.Util.isTrue(attr.order_ident())
912                 );
913             }
914         )[0];
915     };
916
917     this.drawOrderIdentSelector = function(li, row) {
918         var self = this;
919         var typeSel = dojo.query('[name=order_ident_type]', row)[0];
920         var valueSel = dojo.query('[name=order_ident_value]', row)[0];
921
922         var attrs = li.attributes();
923
924         // limit to MARC attr defs
925         attrs = attrs.filter(
926             function(attr) {
927                 return (attr.attr_type() == 'lineitem_marc_attr_definition');
928             }
929         );
930
931         var identAttr = this.getLiOrderIdent(li);
932
933
934         // collect the values for each type of identifier
935         // find a reasonable default identifier type to render
936         
937         var values = {};
938         var typeSet = null;
939         dojo.forEach(['isbn', 'upc', 'issn'],
940             function(name) {
941
942                 // collect the values for this attr name
943                 values[name] =  attrs.filter(
944                     function(attr) {
945                         return (attr.attr_name() == name)
946                     }
947                 );
948
949                 // select a reasonable default name in the type-selector
950                 if (!typeSet) {
951                     var useMe = false;
952                     if (identAttr) {
953                         if (identAttr.attr_name() == name)
954                             useMe = true;
955                     } else if (values[name].length) {
956                         useMe = true;
957                     }
958
959                     if (useMe) {
960                         dojo.forEach(typeSel.options, function(opt) {
961                             if (opt.value == name) {
962                                 opt.selected = true;
963                                 typeSet = name;
964                             }
965                         });
966                     }
967                 }
968             }
969         );
970
971         function updateOrderIdent(val) {
972             self._identValuesInFlight = 1;
973             self._applyOrderIdentValue(
974                 this._lineitem,
975                 function(li) {
976                     self.refreshLineitem(li);
977                 }
978             );
979         }
980
981         // replace the ident combobox with a new 
982         // one for the selected ident type 
983         function changeComboBox(sel) {
984             var name = sel.options[sel.selectedIndex].value;
985
986             var td = dojo.query('[name=order_ident_value]', row)[0];
987             if (td.childNodes[0]) 
988                 dojo.destroy(td.childNodes[0]);
989
990             var store = new dojo.data.ItemFileWriteStore({
991                 data : acqlia.toStoreData(values[name])
992             });
993
994             var cbox = new dijit.form.ComboBox(
995                 {   store : store,
996                     labelAttr : 'attr_value',
997                     searchAttr : 'attr_value'
998                 }, 
999                 dojo.create('div', {}, td)
1000             );
1001
1002             cbox.startup();
1003
1004             // set the value for the cbox
1005             if (values[name].length) {
1006                 var orderIdent = self.getLiOrderIdent(li);
1007
1008                 if (orderIdent && orderIdent.attr_name() == name) {
1009                     cbox.attr('value', orderIdent.attr_value());
1010                 } else  {
1011                     cbox.attr('value', values[name][0].attr_value());
1012                 }
1013             }
1014
1015             if (!self.orderIdentAllowed) 
1016                 cbox.attr('disabled', true);
1017
1018             sel._cbox = cbox;
1019             cbox._lineitem = li;
1020             dojo.connect(cbox, 'onChange', updateOrderIdent);
1021         }
1022
1023         changeComboBox(typeSel); // force the initial draw
1024         typeSel.onchange = function() {changeComboBox(typeSel)};
1025     };
1026
1027     this.testOrderIdentPerms = function(org, callback) {
1028         var self = this;
1029         new openils.User().getPermOrgList(
1030             'ACQ_SET_LINEITEM_IDENTIFIER',
1031             function(orgs) { 
1032                 console.log('found orgs = ' + orgs);
1033                 for (var i = 0; i < orgs.length; i++) {
1034                     if (Number(orgs[i]) == Number(org)) {
1035                         self.orderIdentAllowed = true;
1036                         if (callback) callback();
1037                         return;
1038                     }
1039                 }
1040                 if (callback) callback();
1041             }, 
1042             true, true
1043         );
1044     };
1045
1046     this.checkClaimEligibility = function(li, callback, row) {
1047         /* Assume always eligible, i.e. from this interface we don't care about
1048          * claim eligibility any more. this is where the user would force a
1049          * claime. */
1050         this.clearEligibility(li);
1051         this.claimEligibleLidByLi[li.id()] = li.lineitem_details().map(
1052             function(lid) { return lid.id(); }
1053         );
1054         li.lineitem_details().forEach(
1055             function(lid) { self.claimEligibleLid[lid.id()] = true; }
1056         );
1057         this.reconsiderClaimControl(li, row);
1058         if (callback) callback(li);
1059         /*
1060         this.clearEligibility(li);
1061         fieldmapper.standardRequest(
1062             ["open-ils.acq", "open-ils.acq.claim.eligible.lineitem_detail"], {
1063                 "params": [openils.User.authtoken, {"lineitem": li.id()}],
1064                 "async": true,
1065                 "onresponse": function(r) {
1066                     if (r = openils.Util.readResponse(r)) {
1067                         self.claimEligibleLidByLi[li.id()].push(
1068                             r.lineitem_detail()
1069                         );
1070                         self.claimEligibleLid[r.lineitem_detail()] = true;
1071                     }
1072                 },
1073                 "oncomplete": function() {
1074                     self.reconsiderClaimControl(li, row);
1075                     if (typeof(callback) == "function")
1076                         callback();
1077                 }
1078             }
1079         );
1080         */
1081     };
1082
1083     this.updateLiNotesCount = function(li, row) {
1084         if (!row) row = this._findLiRow(li);
1085
1086         var has_notes = (li.lineitem_notes().filter(
1087                 function(o) { return Boolean (o.alert_text()); }
1088             ).length > 0);
1089
1090         /* U+2691 is the code point for a filled-in flag character */
1091         nodeByName("notes_alert_flag", row).innerHTML =
1092              has_notes ? "&#x2691;" : "";
1093         nodeByName("noteslink", row).style.fontStyle =
1094             has_notes ? "italic" : "normal";
1095         nodeByName("notes_count", row).innerHTML = li.lineitem_notes().length;
1096     };
1097
1098     /* XXX NOT related to _updateLiState(). rethink */
1099     this.updateLiState = function(li, row) {
1100         if (!row) row = this._findLiRow(li);
1101
1102         var actUpdateBarcodes = nodeByName("action_update_barcodes", row);
1103         var actHoldingsMaint = nodeByName("action_holdings_maint", row);
1104
1105         // always allow access to LI history
1106         nodeByName('action_view_history', row).onclick = 
1107             function() { location.href = oilsBasePath + '/acq/lineitem/history/' + li.id(); };
1108
1109         /* handle row coloring for based on LI state */
1110         openils.Util.removeCSSClass(row, /^oils-acq-li-state-/);
1111         openils.Util.addCSSClass(row, "oils-acq-li-state-" + li.state());
1112
1113         // Expose invoice actions for any lineitem that is linked to a PO 
1114         if (li.purchase_order()) {
1115             openils.Util.show(nodeByName("invoices_span", row), "inline");
1116             var link = nodeByName("invoices_link", row);
1117             link.onclick = function() {
1118                 openils.XUL.newTabEasy(
1119                     oilsBasePath + "/acq/search/unified?so=" +
1120                     base64Encode({"jub":[{"id": li.id()}]}) + "&rt=invoice"
1121                 );
1122                 return false;
1123             };
1124         }
1125                 
1126
1127         /*
1128          * If we haven't fleshed the lineitem_details, default to allowing access to the 
1129          * holdings maintenence actions.  The alternative is to flesh LIDs on every lineitem, 
1130          * but that will add to page render time.  Let's see if this will suffice...
1131          */
1132         var lids = li.lineitem_details();
1133         if( !lids || 
1134                 (lids && !lids.filter(function(lid) { return lid.eg_copy_id() })[0] )) {
1135
1136             actUpdateBarcodes.disabled = false;
1137             actUpdateBarcodes.onclick = function() {
1138                 self.showRealCopyEditUI(li);
1139                 nodeByName("action_none", row).selected = true;
1140             }
1141             actHoldingsMaint.disabled = false;
1142             actHoldingsMaint.onclick = 
1143                 self.generateMakeRecTab( li.eg_bib_id(), 'copy_browser', row );
1144         }
1145
1146         var state_cell = nodeByName("li_state", row);
1147
1148         switch(li.state()) {
1149
1150             case 'cancelled':
1151                 if(typeof li.cancel_reason() == "object") {
1152                     var holds_state = dojo.create(
1153                         "span", {
1154                             "style": "border-bottom: 1px dashed #000;",
1155                             "innerHTML": li.state()
1156                         }, state_cell, "only"
1157                     );
1158                     new dijit.Tooltip(
1159                         {
1160                             "label": "<em>" + li.cancel_reason().label() +
1161                                 "</em><br />" + li.cancel_reason().description(),
1162                             "connectId": [holds_state]
1163                         }, dojo.create("span", null, state_cell, "last")
1164                     );
1165                 }
1166                 return; // all done
1167
1168             case "on-order":
1169                 break;
1170
1171             case "received":
1172                 break;
1173         }
1174
1175         state_cell.innerHTML = li.state(); // TODO i18n state labels
1176     };
1177
1178
1179     this._setAlertStore = function() {
1180         acqLitAlertAlertText.store = new dojo.data.ItemFileReadStore(
1181             {
1182                 "data": acqliat.toStoreData(
1183                     this.pcrud.search(
1184                         "acqliat", {
1185                             "owning_lib": aou.orgNodeTrail(
1186                                 aou.findOrgUnit(openils.User.user.ws_ou())
1187                             ).map(function(o) { return o.id(); })
1188                         }
1189                     )
1190                 )
1191             }
1192         );
1193         acqLitAlertAlertText.setValue(); /* make the store "live" */
1194         acqLitAlertAlertText._store_ready = true;
1195     };
1196
1197     /**
1198      * Draws and shows the lineitem notes pane
1199      */
1200     this.drawLiNotes = function(li) {
1201         var self = this;
1202         this.focusLineitem = li.id();
1203
1204         if (!acqLitAlertAlertText._store_ready)
1205             this._setAlertStore();
1206
1207         li.lineitem_notes(
1208             li.lineitem_notes().sort(
1209                 function(a, b) { 
1210                     if(a.edit_time() < b.edit_time()) return 1;
1211                     return -1;
1212                 }
1213             )
1214         );
1215
1216         while(this.liNotesTbody.childNodes[0])
1217             this.liNotesTbody.removeChild(this.liNotesTbody.childNodes[0]);
1218         this.show('notes');
1219
1220         acqLitCreateNoteSubmit.onClick = function() {
1221             var value = acqLitCreateNoteText.attr('value');
1222             if(!value) return;
1223             var note = new fieldmapper.acqlin();
1224             note.isnew(true);
1225             note.vendor_public(
1226                 Boolean(acqLitCreateNoteVendorPublic.attr('checked'))
1227             );
1228             note.value(value);
1229             note.lineitem(li.id());
1230
1231             self.updateLiNotes(li, note);
1232             acqLitCreateNoteVendorPublic.attr("checked", false);
1233             acqLitCreateNoteText.attr("value", "");
1234         }
1235
1236         acqLitCreateAlertSubmit.onClick = function() {
1237             if (!acqLitAlertAlertText.item) {
1238                 alert(localeStrings.ALERT_UNSELECTED);
1239                 return;
1240             }
1241
1242             var alert_text = new fieldmapper.acqliat().fromStoreItem(
1243                 acqLitAlertAlertText.item
1244             );
1245             var value = acqLitAlertNoteValue.attr("value") || "";
1246
1247             var note = new fieldmapper.acqlin();
1248             note.isnew(true);
1249             note.lineitem(li.id());
1250             note.value(value);
1251             note.alert_text(alert_text);
1252
1253             self.updateLiNotes(li, note);
1254         }
1255
1256         dojo.forEach(li.lineitem_notes(), function(note) { self.addLiNote(li, note) });
1257     }
1258
1259     /**
1260      * Draws a single lineitem note in the notes pane
1261      */
1262     this.addLiNote = function(li, note) {
1263         if(note.isdeleted()) return;
1264         var self = this;
1265         var row = self.liNotesRow.cloneNode(true);
1266         nodeByName("value", row).innerHTML = note.value();
1267         var alert_node = nodeByName("alert_code", row);
1268         if (note.alert_text()) {
1269             alert_node.innerHTML = dojo.string.substitute(
1270                 "[${0}] ${1}", [
1271                     aou.findOrgUnit(note.alert_text().owning_lib()).shortname(),
1272                     note.alert_text().code()
1273                 ]
1274             );
1275             if (note.alert_text().description()) {
1276                 new dijit.Tooltip(
1277                     {
1278                         "connectId": [alert_node],
1279                         "label": note.alert_text().description()
1280                     }, dojo.create("span", null, alert_node, "after")
1281                 );
1282             }
1283         }
1284
1285         if (openils.Util.isTrue(note.vendor_public()))
1286             nodeByName("vendor_public", row).innerHTML =
1287                 localeStrings.VENDOR_PUBLIC;
1288
1289         nodeByName("delete", row).onclick = function() {
1290             note.isdeleted(true);
1291             self.liNotesTbody.removeChild(row);
1292             self.updateLiNotes(li);
1293         };
1294
1295         if(note.edit_time()) {
1296             nodeByName("edit_time", row).innerHTML =
1297                 dojo.date.locale.format(
1298                     dojo.date.stamp.fromISOString(note.edit_time()), 
1299                     {formatLength:'short'});
1300         }
1301
1302         self.liNotesTbody.appendChild(row);
1303     }
1304
1305     /**
1306      * Updates any new/changed/deleted notes on the server
1307      */
1308     this.updateLiNotes = function(li, newNote) {
1309
1310         var notes;
1311         if(newNote) {
1312             notes = [newNote];
1313         } else {
1314             notes = li.lineitem_notes().filter(
1315                 function(note) {
1316                     if(note.ischanged() || note.isnew() || note.isdeleted())
1317                         return note;
1318                 }
1319             );
1320         }
1321
1322         if(notes.length == 0) return;
1323         progressDialog.show();
1324
1325         fieldmapper.standardRequest(
1326             ['open-ils.acq', 'open-ils.acq.lineitem_note.cud.batch'],
1327             {   async : true,
1328                 params : [this.authtoken, notes],
1329                 onresponse : function(r) {
1330                     var resp = openils.Util.readResponse(r);
1331
1332                     if(resp.complete) {
1333
1334                         if(!newNote) {
1335                             // remove the old changed notes
1336                             var list = [];
1337                             dojo.forEach(li.lineitem_notes(), 
1338                                 function(note) {
1339                                     if(!(note.ischanged() || note.isnew() || note.isdeleted()))
1340                                         list.push(note);
1341                                 }
1342                             );
1343                             li.lineitem_notes(list);
1344                         }
1345
1346                         progressDialog.hide();
1347                         self.updateLiNotesCount(li);
1348                         self.drawLiNotes(li);
1349                         return;
1350                     }
1351
1352                     progressDialog.update(resp);
1353                     var newnote = resp.note;
1354
1355                     if(!newnote.isdeleted()) {
1356                         newnote.isnew(false);
1357                         newnote.ischanged(false);
1358                         li.lineitem_notes().push(newnote);
1359                     }
1360                 },
1361             }
1362         );
1363     }
1364
1365     this.updateLiPrice = function(input, li) {
1366         var self = this;
1367         var price = input.value;
1368         if(Number(price) == Number(li.estimated_unit_price())) return;
1369
1370         fieldmapper.standardRequest(
1371             ['open-ils.acq', 'open-ils.acq.lineitem.price.set'],
1372             {   async : false, // redundant w/ timeout
1373                 timeout : 10,
1374                 params : [this.authtoken, li.id(), price],
1375                 oncomplete : function(r) {
1376                     openils.Util.readResponse(r);
1377                     li.estimated_unit_price(price); // update local copy
1378
1379                     /*
1380                      * If this is a PO and every visible lineitem has a price,
1381                      * check again to see if this PO can be activated.  Note that 
1382                      * every visible lineitem having a price does not guarantee it can
1383                      * be activated, which is why we still make the call.  Having a price
1384                      * set for every visiable lineitem is just the lowest barrier to entry.
1385                      */
1386                     if (self.isPO) {
1387                         var priceNodes = dojo.query('[name=price]', dojo.byId('acq-lit-tbody'));
1388                         var allSet = true;
1389                         dojo.forEach(priceNodes, function(node) { if (node.value == '') allSet = false});
1390                         if (allSet) checkCouldActivatePo();
1391                     }
1392                 }
1393             }
1394         );
1395     }
1396
1397     this.removeLineitem = function(liId) {
1398         this.tbody.removeChild(dojo.query('[li='+liId+']', this.tbody)[0]);
1399         delete this.liCache[liId];
1400         //selected.push(self.liCache[i.parentNode.parentNode.getAttribute('li')]);
1401     }
1402
1403     this.drawInfo = function(liId) {
1404         this.focusLineitem = liId;
1405         if (!this._isRelatedViewer) {
1406             var d = dojo.byId("acq-lit-info-related");
1407             if (!this.relCache[liId]) {
1408                 fieldmapper.standardRequest(
1409                     [
1410                         "open-ils.acq",
1411                         "open-ils.acq.lineitems_for_bib.by_lineitem_id.count"
1412                     ], {
1413                         "async": true,
1414                         "params": [openils.User.authtoken, liId],
1415                         "onresponse": function(r) {
1416                             self.relCache[liId] = openils.Util.readResponse(r);
1417                             nodeByName("related_number", d).innerHTML =
1418                                 self.relCache[liId];
1419                             openils.Util[
1420                                 self.relCache[liId] >1 ? "show" : "hide"
1421                             ](d);
1422                         }
1423                     }
1424                 );
1425             } else {
1426                 nodeByName("related_number", d).innerHTML = this.relCache[liId];
1427                 openils.Util[this.relCache[liId] > 1 ? "show" : "hide"](d);
1428             }
1429         }
1430
1431         this.show('info');
1432         openils.acq.Lineitem.fetchAttrDefs(
1433             function() { 
1434                 self._fetchLineitem(liId, function(li){self._drawInfo(li);}); 
1435             } 
1436         );
1437     };
1438
1439     this.toggleInlineCopies = function() {
1440         // if any inline copies are not displayed, 
1441         // display them all otherwise, hide them all.
1442
1443         var displayAll = false;
1444
1445         for (var liId in this.liCache) {
1446             if (!this.inlineCopiesVisible(liId)) {
1447                 displayAll = true;
1448                 break;
1449             }
1450         }
1451
1452         for (var liId in this.liCache) {
1453             var row = dojo.byId('acq-inline-copies-row-' + liId);
1454             if (displayAll) {
1455                 if (!row || row._hidden) {
1456                     this.drawInlineCopies(liId);
1457                 }
1458             } else { // hide all
1459                 if (row) {
1460                     // drawInlineCopies() on a visible row will hide it.
1461                     this.drawInlineCopies(liId);
1462                 }
1463             }
1464         }
1465
1466     };
1467
1468     this.inlineCopiesVisible = function(liId) {
1469         var row = dojo.byId('acq-inline-copies-row-' + liId); 
1470         return (row && !row._hidden);
1471     }
1472
1473     this.refreshInlineCopies = function(all, reFetch) {
1474         var self = this;
1475         var liIds = this.inlineCopiesNeedingRefresh;
1476         if (all) liIds = openils.Util.objectProperties(liCache);
1477         liIds.forEach(function(liId) {
1478             if (self.inlineCopiesVisible(liId)) {
1479                 self.drawInlineCopies(liId, reFetch); // hide
1480                 self.drawInlineCopies(liId, reFetch); // re-draw
1481             }
1482         });
1483     };
1484
1485     // draw inline copy table.  if the table is 
1486     // already visible, hide the table as-is
1487     // reFetch forces a retrieval of the lineitem and 
1488     // copies from the server.  otherwise the locally
1489     // cached version of each is used.
1490     this.drawInlineCopies = function(liId, reFetch) {
1491         var self = this;
1492             
1493         // find or create the row where the inline copies table will live
1494         var containerRow = dojo.byId('acq-inline-copies-row-' + liId);
1495         var liRow = dojo.query('[li=' + liId + ']')[0];
1496
1497         if (!containerRow) {
1498
1499             // build the inline copies container row and add it to 
1500             // the DOM directly after the primary lineitem row
1501
1502             containerRow = self.inlineCopyContainer.cloneNode(true);
1503             containerRow.id = 'acq-inline-copies-row-' + liId;
1504
1505             if (liRow.nextSibling) {
1506                 self.tbody.insertBefore(containerRow, liRow.nextSibling);
1507             } else {
1508                 self.tbody.appendChild(containerRow);
1509             }
1510
1511         } else {
1512
1513             // toggle the visible state
1514             containerRow._hidden = !containerRow._hidden;
1515             openils.Util.toggle(containerRow, 'table-row');
1516
1517             if (containerRow._hidden) return; // hide only
1518         }
1519
1520         var handler = function(li) {
1521
1522             var tbody = dojo.query(
1523                 '[name=acq-li-inline-copies-tbody]', 
1524                 containerRow)[0];
1525
1526             // reset the table before adding copy rows
1527             while (tbody.childNodes[0])
1528                 tbody.removeChild(tbody.childNodes[0]);
1529
1530             if(li.lineitem_details().length == 0) {
1531                 tbody.appendChild(
1532                     self.inlineNoCopies.cloneNode(true));
1533                 return; // no copies to show
1534             }
1535
1536             // add a row to the inline copy table for each copy
1537             dojo.forEach(li.lineitem_details(),
1538                 function(copy) {
1539                     var row = self.inlineCopyTemplate.cloneNode(true);
1540                     tbody.appendChild(row);
1541                     self.addInlineCopy(li, copy, row);
1542                 }
1543             );
1544         };
1545
1546         this._fetchLineitem(liId, handler, reFetch);
1547     };
1548
1549     /** Draw read-only copy widgets for inline copies */
1550     this.addInlineCopy = function(li, copy, row) {
1551
1552         var self = this;
1553         dojo.forEach(liDetailFields,
1554             function(field) {
1555
1556                 var widget = new openils.widget.AutoFieldWidget({
1557                     fmObject : copy,
1558                     fmField : field,
1559                     labelFormat : (field == 'fund') ? fundLabelFormat : null,
1560                     searchFormat : (field == 'fund') ? fundSearchFormat : null,
1561                     dijitArgs: {"labelType": (field == 'fund') ? "html" : null},
1562                     fmClass : 'acqlid',
1563                     parentNode : dojo.query('[name=' + field + ']', row)[0],
1564                     readOnly : true,
1565                 });
1566
1567                 widget.build();
1568             }
1569         );
1570     };
1571
1572     /* For a given list of lineitem ids, build a list of full lineitems
1573      * re-using the fetching logic that is otherwise typical to use in this
1574      * module.
1575      *
1576      * If we've already got a lineitem in the cache, just use that.
1577      *
1578      * Once we've built a list of lineitems, call callback(thatlist).
1579      */
1580     this.fetchLineitemsById = function(id_list, callback) {
1581         var total = id_list.length;
1582         var result_list = [];
1583
1584         var inner = function(li) {
1585             result_list.push(li)
1586             if (--total <= 0)
1587                 callback(result_list);
1588         };
1589
1590         id_list.forEach(function(id) { self._fetchLineitem(id, inner); });
1591     };
1592
1593     this._fetchLineitem = function(liId, handler, force) {
1594
1595         var li = this.liCache[liId];
1596         if(li && li.marc() && li.lineitem_details() && !force)
1597             return handler(li);
1598         
1599         fieldmapper.standardRequest(
1600             ['open-ils.acq', 'open-ils.acq.lineitem.retrieve.authoritative'],
1601             {   async: true,
1602
1603                 params: [self.authtoken, liId, {
1604                     flesh_attrs: true,
1605                     flesh_cancel_reason: true,
1606                     flesh_li_details: true,
1607                     flesh_notes: true,
1608                     flesh_fund_debit: true }],
1609
1610                 oncomplete: function(r) {
1611                     var li = openils.Util.readResponse(r);
1612                     self.liCache[liId] = li;
1613                     handler(li)
1614                 }
1615             }
1616         );
1617     };
1618
1619     this._drawInfo = function(li) {
1620
1621         acqLitEditOrderMarc.onClick = function() { self.editOrderMarc(li); }
1622
1623         if(li.eg_bib_id()) {
1624             openils.Util.hide('acq-lit-marc-order-record-label');
1625             openils.Util.hide(acqLitEditOrderMarc.domNode);
1626             openils.Util.show('acq-lit-marc-real-record-label');
1627         } else {
1628             openils.Util.show('acq-lit-marc-order-record-label');
1629             openils.Util.show(acqLitEditOrderMarc.domNode);
1630             openils.Util.hide('acq-lit-marc-real-record-label');
1631         }
1632
1633         this.drawMarcHTML(li);
1634         this.infoTbody = dojo.byId('acq-lit-info-tbody');
1635
1636         if(!this.infoRow)
1637             this.infoRow = this.infoTbody.removeChild(dojo.byId('acq-lit-info-row'));
1638         while(this.infoTbody.childNodes[0])
1639             this.infoTbody.removeChild(this.infoTbody.childNodes[0]);
1640
1641         for(var i = 0; i < li.attributes().length; i++) {
1642             var attr = li.attributes()[i];
1643             var row = this.infoRow.cloneNode(true);
1644
1645             var type = attr.attr_type().replace(/lineitem_(.*)_attr_definition/, '$1');
1646             var name = openils.acq.Lineitem.attrDefs[type].filter(
1647                 function(a) {
1648                     return (a.code() == attr.attr_name());
1649                 }
1650             ).pop().description();
1651
1652             dojo.query('[name=label]', row)[0].appendChild(document.createTextNode(name));
1653             dojo.query('[name=value]', row)[0].appendChild(document.createTextNode(attr.attr_value()));
1654             this.infoTbody.appendChild(row);
1655         }
1656
1657         if (!this._isRelatedViewer) {
1658             nodeByName("rel_link", dojo.byId("acq-lit-info-related")).href =
1659                 oilsBasePath + "/acq/lineitem/related/" + li.id();
1660         }
1661
1662         // if a top scroll point is defined, jump up to it here
1663         var node = dojo.byId('oils-scroll-to-top');
1664         if (node) dijit.scrollIntoView(node);
1665     };
1666
1667     this.generateMakeRecTab = function(bib_id,default_view, row) {
1668         return function() {
1669             xulG.new_tab(
1670                 XUL_OPAC_WRAPPER,
1671                 {tab_name: localeStrings.XUL_RECORD_DETAIL_PAGE, browser:false},
1672                 {
1673                     no_xulG : false, 
1674                     show_nav_buttons : true, 
1675                     show_print_button : true, 
1676                     opac_url : xulG.url_prefix('opac_rdetail|' + bib_id),
1677                     default_view : default_view
1678                 }
1679             );
1680
1681             if(row) nodeByName("action_none", row).selected = true;
1682         }
1683     };
1684
1685     this.drawMarcHTML = function(li) {
1686         var params = [null, true, li.marc()];
1687         if(li.eg_bib_id()) 
1688             params = [li.eg_bib_id(), true];
1689
1690         fieldmapper.standardRequest(
1691             ['open-ils.search', 'open-ils.search.biblio.record.html'],
1692             {   async: true,
1693                 params: params,
1694                 oncomplete: function(r) {
1695                     dojo.byId('acq-lit-marc-div').innerHTML = 
1696                         openils.Util.readResponse(r);
1697                 }
1698             }
1699         );
1700     }
1701
1702     this.drawCopies = function(liId, force_fetch) {
1703         this.focusLineitem = liId;
1704         if (typeof force_fetch == "undefined")
1705             force_fetch = false;
1706
1707         var cgi = new openils.CGI();
1708         var source = cgi.param('source');
1709         if (source && source.match(/invoice/)) {
1710             // got here from the invoice page, show the 'return-to-invoice' button
1711             var cgi = new openils.CGI({url : source});
1712             cgi.param('focus_li', liId);
1713             openils.Util.show(dojo.byId('acq-lit-copies-back-to-invoice-button-wrapper'), 'inline');
1714             var button = dojo.byId('acq-lit-copies-back-to-invoice-button');
1715             button.onclick = function() { location.href = cgi.url() };
1716         }
1717
1718         openils.acq.Lineitem.fetchAndRender(liId, {}, 
1719             function(li, html) {
1720                 dojo.byId('acq-lit-copies-li-summary').innerHTML = html;
1721             }
1722         );
1723
1724         this.show('copies');
1725         var self = this;
1726         this.copyCache = {};
1727         this.copyWidgetCache = {};
1728         this.oldCopyWidgetCache = {};
1729         this.virtDfaCounts = {};
1730         this.realDfaCache = {};
1731         this.dfeOffset = 0;
1732
1733         acqLitSaveCopies.onClick = function() { self.saveCopyChanges(liId) };
1734         acqLitBatchUpdateCopies.onClick = function() { self.batchCopyUpdate() };
1735         acqLitCopyCountInput.attr('value', '0');
1736
1737         if (this.isPO && PO && PO.order_date()) {
1738             // prevent adding copies to activated POs
1739             acqLitCopyCountInput.attr('disabled', true);
1740             acqLitAddCopyCount.attr('disabled', true);
1741         }
1742
1743         while(this.copyTbody.childNodes[0])
1744             this.copyTbody.removeChild(this.copyTbody.childNodes[0]);
1745
1746         this._drawBatchCopyWidgets();
1747
1748         this._drawDistribApplied(liId);
1749
1750         this._fetchDistribFormulas(
1751             function() {
1752                 openils.acq.Lineitem.fetchAttrDefs(
1753                     function() { 
1754                         self._fetchLineitem(liId, function(li){self._drawCopies(li);}, force_fetch); 
1755                     } 
1756                 );
1757             }
1758         );
1759     };
1760
1761     this._saveDistribAppliedTemplates = function() {
1762         if (!this._appliedDistribTemplate) {
1763             this._appliedDistribTemplate =
1764                 dojo.byId("acq-lit-distrib-applied-tbody").
1765                     removeChild(dojo.byId("acq-lit-distrib-applied-row"));
1766             dojo.attr(this._appliedDistribTemplate, "id");
1767         }
1768     };
1769
1770     this._drawDistribApplied = function(liId) {
1771         /* Build this table while hidden to prevent rendering artifacts */
1772         openils.Util.hide("acq-lit-distrib-applied-tbody");
1773
1774         this._saveDistribAppliedTemplates();
1775
1776         /* Remove any rows in the table from previous populations */
1777         dojo.query("tr[formula]", "acq-lit-distrib-applied-tbody").
1778             forEach(dojo.destroy);
1779
1780         /* Unregister all dijits previously created (for some reason this isn't
1781          * covered by the above destroy calls). */
1782         dijit.registry.forEach(
1783             function(w) { if (/^dfa-/.test(w.id)) w.destroyRecursive(); }
1784         );
1785
1786         /* Populate the table with our liId */
1787         var total = 0;
1788         fieldmapper.standardRequest(
1789             ["open-ils.acq",
1790             "open-ils.acq.distribution_formula_application.ranged.retrieve"],
1791             {
1792                 "async": true,
1793                 "params": [self.authtoken, liId],
1794                 "onresponse": function(r) {
1795                     var dfa = openils.Util.readResponse(r);
1796                     if (dfa) {
1797                         total++;
1798                         self.realDfaCache[dfa.id()] = dfa;
1799                         self._drawDistribAppliedUnit(dfa);
1800                     }
1801                 },
1802                 "oncomplete": function() {
1803                     /* Reveal built table */
1804                     if (total) {
1805                         openils.Util.show(
1806                             "acq-lit-distrib-applied-tbody", "table-row-group"
1807                         );
1808                     }
1809                 }
1810             }
1811         );
1812     };
1813
1814     this._drawDistribAppliedUnit = function(dfa) {
1815         var new_row = false;
1816         var row = dojo.query(
1817             'tr[formula="' + dfa.formula().id() + '"]',
1818             "acq-lit-distrib-applied-tbody"
1819         )[0];
1820
1821         if (!row) {
1822             new_row = true;
1823             row = dojo.clone(this._appliedDistribTemplate);
1824             dojo.attr(row, "formula", dfa.formula().id());
1825             dojo.query("th", row)[0].innerHTML = dfa.formula().name();
1826         }
1827
1828         var td = dojo.query("td", row)[0];
1829
1830         dojo.create("span", {"id": "dfa-button-" + dfa.id()}, td, "last");
1831         dojo.create("span", {"id": "dfa-tip-" + dfa.id()}, td, "last");
1832
1833         if (new_row)
1834             dojo.place(row, "acq-lit-distrib-applied-tbody", "last");
1835
1836         new dijit.form.Button(
1837             {
1838                 "onClick": function() {
1839                     if (confirm(localeStrings.EXPLAIN_DFA_MGMT))
1840                         self.deleteDfa(dfa);
1841                 },
1842                 "label": "X",
1843                 /* XXX I /cannot/ make the following work in as a CSS class
1844                  * for some reason. So frustrating... */
1845                 "style": function(id) {
1846                      return (id > 0 ?
1847                         "font-weight: bold; color: #c00;" :
1848                         "color: #666;");
1849                      }(dfa.id()) + "margin: 0 6px;display: inline;"
1850             }, "dfa-button-" + dfa.id()
1851         );
1852         new dijit.Tooltip(
1853             {
1854                 "connectId": ["dfa-button-" + dfa.id()],
1855                 "label": dojo.string.substitute(
1856                     localeStrings.DFA_TIP, dfa.id() > 0 ? [
1857                         openils.User.formalName(dfa.creator()),
1858                         dojo.date.locale.format(
1859                             dojo.date.stamp.fromISOString(dfa.create_time()),
1860                             {"formatLength":"short"}
1861                         )
1862                     ] : [localeStrings.ITS_YOU, localeStrings.JUST_NOW]
1863                 )
1864             }, "dfa-tip-" + dfa.id()
1865         );
1866     }
1867
1868     this.deleteDfa = function(dfa) {
1869         if (dfa.id() > 0) { /* real */
1870             this.pcrud.eliminate(
1871                 dfa, {
1872                     "async": true,
1873                     "oncomplete": function() {
1874                         self._removeDistribApplied(dfa.id());
1875                         delete self.realDfaCache[dfa.id()];
1876                     }
1877                 }
1878             );
1879         } else { /* virtual */
1880             if (--(this.virtDfaCounts[dfa.formula().id()]) < 0)
1881             this.virtDfaCounts[dfa.formula().id()] = 0;
1882             /* hasn't been saved yet, so no need to do anything server side */
1883             this._removeDistribApplied(dfa.id());
1884         }
1885
1886     };
1887
1888     this._removeDistribApplied = function(dfaId) {
1889         var re = new RegExp("^dfa-\\w+-" + String(dfaId));
1890         dijit.registry.forEach(
1891             function(w) { if (re.test(w.id)) w.destroyRecursive(); }
1892         );
1893         this._removeDistribAppliedEmptyRows();
1894     };
1895
1896     this._removeAllDistribAppliedVirtual = function() {
1897         /* Unregister dijits */
1898         dijit.registry.forEach(
1899             function(w) { if (/^dfa-\w+--/.test(w.id)) w.destroyRecursive(); }
1900         );
1901         this._removeDistribAppliedEmptyRows();
1902     };
1903
1904     this._removeDistribAppliedEmptyRows = function() {
1905         /* Remove any rows with no DFA at all */
1906         dojo.query("tr[formula] td", "acq-lit-distrib-applied-tbody").forEach(
1907             function(o) {
1908                 if (o.childNodes.length < 1) dojo.destroy(o.parentNode);
1909             }
1910         );
1911     };
1912
1913     /**
1914      * Insert a new row into the distribution formula selection form
1915      */
1916     this._addDistribFormulaRow = function() {
1917         var self = this;
1918
1919         if (!self.distribForms) {
1920             // no formulas, hide the form
1921             openils.Util.hide('acq-lit-distrib-formula-table');
1922             return;
1923         }
1924
1925         if(!this.distribFormulaTemplate) 
1926             this.distribFormulaTemplate = 
1927                 dojo.byId('acq-lit-distrib-formula-tbody').removeChild(dojo.byId('acq-lit-distrib-form-row'));
1928
1929         var row = this.distribFormulaTemplate.cloneNode(true);
1930         dojo.place(row, "acq-lit-distrib-formula-tbody", "only");
1931
1932         this.dfSelector = new dijit.form.FilteringSelect(
1933             {"labelAttr": "dynLabel", "labelType": "html"},
1934             nodeByName("selector", row)
1935         );
1936         this._updateFormulaStore();
1937         this.dfSelector.fetchProperties =
1938             {"sort": [{"attribute": "use_count", "descending": true}]};
1939
1940         var apply = new dijit.form.Button(
1941             {"label": localeStrings.APPLY},
1942             nodeByName('set_button', row)
1943         ); 
1944
1945         var reset = new dijit.form.Button(
1946             {"label": localeStrings.RESET_FORMULAE, "disabled": true},
1947             nodeByName("reset_button", row)  
1948         );
1949
1950         dojo.connect(apply, 'onClick', 
1951             function() {
1952                 var form_id = self.dfSelector.attr("value");
1953                 if(!form_id) return;
1954                 self._applyDistribFormula(form_id);
1955                 reset.attr("disabled", false);
1956             }
1957         );
1958
1959         dojo.connect(reset, 'onClick', 
1960             function() {
1961                 self.restoreCopyFieldsBeforeDF();
1962                 self.virtDfaCounts = {};
1963                 self.virtDfaId = -1;
1964                 self.dfeOffset = 0;
1965                 self._updateFormulaStore();
1966                 self._removeAllDistribAppliedVirtual();
1967                 reset.attr("disabled", "true");
1968             }
1969         );
1970
1971     };
1972
1973     /**
1974      * Applies a distrib formula to the current set of copies
1975      */
1976     this._applyDistribFormula = function(formula) {
1977         if(!formula) return;
1978
1979         formula = this.distribForms.filter(
1980             function(form) { return form.id() == formula; }
1981         )[0];
1982
1983         var copyRows = dojo.query('tr', self.copyTbody);
1984
1985         if (this.dfeOffset >= copyRows.length) {
1986             alert(localeStrings.OUT_OF_COPIES);
1987             return;
1988         }
1989
1990         var entries_applied = 0;
1991         for(
1992             var rowIndex = this.dfeOffset;
1993             rowIndex < copyRows.length;
1994             rowIndex++
1995         ) {
1996             
1997             var row = copyRows[rowIndex];
1998             var copy_id = row.getAttribute('copy_id');
1999             var copyWidgets = this.copyWidgetCache[copy_id];
2000             var entryIndex = this.dfeOffset;
2001             var entry = null;
2002
2003             // find the correct entry for the current row
2004             dojo.forEach(formula.entries(), 
2005                 function(e) {
2006                     if(!entry) {
2007                         entryIndex += e.item_count();
2008                         if(entryIndex > rowIndex)
2009                             entry = e;
2010                     }
2011                 }
2012             );
2013
2014             if(entry) {
2015                 
2016                 //console.log("rowIndex = " + rowIndex + ", entry = " + entry.id() + ", entryIndex=" + 
2017                 //  entryIndex + ", owning_lib = " + entry.owning_lib() + ", location = " + entry.location());
2018     
2019                 entries_applied++;
2020                 this.saveCopyFieldsBeforeDF(copy_id);
2021                 this._copy_fields_for_acqdf.forEach(
2022                     function(field) {
2023                         if(entry[field]()) {
2024                             copyWidgets[field].attr('value', (entry[field]()));
2025                         }
2026                     }
2027                 );
2028             }
2029         }
2030
2031         if (entries_applied) {
2032             this.virtDfaCounts[formula.id()] =
2033                 ++(this.virtDfaCounts[formula.id()]) || 1;
2034             this._updateFormulaStore();
2035             this._drawDistribAppliedUnit(
2036                 function(df) {
2037                     var dfa = new acqdfa();
2038                     dfa.formula(df); dfa.id(self.virtDfaId--); return dfa;
2039                 }(formula)
2040             );
2041             this.dfeOffset += entries_applied;
2042         };
2043     };
2044
2045     /**
2046      * This function updates the DF store for the dropdown so that use_counts
2047      * can reflect DF applications from this session before they're saved
2048      * server-side.
2049      */
2050     this._updateFormulaStore = function() {
2051         this.dfSelector.store = new dojo.data.ItemFileReadStore(
2052             {
2053                 "data": self._labelFormulasWithCounts(
2054                     acqdf.toStoreData(self.distribForms)
2055                 )
2056             }
2057         );
2058     };
2059
2060     this.saveCopyFieldsBeforeDF = function(copy_id) {
2061         var self = this;
2062         if (!this.oldCopyWidgetCache[copy_id]) {
2063             var copyWidgets = this.copyWidgetCache[copy_id];
2064
2065             this.oldCopyWidgetCache[copy_id] = {};
2066             this._copy_fields_for_acqdf.forEach(
2067                 function(f) {
2068                     self.oldCopyWidgetCache[copy_id][f] =
2069                         copyWidgets[f].attr("value");
2070                 }
2071             );
2072         }
2073     };
2074
2075     this.restoreCopyFieldsBeforeDF = function() {
2076         var self = this;
2077         for (var copy_id in this.oldCopyWidgetCache) {
2078             this._copy_fields_for_acqdf.forEach(
2079                 function(f) {
2080                     self.copyWidgetCache[copy_id][f].attr(
2081                         "value", self.oldCopyWidgetCache[copy_id][f]
2082                     );
2083                 }
2084             );
2085         }
2086     };
2087
2088     this._labelFormulasWithCounts = function(store_data) {
2089         for (var key in store_data.items) {
2090             var obj = store_data.items[key];
2091             obj.use_count = Number(obj.use_count); /* needed for sorting */
2092
2093             if (this.virtDfaCounts[obj.id])
2094                 obj.use_count = obj.use_count + Number(this.virtDfaCounts[obj.id]);
2095
2096             obj.dynLabel = "<span class='acq-lit-distrib-form-use-count'>[" +
2097                 obj.use_count + "]</span>&nbsp; " + obj.name;
2098         }
2099         return store_data;
2100     };
2101
2102     /**
2103      * This method formerly would not refetch the DF formulas if they'd been
2104      * loaded already, but now it always re-fetches, since use_count changes.
2105      */
2106     /** TODO: port distrib-formula selector to autofieldwidget+pcrud/dojo store */
2107     this._fetchDistribFormulas = function(onload) {
2108         fieldmapper.standardRequest(
2109             ["open-ils.acq",
2110                 "open-ils.acq.distribution_formula.ranged.retrieve.atomic"],
2111             {
2112                 "async": true,
2113                 "params": [openils.User.authtoken, 0, 500],
2114                 "oncomplete": function(r) {
2115                     self.distribForms = openils.Util.readResponse(r);
2116                     if(!self.distribForms || self.distribForms.length == 0) {
2117                         self.distribForms = [];
2118                     }
2119                     self._addDistribFormulaRow();
2120                     onload();
2121                 }
2122             }
2123         );
2124     }
2125
2126     this._drawBatchCopyWidgets = function() {
2127         var row = this.copyBatchRow;
2128         dojo.forEach(liDetailBatchFields, 
2129             function(field) {
2130                 if(self.copyBatchRowDrawn) {
2131                     self.copyBatchWidgets[field].attr('value', null);
2132                 } else {
2133                     var args = self.afwCopyFieldArgs(field, "CREATE_PICKLIST");
2134                     args.parentNode = dojo.query('[name='+field+']', row)[0];
2135
2136                     var widget = new openils.widget.AutoFieldWidget(args);
2137                     widget.build(
2138                         function(w, ww) {
2139                             if (field == "fund" && w.store)
2140                                 self._ensureCSSFundClasses(w.store);
2141                             self.copyBatchWidgets[field] = w;
2142                         }
2143                     );
2144                     if (field == "fund") {
2145                         dojo.connect(
2146                             widget.widget, "onChange", function(val) {
2147                                 self._updateFundSelectorStyle(widget, val);
2148                             }
2149                         );
2150                     }
2151                 }
2152             }
2153         );
2154         this.copyBatchRowDrawn = true;
2155     };
2156
2157     this.batchCopyUpdate = function() {
2158         var self = this;
2159         for(var k in this.copyWidgetCache) {
2160             var cache = this.copyWidgetCache[k];
2161             dojo.forEach(liDetailBatchFields, function(f) {
2162                 var newval = self.copyBatchWidgets[f].attr('value');
2163                 if(newval) cache[f].attr('value', newval);
2164             });
2165         }
2166     };
2167
2168     this._drawCopies = function(li) {
2169         var self = this;
2170
2171         // this button sets the total number of copies for a given lineitem
2172         acqLitAddCopyCount.onClick = function() { 
2173             var count = acqLitCopyCountInput.attr('value');
2174
2175             // add new rows
2176             while(self.copyCount() < count)
2177                 self.addCopy(li); 
2178             
2179             // delete rows if necessary
2180             var diff = self.copyCount() - count;
2181             if(diff > 0) {
2182                 var rows = dojo.query('tr', self.copyTbody).reverse().slice(0, diff);
2183                 if(confirm(dojo.string.substitute(localeStrings.DELETE_LI_COPIES_CONFIRM, [diff]))) {
2184                     dojo.forEach(rows, function(row) {self.deleteCopy(row); });
2185                 } else {
2186                     acqLitCopyCountInput.attr('value', self.copyCount()+'');
2187                 }
2188             }
2189         }
2190
2191
2192         if(li.lineitem_details().length > 0) {
2193             dojo.forEach(li.lineitem_details(),
2194                 function(copy) {
2195                     self.addCopy(li, copy);
2196                 }
2197             );
2198         } else {
2199             self.addCopy(li);
2200         }
2201     };
2202
2203     this.copyCount = function() {
2204         var count = 0;
2205         for(var id in this.copyCache) {
2206             if(!this.copyCache[id].isdeleted())
2207                 count++;
2208         }
2209         return count;
2210     }
2211
2212     this.virtCopyId = -1;
2213     this.addCopy = function(li, copy) {
2214         var row = this.copyRow.cloneNode(true);
2215         this.copyTbody.appendChild(row);
2216         var self = this;
2217
2218         if(!copy) {
2219             copy = new fieldmapper.acqlid();
2220             copy.isnew(true);
2221             copy.id(this.virtCopyId--);
2222             copy.lineitem(li.id());
2223         }
2224
2225         this.copyCache[copy.id()] = copy;
2226         row.setAttribute('copy_id', copy.id());
2227         self.copyWidgetCache[copy.id()] = {};
2228
2229         acqLitCopyCountInput.attr('value', self.copyCount()+'');
2230
2231         var rcvr = copy.receiver();
2232         if (rcvr) {
2233             if (!userCache[rcvr]) {
2234                 if(rcvr == openils.User.user.id()) {
2235                     userCache[rcvr] = openils.User.user;
2236                 } else {
2237                     userCache[rcvr] = fieldmapper.standardRequest(
2238                         ['open-ils.actor', 'open-ils.actor.user.retrieve'],
2239                         {params: [openils.User.authtoken, rcvr]}
2240                     );
2241                 }
2242             }
2243             dojo.query('[name=receiver]', row)[0].innerHTML =  userCache[rcvr].usrname();
2244         }
2245
2246         dojo.forEach(liDetailFields,
2247             function(field) {
2248                 var searchFilter;
2249                 if (field == "fund") {
2250                     searchFilter = (copy.fund() ?
2251                         {"-or": {"active": "t", "id": copy.fund()}} :
2252                         {"active" : "t"});
2253                 } else {
2254                     searchFilter = null;
2255                 }
2256
2257                 var readOnly = false;
2258                 
2259                 // TODO: Add support for changing the owning_lib after real copies have been made.  
2260                 // owning_lib is order data as much as its item data
2261                 if(copy.eg_copy_id() && ['owning_lib', 'location', 'circ_modifier', 'cn_label', 'barcode'].indexOf(field) >= 0) {
2262                     readOnly = true;
2263                 }
2264
2265                 // TODO: add support for changing the fund after debits have been created
2266                 // Note: invoicing allows the change
2267                 if(copy.fund_debit() && field == 'fund') {
2268                     readOnly = true;
2269                 }
2270
2271
2272                 var widget = new openils.widget.AutoFieldWidget({
2273                     fmObject : copy,
2274                     fmField : field,
2275                     labelFormat : (field == 'fund') ? fundLabelFormat : null,
2276                     searchFormat : (field == 'fund') ? fundSearchFormat : null,
2277                     dijitArgs: {"labelType": (field == 'fund') ? "html" : null},
2278                     searchFilter : searchFilter,
2279                     noCache: (field == "fund"),
2280                     fmClass : 'acqlid',
2281                     parentNode : dojo.query('[name='+field+']', row)[0],
2282                     orgLimitPerms : ['CREATE_PICKLIST', 'CREATE_PURCHASE_ORDER'],
2283                     readOnly : readOnly,
2284                     orgDefaultsToWs : true
2285                 });
2286
2287                 widget.build(
2288                     // make sure we capture the value from any async widgets
2289                     function(w, ww) { 
2290
2291                         if (field == "fund" && w.store)
2292                             self._ensureCSSFundClasses(w.store);
2293
2294                         if(!readOnly) 
2295                             copy[field](ww.getFormattedValue()) 
2296
2297                         self.copyWidgetCache[copy.id()][field] = w;
2298
2299                         dojo.connect(w, 'onChange', 
2300                             function(val) { 
2301                                 if (field == "fund")
2302                                     self._updateFundSelectorStyle(widget, val);
2303
2304                                 if (!readOnly && (copy.isnew() || val != copy[field]())) {
2305                                     // prevent setting ischanged() automatically on widget load for existing copies
2306                                     copy[field](widget.getFormattedValue()) 
2307                                     copy.ischanged(true);
2308                                 }
2309                             }
2310                         );
2311                     }
2312                 );
2313             }
2314         );
2315
2316         this.updateLidState(copy, row);
2317     };
2318
2319     this._ensureCSSFundClass = function(id) {
2320         if (!this.fundStyleSheet) {
2321             dojo.create(
2322                 "style", {"type": "text/css"},
2323                 document.getElementsByTagName("head")[0], "last"
2324             );
2325             this.fundStyleSheet = document.styleSheets[
2326                 document.styleSheets.length - 1
2327             ];
2328         }
2329
2330         var cn = "fund_" + id;
2331         if (!this.haveFundClass[cn]) {
2332             fieldmapper.standardRequest(
2333                 ["open-ils.acq", "open-ils.acq.fund.check_balance_percentages"],
2334                 {
2335                     "params": [openils.User.authtoken, id],
2336                     "async": true,
2337                     "oncomplete": function(r) {
2338                         r = openils.Util.readResponse(r);
2339                         self.fundBalanceState[id] = r;
2340                         var style = "";
2341                         if (r[0] /* stop */)
2342                             style = fundStyles.stop;
2343                         else if (r[1] /* warning */)
2344                             style = fundStyles.warning;
2345                         self.fundStyleSheet.insertRule(
2346                             "." + cn + " { " + style + " }",
2347                             self.fundStyleSheet.cssRules.length
2348                         );
2349                         self.haveFundClass[cn] = true;
2350                     }
2351                 }
2352             );
2353         }
2354     };
2355
2356     this._ensureCSSFundClasses = function(store) {
2357         store.fetch({
2358             "query": {"id": "*"},
2359             "onItem": function(o) { self._ensureCSSFundClass(o.id[0]); }
2360         });
2361     };
2362
2363     this._updateFundSelectorStyle = function(widget, fund_id) {
2364         openils.Util.removeCSSClass(widget.widget.domNode, /fund_\d+/);
2365         openils.Util.addCSSClass(widget.widget.domNode, "fund_" + fund_id);
2366     };
2367
2368     this.updateLidState = function(copy, row) {
2369         var self = this;
2370
2371         if (typeof(row) == "undefined") {
2372             row = dojo.query('tr[copy_id="' + copy.id() + '"]', this.copyTbody)[0];
2373         }
2374
2375         // action links
2376         var recv_link = nodeByName("receive", row);
2377         var unrecv_link = nodeByName("unreceive", row);
2378         var del_link = nodeByName("delete", row);
2379         var cxl_link = nodeByName("cancel", row);
2380         var claim_link = nodeByName("claim", row);
2381         var cxl_reason_link = nodeByName("cancel_reason", row);
2382
2383         // by default, hide all the actions
2384         openils.Util.hide(del_link.parentNode);
2385         openils.Util.hide(recv_link);
2386         openils.Util.hide(unrecv_link);
2387         openils.Util.hide(cxl_link);
2388         openils.Util.hide(claim_link);
2389         openils.Util.hide(cxl_reason_link);
2390
2391         if (copy.id() > 0) { // real copies (LIDs)
2392
2393             if (copy.cancel_reason()) { 
2394
2395                 /* --------- cancelled -------------------------- */
2396
2397                 /* XXX the following may leak memory in a long lived table: 
2398                  * dijits may not get destroyed... not positive. revisit. */
2399                 var holds_reason = dojo.create(
2400                     "span", {
2401                         "style": "border-bottom: 1px dashed #000;",
2402                         "innerHTML": "Cancelled" /* XXX [sic] and i18n */
2403                     }, cxl_reason_link, "only"
2404                 );
2405                 new dijit.Tooltip(
2406                     {
2407                         "label": "<em>" + copy.cancel_reason().label() +
2408                             "</em><br />" + copy.cancel_reason().description(),
2409                         "connectId": [holds_reason]
2410                     }, dojo.create("span", null, cxl_reason_link, "last")
2411                 );
2412                 openils.Util.show(cxl_reason_link, "inline");
2413
2414             } else if (copy.recv_time()) { 
2415
2416                 /* --------- received -------------------------- */
2417
2418                 openils.Util.show(unrecv_link, "inline");
2419                 unrecv_link.onclick = function() {
2420                     if (confirm(localeStrings.UNRECEIVE_LID))
2421                         self.issueReceive(copy, /* rollback */ true);
2422                 };
2423
2424             } else if (this.liCache[copy.lineitem()].state() == 'on-order') {
2425                 
2426                 /* --------- on order -------------------------- */
2427
2428                 openils.Util.show(recv_link, 'inline');
2429                 openils.Util.show(cxl_link, "inline");
2430
2431                 recv_link.onclick = function() {
2432                     if (self.checkLiAlerts(copy.lineitem()))
2433                         self.issueReceive(copy);
2434                 };
2435
2436                 cxl_link.onclick = function() { self.cancelLid(copy.id()) };
2437
2438             } else {
2439
2440                 /* --------- pre-order copies  -------------------------- */
2441
2442                 del_link.onclick = function() { self.deleteCopy(row) };
2443                 openils.Util.show(del_link.parentNode);
2444
2445             }
2446
2447         } else { 
2448
2449             /* --------- virtual copies  -------------------------- */
2450
2451             del_link.onclick = function() { self.deleteCopy(row) };
2452             openils.Util.show(del_link.parentNode);
2453         }
2454     };
2455
2456     this.cancelLid = function(lid_id) {
2457         lidCancelDialog._lid_id = lid_id;
2458         openils.Util.show(lidCancelDialog.domNode.parentNode);
2459         lidCancelDialog.show();
2460         if (!lidCancelDialog._prepared) {
2461             var widget = new openils.widget.AutoFieldWidget({
2462                 "fmField": "cancel_reason",
2463                 "fmClass": "acqlid",
2464                 "parentNode": dojo.byId("acq-lit-lid-cancel-reason"),
2465                 "orgLimitPerms": ["CREATE_PURCHASE_ORDER"],
2466                 "forceSync": true
2467             });
2468             widget.build(
2469                 function(w, ww) {
2470                     acqLidCancelButton.onClick = function() {
2471                         if (w.attr("value")) {
2472                             if (confirm(localeStrings.LID_CANCEL_CONFIRM)) {
2473                                 self._cancelLid(
2474                                     lidCancelDialog._lid_id,
2475                                     w.attr("value")
2476                                 );
2477                             }
2478                             lidCancelDialog.hide();
2479                         }
2480                     };
2481                     lidCancelDialog._prepared = true;
2482                 }
2483             );
2484         }
2485     };
2486
2487     this._cancelLid = function(lid_id, reason) {
2488         fieldmapper.standardRequest(
2489             ["open-ils.acq", "open-ils.acq.lineitem_detail.cancel"], {
2490                 "params": [openils.User.authtoken, lid_id, reason],
2491                 "async": true,
2492                 "onresponse": function(r) {
2493                     if (r = openils.Util.readResponse(r)) {
2494                         if (r.lid) {
2495                             for (var id in r.lid) {
2496                                 /* actually this should only iterate once */
2497                                 self.copyCache[id].cancel_reason(
2498                                     r.lid[id].cancel_reason
2499                                 );
2500                                 self.updateLidState(self.copyCache[id]);
2501                             }
2502                         }
2503                     }
2504                 }
2505             }
2506         );
2507     };
2508
2509     this._confirmAlert = function(li, lin) {
2510         return confirm(
2511             dojo.string.substitute(
2512                 localeStrings.CONFIRM_LI_ALERT, [
2513                     (new openils.acq.Lineitem({"lineitem": li})).findAttr(
2514                         "title", "lineitem_marc_attr_definition"
2515                     ), (
2516                         /* XXX it's really better add a parameter and to adjust
2517                          * the format string rather than do this concatenation
2518                          * here, but if someone wants this for 2.2 in a hurry,
2519                          * we can sidestep the problem of updating the strings
2520                          * while the translators are working. */
2521                         "[" +
2522                         aou.findOrgUnit(lin.alert_text().owning_lib()).shortname() +
2523                         "] " +
2524                         lin.alert_text().code()
2525                     ),
2526                     lin.alert_text().description() || "",
2527                     lin.value()
2528                 ]
2529             )
2530         );
2531     };
2532
2533     this.checkLiAlerts = function(li_id) {
2534         var li = this.liCache[li_id];
2535
2536         var alert_notes = li.lineitem_notes().filter(
2537             function(o) { return Boolean(o.alert_text()); }
2538         );
2539
2540         /* this is _intentionally_ not done in a call to forEach() ... */
2541         for (var i = 0; i < alert_notes.length; i++) {
2542             if (this.noteAcks[alert_notes[i].id()])
2543                 continue;
2544             else if (!this._confirmAlert(li, alert_notes[i]))
2545                 return false;
2546             else
2547                 this.noteAcks[alert_notes[i].id()] = true;
2548         }
2549
2550         return true;
2551     };
2552
2553     this.deleteCopy = function(row) {
2554         var copy = this.copyCache[row.getAttribute('copy_id')];
2555         copy.isdeleted(true);
2556         if(copy.isnew())
2557             delete this.copyCache[copy.id()];
2558         this.copyTbody.removeChild(row);
2559     }
2560
2561     this._virtDfaCountsAsList = function() {
2562         var L = [];
2563         for (var key in this.virtDfaCounts) {
2564             for (var i = 0; i < this.virtDfaCounts[key]; i++)
2565                 L.push(key);
2566         }
2567         return L;
2568     }
2569
2570     this.confirmBreachedCopyFunds = function(copies) {
2571         var stop = 0, warning = 0;
2572         copies.forEach(
2573             function(o) {
2574                 if (o.fund()) {
2575                     var state = self.fundBalanceState[o.fund()];
2576                     if (state[0] /* stop */)
2577                         stop++;
2578                     else if (state[1] /* warning */)
2579                         warning++;
2580                 }
2581             }
2582         );
2583
2584         if (stop) {
2585             return confirm(localeStrings.CONFIRM_FUNDS_AT_STOP);
2586         } else if (warning) {
2587             return confirm(localeStrings.CONFIRM_FUNDS_AT_WARNING);
2588         }
2589         return true;
2590     };
2591
2592     this.saveCopyChanges = function(liId) {
2593         var self = this;
2594         var copies = [];
2595
2596
2597         var total = 0;
2598         for(var id in this.copyCache) {
2599             var c = this.copyCache[id];
2600             if(!c.isdeleted()) total++;
2601             if(c.isnew() || c.ischanged() || c.isdeleted()) {
2602                 if(c.id() < 0) c.id(null);
2603                 copies.push(c);
2604             }
2605         }
2606
2607
2608         dojo.byId('acq-lit-copy-count-label-' + liId).innerHTML = total;
2609
2610
2611         if (copies.length > 0) {
2612             if (!this.confirmBreachedCopyFunds(copies))
2613                 return;
2614
2615             if (typeof(this._copy_count_cb) == "function")
2616                 this._copy_count_cb(liId, total);
2617
2618             openils.Util.show("acq-lit-update-copies-progress");
2619             fieldmapper.standardRequest(
2620                 ['open-ils.acq', 'open-ils.acq.lineitem_detail.cud.batch'],
2621                 {   async: true,
2622                     params: [openils.User.authtoken, copies],
2623                     onresponse: function(r) {
2624                         var res = openils.Util.readResponse(r);
2625                         litUpdateCopiesProgress.update(res);
2626                     },
2627                     oncomplete: function() {
2628                         self.drawCopies(liId, true /* force_fetch */);
2629                         openils.Util.hide("acq-lit-update-copies-progress");
2630                     }
2631                 }
2632             );
2633         }
2634
2635         var dfa_list = this._virtDfaCountsAsList();
2636         if (dfa_list.length > 0) {
2637             fieldmapper.standardRequest(
2638                 ["open-ils.acq",
2639                 "open-ils.acq.distribution_formula.record_application"],
2640                 {
2641                     "async": true,
2642                     "params": [openils.User.authtoken, dfa_list, liId],
2643                     "onresponse": function(r) {
2644                         var res = openils.Util.readResponse(r);
2645                         if (res && res.length < dfa_list.length)
2646                             alert(localeStrings.DFA_NOT_ALL);
2647                     }
2648                 }
2649             );
2650             this.virtDfaCounts = {};
2651         }
2652
2653         if (this.inlineCopiesNeedingRefresh.indexOf(liId) < 0)
2654             this.inlineCopiesNeedingRefresh.push(liId);
2655     };
2656
2657     this._updateCreatePoPrepayCheckbox = function(prepay) {
2658         var prepay = openils.Util.isTrue(prepay);
2659         this._prepayRequiredByVendor = prepay;
2660         dijit.byId("acq-lit-po-prepay").attr("checked", prepay);
2661     };
2662
2663     this._confirmPoPrepaySituation = function() {
2664         var want_prepay = dijit.byId("acq-lit-po-prepay").attr("checked");
2665         if (want_prepay != this._prepayRequiredByVendor) {
2666             return confirm(
2667                 want_prepay ?
2668                     localeStrings.VENDOR_SAYS_PREPAY_NOT_NEEDED :
2669                     localeStrings.VENDOR_SAYS_PREPAY_NEEDED
2670             );
2671         } else {
2672             return true;
2673         }
2674     };
2675
2676     this.applySelectedLiAction = function(action) {
2677         var self = this;
2678         switch(action) {
2679
2680             case 'delete_selected':
2681                 this._deleteLiList(self.getSelected());
2682                 break;
2683
2684             case 'add_to_order':
2685                 addToPoDialog._get_li = dojo.hitch(
2686                     this,
2687                     function() { return this.getSelected(false, null, true); }
2688                 );
2689                 addToPoDialog.show();
2690                 break;
2691
2692             case 'create_order':
2693                 this._loadPOSelect();
2694                 acqLitPoCreateDialog.show();
2695                 break;
2696
2697             case 'save_picklist':
2698                 acqLitSavePlDialog.show();
2699                 break;
2700
2701             case 'selector_ready':
2702             case 'order_ready':
2703                 acqLitChangeLiStateDialog.attr('state', action.replace('_', '-'));
2704                 acqLitChangeLiStateDialog.show();
2705                 break;
2706
2707             case 'print_po':
2708                 this.printPO();
2709                 break;
2710
2711             case 'po_history':
2712                 location.href = oilsBasePath + '/acq/po/history/' + this.isPO;
2713                 break;
2714
2715             case 'batch_create_invoice':
2716                 this.batchCreateInvoice();
2717                 break;
2718
2719             case 'batch_link_invoice':
2720                 this.batchLinkInvoice();
2721                 break;
2722
2723             case 'receive_lineitems':
2724                 this.receiveSelectedLineitems();
2725                 break;
2726
2727             case 'rollback_receive_lineitems':
2728                 this.rollbackReceiveLineitems();
2729                 break;
2730
2731             case 'create_assets':
2732                 this.showAssetCreator();
2733                 break;
2734
2735             case 'export_attr_list':
2736                 this.chooseExportAttr();
2737                 break;
2738
2739             case 'batch_apply_funds':
2740                 this.applyBatchLiFunds();
2741                 break;
2742
2743             case 'add_brief_record':
2744                 if(this.isPO)
2745                     location.href = oilsBasePath + '/acq/picklist/brief_record?po=' + this.isPO;
2746                 else
2747                     location.href = oilsBasePath + '/acq/picklist/brief_record?pl=' + this.isPL;
2748
2749                 break;
2750
2751             case "cancel_lineitems":
2752                 this.maybeCancelLineitems();
2753                 break;
2754
2755             case "apply_claim_policy":
2756                 var li_list = this.getSelected();
2757                 this.claimPolicyPicker.attr("value", null);
2758                 liClaimPolicyDialog.show();
2759                 liClaimPolicySave.onClick = function() {
2760                     self.changeClaimPolicy(
2761                         li_list,
2762                         self.claimPolicyPicker.attr("value"),
2763                         function() {
2764                             li_list.forEach(
2765                                 function(li) {
2766                                     self.setClaimPolicyControl(li);
2767                                     self.reconsiderClaimControl(li);
2768                                 }
2769                             );
2770                             liClaimPolicyDialog.hide();
2771                         }
2772                     )
2773                 };
2774                 break;
2775         }
2776     };
2777
2778     this.changeClaimPolicy = function(li_list, value, callback) {
2779         li_list.forEach(
2780             function(li) { li.claim_policy(value); }
2781         );
2782         fieldmapper.standardRequest(
2783             ["open-ils.acq", "open-ils.acq.lineitem.update"], {
2784                 "params": [openils.User.authtoken, li_list],
2785                 "async": true,
2786                 "oncomplete": function(r) {
2787                     r = openils.Util.readResponse(r);
2788                     if (callback) callback(r);
2789                 }
2790             }
2791         );
2792     };
2793
2794     this.showAssetCreator = function(onAssetsCreated) {
2795         if(!this.isPO) return;
2796         var self = this;
2797     
2798         // first, let's see if this PO has any LI's that need to be merged/imported
2799         self.pcrud.search('jub', {purchase_order : this.isPO, eg_bib_id : null}, {
2800             id_list : true,
2801             oncomplete : function(r) {
2802                 var resp = openils.Util.readResponse(r);
2803                 if (resp && resp.length) {
2804                     // PO has some non-linked jubs.  
2805                     
2806                     self.show('asset-creator');
2807                     if(!self.vlAgent.loaded)
2808                         self.vlAgent.init();
2809
2810                     dojo.connect(assetCreatorButton, 'onClick', 
2811                         function() { self.createAssets(onAssetsCreated) });
2812
2813                 } else {
2814
2815                     // all jubs linked, move on to asset creation
2816                     self.createAssets(onAssetsCreated, true); 
2817                 }
2818             }
2819         });
2820     }
2821
2822     this.createAssets = function(onAssetsCreated, noVl) {
2823         this.show('acq-lit-progress-numbers');
2824         var self = this;
2825         var vlArgs = (noVl) ? {} : {vandelay : this.vlAgent.values()};
2826         fieldmapper.standardRequest(
2827             ['open-ils.acq', 'open-ils.acq.purchase_order.assets.create'],
2828             {   async: true,
2829                 params: [this.authtoken, this.isPO, vlArgs],
2830                 onresponse: function(r) {
2831                     var resp = openils.Util.readResponse(r);
2832                     self._updateProgressNumbers(resp, !Boolean(onAssetsCreated), onAssetsCreated);
2833                 }
2834             }
2835         );
2836     }
2837
2838     this.maybeCancelLineitems = function() {
2839         openils.Util.show("acq-lit-cancel-reason", "inline");
2840         if (!acqLitCancelLineitemsButton._prepared) {
2841             var widget = new openils.widget.AutoFieldWidget({
2842                 "fmField": "cancel_reason",
2843                 "fmClass": "jub",
2844                 "parentNode": dojo.byId("acq-lit-cancel-reason-selector"),
2845                 "orgLimitPerms": ["CREATE_PURCHASE_ORDER"],
2846                 "forceSync": true
2847             });
2848             widget.build(
2849                 function(w, ww) {
2850                     acqLitCancelLineitemsButton.onClick = function() {
2851                         if (w.attr("value")) {
2852                             if (confirm(localeStrings.LI_CANCEL_CONFIRM)) {
2853                                 self._cancelLineitems(w.attr("value"));
2854                             }
2855                             openils.Util.hide("acq-lit-cancel-reason");
2856                         }
2857                     };
2858                     acqLitCancelLineitemsButton._prepared = true;
2859                 }
2860             );
2861         }
2862     };
2863
2864     this._cancelLineitems = function(reason) {
2865         var id_list = this.getSelected().map(function(o) { return o.id(); });
2866         fieldmapper.standardRequest(
2867             ["open-ils.acq", "open-ils.acq.lineitem.cancel.batch"], {
2868                 "params": [openils.User.authtoken, id_list, reason],
2869                 "async": true,
2870                 "onresponse": function(r) {
2871                     if (r = openils.Util.readResponse(r)) {
2872                         if (r.li) {
2873                             for (var id in r.li) {
2874                                 self.liCache[id].state(r.li[id].state);
2875                                 self.liCache[id].cancel_reason(
2876                                     r.li[id].cancel_reason
2877                                 );
2878                                 self.updateLiState(self.liCache[id]);
2879                             }
2880                         }
2881                         if (r.lid && self.copyCache) {
2882                             for (var id in r.lid) {
2883                                 if (self.copyCache[id]) {
2884                                     self.copyCache[id].cancel_reason(
2885                                         r.lid[id].cancel_reason
2886                                     );
2887                                     self.updateLidState(self.copyCache[id]);
2888                                 }
2889                             }
2890                         }
2891                     }
2892                 }
2893             }
2894         );
2895     };
2896
2897     this.chooseExportAttr = function() {
2898         if (!acqLitExportAttrSelector._li_setup) {
2899             var self = this;
2900             acqLitExportAttrSelector.store = new dojo.data.ItemFileReadStore(
2901                 {
2902                     "data": acqlimad.toStoreData(
2903                         this.pcrud.search(
2904                             "acqlimad", {"code": li_exportable_attrs}
2905                         )
2906                     )
2907                 }
2908             );
2909             acqLitExportAttrSelector.setValue();
2910             acqLitExportAttrButton.onClick = function(){self.exportAttrList();};
2911             acqLitExportAttrSelector._li_setup = true;
2912         }
2913         openils.Util.show("acq-lit-export-attr-holder", "inline");
2914     };
2915
2916     this.exportAttrList = function() {
2917         var attr_def = acqLitExportAttrSelector.item;
2918         var li_list = this.getSelected();
2919         var value_list = li_list.map(
2920             function(li) {
2921                 return (new openils.acq.Lineitem({"lineitem": li})).findAttr(
2922                     attr_def.code, "lineitem_marc_attr_definition"
2923                 );
2924             }
2925         ).filter(function(attr) { return Boolean(attr); });
2926
2927         if (value_list.length > 0) {
2928             if (value_list.length < li_list.length) {
2929                 if (!confirm(
2930                     dojo.string.substitute(
2931                         localeStrings.EXPORT_SHORT_LIST, [attr_def.description]
2932                     )
2933                 )) {
2934                     return;
2935                 }
2936             }
2937             try {
2938                 openils.XUL.contentToFileSaveDialog(
2939                     value_list.join("\n"),
2940                     localeStrings.EXPORT_SAVE_DIALOG_TITLE
2941                 );
2942             } catch (E) {
2943                 alert(E);
2944             }
2945         } else {
2946             alert(dojo.string.substitute(
2947                 localeStrings.EXPORT_EMPTY_LIST, [attr_def.description]
2948             ));
2949         }
2950
2951         openils.Util.hide("acq-lit-export-attr-holder");
2952     };
2953
2954     this.printPO = function() {
2955         if(!this.isPO) return;
2956         progressDialog.show(true);
2957         fieldmapper.standardRequest(
2958             ['open-ils.acq', 'open-ils.acq.purchase_order.format'],
2959             {   async: true,
2960                 params: [this.authtoken, this.isPO, 'html'],
2961                 oncomplete: function(r) {
2962                     progressDialog.hide();
2963                     var evt = openils.Util.readResponse(r);
2964                     if(evt && evt.template_output()) {
2965                         openils.Util.printHtmlString(evt.template_output().data());
2966                     }
2967                 }
2968             }
2969         );
2970     };
2971
2972     this.batchCreateInvoice = function() {
2973         var liIds = this.getSelected(false, null, true /* id_list */)
2974         if (!liIds.length) return;
2975         var path = oilsBasePath + '/acq/invoice/view?create=1';
2976         dojo.forEach(liIds, function(li, idx) { path += '&attach_li=' + li });
2977         if (openils.XUL.isXUL())
2978             openils.XUL.newTabEasy(path, localeStrings.NEW_INVOICE, null, true);
2979         else
2980             location.href = path;
2981     };
2982
2983     this.batchLinkInvoice = function(create) {
2984         var liIds = this.getSelected(false, null, true /* id_list */)
2985         if (!liIds.length) return;
2986         if (!self.invoiceLinkDialogManager) {
2987             self.invoiceLinkDialogManager =
2988                 new InvoiceLinkDialogManager("li");
2989         }
2990         self.invoiceLinkDialogManager.target = liIds;
2991         acqLitLinkInvoiceDialog.show();
2992     };
2993
2994     this.receiveSelectedLineitems = function() {
2995         var li_list = this.getSelected();
2996
2997         if (!li_list.length) {
2998             alert(localeStrings.NO_LI_GENERAL);
2999             return;
3000         }
3001
3002         for (var i = 0; i < li_list.length; i++) {
3003             var li = li_list[i];
3004
3005             if (li.state() != "received" &&
3006                 !this.checkLiAlerts(li.id())) return;
3007         }
3008
3009         this.show('acq-lit-progress-numbers');
3010
3011         var self = this;
3012         fieldmapper.standardRequest(
3013             ['open-ils.acq', 'open-ils.acq.lineitem.receive.batch'],
3014             {   async: true,
3015                 params: [
3016                     this.authtoken,
3017                     li_list.map(function(li) { return li.id(); })
3018                 ],
3019                 onresponse : function(r) {
3020                     var resp = openils.Util.readResponse(r);
3021                     self._updateProgressNumbers(resp, true);
3022                 },
3023             }
3024         );
3025     };
3026
3027     this.issueReceive = function(obj, rollback) {
3028         var part =
3029             {"jub": "lineitem", "acqlid": "lineitem_detail"}[obj.classname];
3030         var method =
3031             "open-ils.acq." + part + ".receive" + (rollback ? ".rollback" : "");
3032
3033         progressDialog.show(true);
3034         fieldmapper.standardRequest(
3035             ["open-ils.acq", method], {
3036                 "async": true,
3037                 "params": [this.authtoken, obj.id()],
3038                 "onresponse": function(r) {
3039                     if (r = openils.Util.readResponse(r)) {
3040                         self.fetchClaimInfo(
3041                             part == "lineitem" ? obj.id() : obj.lineitem(),
3042                             /* force */ true,
3043                             function() { self.handleReceive(r); }
3044                         );
3045                         progressDialog.hide();
3046                     }
3047                 }
3048             }
3049         );
3050     };
3051
3052     /**
3053      * Handles the responses from receive and rollback ML calls.
3054      */
3055     this.handleReceive = function(resp) {
3056         if (resp) {
3057             if (resp.li) {
3058                 for (var li_id in resp.li) {
3059                     for (var key in resp.li[li_id])
3060                         self.liCache[li_id][key](resp.li[li_id][key]);
3061                     self.updateLiState(self.liCache[li_id]);
3062                 }
3063             }
3064             if (resp.po) {
3065                 if (typeof(self.poUpdateCallback) == "function")
3066                     self.poUpdateCallback(resp.po);
3067             }
3068             if (resp.lid) {
3069                 for (var lid_id in resp.lid) {
3070                     for (var key in resp.lid[lid_id])
3071                         self.copyCache[lid_id][key](resp.lid[lid_id][key]);
3072                     self.updateLidState(self.copyCache[lid_id]);
3073                 }
3074             }
3075         }
3076     };
3077
3078     this.rollbackReceiveLineitems = function() {
3079         var li_id_list = this.getSelected(false, null, true);
3080         if (!li_id_list.length) {
3081             alert(localeStrings.NO_LI_GENERAL);
3082             return;
3083         }
3084
3085         if (!confirm(localeStrings.ROLLBACK_LI_RECEIVE_CONFIRM)) return;
3086
3087         this.show('acq-lit-progress-numbers');
3088         var self = this;
3089
3090         fieldmapper.standardRequest(
3091             ['open-ils.acq', 'open-ils.acq.lineitem.receive.rollback.batch'],
3092             {   async: true,
3093                 params: [this.authtoken, li_id_list],
3094                 onresponse : function(r) {
3095                     var resp = openils.Util.readResponse(r);
3096                     self._updateProgressNumbers(resp, true);
3097                 },
3098             }
3099         );
3100     };
3101
3102     this._updateProgressNumbers = function(resp, reloadOnComplete, onComplete) {
3103         this.vlAgent.handleResponse(resp,
3104             function(resp, res) {
3105                 if(reloadOnComplete)
3106                      location.href = location.href;
3107                 if (onComplete)
3108                     onComplete(resp, res);
3109             }
3110         );
3111     }
3112
3113
3114     this._createPO = function(fields) {
3115         var wantall = (fields.create_from == "all");
3116
3117         /* If we're a picklist or purchase order already and the user wants
3118          * all lineitems, we might have pages' worth of lineitems haven't all
3119          * been loaded yet, so getSelected() won't find them.  The server,
3120          * however, should know about all our lineitems, so let's ask the
3121          * server for a complete list.
3122          */
3123
3124         if (wantall) {
3125             this.getSelected(
3126                 true, function(list) {
3127                     self._createPOFromLineitems(fields, list);
3128                 }, /* id_list */ true
3129             );
3130         } else {
3131             this._createPOFromLineitems(fields, this.getSelected(false, null, true /* id_list */));
3132         }
3133     };
3134
3135     this._createPOFromLineitems = function(fields, selected) {
3136         if (selected.length == 0) return;
3137         var self = this;
3138
3139         var po = new fieldmapper.acqpo();
3140         po.provider(this.createPoProviderSelector.attr("value"));
3141         po.ordering_agency(this.createPoAgencySelector.attr("value"));
3142         po.prepayment_required(fields.prepayment_required[0] ? true : false);
3143
3144         // if we're creating assets, delay the asset creation 
3145         // until after the PO is created.  This will allow us to 
3146         // use showAssetCreator() directly.
3147
3148         fieldmapper.standardRequest(
3149             ["open-ils.acq", "open-ils.acq.purchase_order.create"],
3150             {   async: true,
3151                 params: [
3152                     openils.User.authtoken, 
3153                     po, {lineitems : selected}
3154                 ],
3155                 onresponse : function(r) {
3156                     var resp = openils.Util.readResponse(r);
3157                     if (resp.complete) {
3158                         // self.isPO is needed for showAssetCreator();
3159                         self.isPO = resp.purchase_order.id(); 
3160                         var redir = oilsBasePath + "/acq/po/view/" + self.isPO;
3161                         if (fields.create_assets[0]) {
3162                             self.showAssetCreator(
3163                                 function() {location.href = redir}
3164                             );
3165                         } else {
3166                            location.href = redir;
3167                         }
3168                     }
3169                 }
3170             }
3171         );
3172     };
3173
3174
3175     this.batchFundWidget = null;
3176
3177     this.applyBatchLiFunds = function() {
3178
3179         var liIds = this.getSelected().map(function(li) { return li.id(); });
3180         if(liIds.length == 0) return; // warn?
3181
3182         var self = this;
3183         batchFundUpdateDialog.show();
3184
3185         if(!this.batchFundWidget) {
3186             this.batchFundWidget = new openils.widget.AutoFieldWidget({
3187                 fmClass : 'acqf',
3188                 selfReference : true,
3189                 labelFormat : fundLabelFormat,
3190                 searchFormat : fundSearchFormat,
3191                 searchFilter : {"active": "t"},
3192                 parentNode : dojo.byId('acq-lit-batch-fund-selector'),
3193                 orgLimitPerms : ['CREATE_PICKLIST', 'CREATE_PURCHASE_ORDER'],
3194                 dijitArgs : { "required": true, "labelType": "html" },
3195                 forceSync : true
3196             });
3197             this.batchFundWidget.build();
3198         }
3199
3200         dojo.connect(batchFundUpdateCancel, 'onClick', function() { batchFundUpdateDialog.hide(); });
3201         dojo.connect(batchFundUpdateSubmit, 'onClick', 
3202             function() { 
3203
3204                 // TODO: call .dry_run first to test thresholds
3205                 fieldmapper.standardRequest(
3206                     ['open-ils.acq', 'open-ils.acq.lineitem.fund.update.batch'],
3207                     {
3208                         params : [
3209                             openils.User.authtoken, 
3210                             liIds,
3211                             self.batchFundWidget.widget.attr('value')
3212                         ],
3213                         oncomplete : function(r) {
3214                             var resp = openils.Util.readResponse(r);
3215                             if(resp) {
3216                                 location.href = location.href;
3217                             }
3218                         }
3219                     }
3220                 )
3221             }
3222         );
3223     }
3224
3225     this._deleteLiList = function(list, idx) {
3226         if(idx == null) idx = 0;
3227         if(idx >= list.length) return;
3228
3229         var li = list[idx];
3230         var liId = li.id();
3231
3232         if (this.isPO && (li.state() == "on-order" || li.state() == "received")) {
3233             /* It makes little sense to delete a lineitem from a PO that has
3234              * already been marked 'on-order'.  Especially if EDI is in use,
3235              * such a purchase order will probably have already been shipped
3236              * off to a vendor, and mucking with it at this point could leave
3237              * your data in a bad state that doesn't jive with reality.
3238              *
3239              * I could see making this restriction even firmer.
3240              *
3241              * I could also see adjusting the li state comparisons, extending
3242              * the comparison to the PO's state, and/or providing functions
3243              * that house the logic for comparing states in a single location.
3244              *
3245              * Yes, this will be really annoying if you have selected a lot
3246              * of lineitems to cancel that have been ordered. You'll get a
3247              * confirm dialog for each one.
3248              */
3249
3250             if (!confirm(localeStrings.DEL_LI_FROM_PO)) {
3251                 self._deleteLiList(list, ++idx); /* move on to next in list */
3252                 return;
3253             }
3254         }
3255
3256         fieldmapper.standardRequest(
3257             ['open-ils.acq',
3258              this.isPO ? 'open-ils.acq.purchase_order.lineitem.delete' : 'open-ils.acq.picklist.lineitem.delete'],
3259             {   async: true,
3260                 params: [openils.User.authtoken, liId],
3261                 oncomplete: function(r) {
3262                     self.removeLineitem(liId);
3263                     self._deleteLiList(list, ++idx);
3264                 }
3265             }
3266         );
3267     }
3268
3269     this.editOrderMarc = function(li) {
3270
3271         /*  To run in Firefox directly, must set signed.applets.codebase_principal_support
3272             to true in about:config */
3273
3274         if(openils.XUL.isXUL()) {
3275             win = window.open('/xul/' + openils.XUL.buildId() + '/server/cat/marcedit.xul','','chrome');
3276         } else {
3277             win = window.open('/xul/server/cat/marcedit.xul','','chrome'); 
3278         }
3279         var self = this;
3280         win.xulG = {
3281             record : {marc : li.marc(), "rtype": "bre"},
3282             save : {
3283                 label: 'Save Record', // XXX I18N
3284                 func: function(xmlString) {
3285                     li.marc(xmlString);
3286                     fieldmapper.standardRequest(
3287                         ['open-ils.acq', 'open-ils.acq.lineitem.update'],
3288                         {   async: true,
3289                             params: [openils.User.authtoken, li],
3290                             oncomplete: function(r) {
3291                                 openils.Util.readResponse(r);
3292                                 win.close();
3293                                 self.drawInfo(li.id())
3294                             }
3295                         }
3296                     );
3297                 },
3298             },
3299             'lock_tab' : typeof xulG != 'undefined' ? (typeof xulG['lock_tab'] != 'undefined' ? xulG.lock_tab : undefined) : undefined,
3300             'unlock_tab' : typeof xulG != 'undefined' ? (typeof xulG['unlock_tab'] != 'undefined' ? xulG.unlock_tab : undefined) : undefined
3301         };
3302     }
3303
3304     this._savePl = function(values) {
3305         this.getSelected(
3306             (values.which == 'all'),
3307             function(list) { self._savePlFromLineitems(values, list); }
3308         );
3309     };
3310
3311     this._savePlFromLineitems = function(values, selected) {
3312         openils.Util.show("acq-lit-generic-progress");
3313
3314         if(values.new_name) {
3315             openils.acq.Picklist.create(
3316                 {name: values.new_name},
3317                 function(id) {
3318                     self._updateLiList(
3319                         id, selected, 0,
3320                         function() {
3321                             location.href =
3322                                 oilsBasePath + "/acq/picklist/view/" + id;
3323                         }
3324                     );
3325                 }
3326             );
3327         } else if(values.existing_pl) {
3328             // update lineitems to use an existing picklist
3329             self._updateLiList(
3330                 values.existing_pl, selected, 0,
3331                 function(){
3332                     location.href =
3333                         oilsBasePath + "/acq/picklist/view/" +
3334                         values.existing_pl;
3335                 }
3336             );
3337         }
3338     };
3339
3340     this._updateLiState = function(values, state) {
3341         progressDialog.show(true);
3342         this.getSelected(
3343             (values.which == 'all'),
3344             function(list) {
3345                 self._updateLiStateFromLineitems(values, state, list);
3346             }
3347         );
3348     };
3349
3350     this._updateLiStateFromLineitems = function(values, state, selected) {
3351         if(!selected.length) return;
3352         dojo.forEach(selected, function(li) {li.state(state);});
3353         self._updateLiList(null, selected, 0,
3354             // TODO consider inline updates for efficiency
3355             function() { location.href = location.href }
3356         );
3357     };
3358
3359     this._updateLiList = function(pl, list, idx, oncomplete) {
3360         if(idx >= list.length) return oncomplete();
3361         var li = list[idx];
3362         if(pl != null) li.picklist(pl);
3363         litGenericProgress.update({maximum: list.length, progress: idx});
3364         new openils.acq.Lineitem({lineitem:li}).update(
3365             function(r) {
3366                 self._updateLiList(pl, list, ++idx, oncomplete);
3367             }
3368         );
3369     }
3370
3371     this._loadPOSelect = function() {
3372         if (!this.createPoProviderSelector) {
3373             var widget = new openils.widget.AutoFieldWidget({
3374                 "fmField": "provider",
3375                 "fmClass": "acqpo",
3376                 "searchFilter": {"active": "t"},
3377                 "parentNode": dojo.byId("acq-lit-po-provider"),
3378                 "dijitArgs": {
3379                     "onChange": function() {
3380                         if (this.item) {
3381                             self._updateCreatePoPrepayCheckbox(
3382                                 this.item.prepayment_required()
3383                             );
3384                         }
3385                     }
3386                 }
3387             });
3388             widget.build(function(w) { self.createPoProviderSelector = w; });
3389         }
3390
3391         if (!this.createPoAgencySelector) {
3392             var widget = new openils.widget.AutoFieldWidget({
3393                 "fmField": "ordering_agency",
3394                 "fmClass": "acqpo",
3395                 "parentNode": dojo.byId("acq-lit-po-agency"),
3396                 "orgLimitPerms": ["CREATE_PURCHASE_ORDER"],
3397             });
3398             widget.build(function(w) { self.createPoAgencySelector = w; });
3399         }
3400     };
3401
3402     this.showRealCopyEditUI = function(li) {
3403         copyList = [];
3404         var self = this;
3405         this.volCache = {};
3406
3407         this._fetchLineitem(li.id(), 
3408             function(fullLi) {
3409                 li = self.liCache[li.id()] = fullLi;
3410
3411                 self.pcrud.search(
3412                     'acp', {
3413                         id : li.lineitem_details().map(
3414                             function(item) { return item.eg_copy_id() }
3415                         )
3416                     }, {
3417                         async : true,
3418                         oncomplete : function(r) {
3419                             try {
3420                                 var r_list = openils.Util.readResponse( r );
3421                                 for (var i = 0; i < r_list.length; i++) {
3422                                     var copy = r_list[i];
3423                                     var volId = copy.call_number();
3424                                     var volume = self.volCache[volId];
3425                                     if(!volume) {
3426                                         volume = self.volCache[volId] = self.pcrud.retrieve('acn', volId);
3427                                     }
3428                                     copy.call_number(volume);
3429                                     copyList.push(copy);
3430                                 }
3431                                 if (xulG) {
3432                                     xulG.volume_item_creator( { 'existing_copies' : copyList } );
3433                                 }
3434                             } catch(E) {
3435                                 alert('error in oncomplete: ' + E);
3436                             }
3437                         }
3438                     }
3439                 );
3440             }
3441         );
3442     },
3443
3444     this.drawBibFinder = function(li) {
3445
3446         var query = '';
3447         var liWrapper = new openils.acq.Lineitem({lineitem:li});
3448
3449         dojo.forEach(
3450             ['isbn', 'upc', 'issn', 'title', 'author'],
3451             function(field) {
3452                 var val = liWrapper.findAttr(field, 'lineitem_marc_attr_definition');
3453                 if(val) {
3454                     if(field == 'title' || field == 'author') {
3455                         query += field +':' + val + ' ';
3456                     } else {
3457                         query += 'identifier|' + field + ':' + val + ' ';
3458                     }
3459                 }
3460             }
3461         );
3462
3463         win = window.open(
3464             oilsBasePath + '/acq/lineitem/findbib?query=' + escape(query),
3465             '', 'resizable,scrollbars=1,chrome');
3466
3467         win.window.recordFound = function(bibId) { 
3468             win.close();
3469
3470             var attrs = li.attributes();
3471             li.attributes(null);
3472             li.eg_bib_id(bibId);
3473
3474             fieldmapper.standardRequest(
3475                 ["open-ils.acq", "open-ils.acq.lineitem.update"], 
3476                 {
3477                     "params": [openils.User.authtoken, li],
3478                     "async": true,
3479                     "oncomplete": function(r) {
3480                         if(openils.Util.readResponse(r)) {
3481                             location.href = location.href;
3482                         }
3483                     }
3484                 }
3485             );
3486         }
3487     }
3488 }
3489