]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/acq/common/li_table.js
f007bf6b8b7235795e15d31f7df98edd76cbd88c
[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         while(this.copyTbody.childNodes[0])
1738             this.copyTbody.removeChild(this.copyTbody.childNodes[0]);
1739
1740         this._drawBatchCopyWidgets();
1741
1742         this._drawDistribApplied(liId);
1743
1744         this._fetchDistribFormulas(
1745             function() {
1746                 openils.acq.Lineitem.fetchAttrDefs(
1747                     function() { 
1748                         self._fetchLineitem(liId, function(li){self._drawCopies(li);}, force_fetch); 
1749                     } 
1750                 );
1751             }
1752         );
1753     };
1754
1755     this._saveDistribAppliedTemplates = function() {
1756         if (!this._appliedDistribTemplate) {
1757             this._appliedDistribTemplate =
1758                 dojo.byId("acq-lit-distrib-applied-tbody").
1759                     removeChild(dojo.byId("acq-lit-distrib-applied-row"));
1760             dojo.attr(this._appliedDistribTemplate, "id");
1761         }
1762     };
1763
1764     this._drawDistribApplied = function(liId) {
1765         /* Build this table while hidden to prevent rendering artifacts */
1766         openils.Util.hide("acq-lit-distrib-applied-tbody");
1767
1768         this._saveDistribAppliedTemplates();
1769
1770         /* Remove any rows in the table from previous populations */
1771         dojo.query("tr[formula]", "acq-lit-distrib-applied-tbody").
1772             forEach(dojo.destroy);
1773
1774         /* Unregister all dijits previously created (for some reason this isn't
1775          * covered by the above destroy calls). */
1776         dijit.registry.forEach(
1777             function(w) { if (/^dfa-/.test(w.id)) w.destroyRecursive(); }
1778         );
1779
1780         /* Populate the table with our liId */
1781         var total = 0;
1782         fieldmapper.standardRequest(
1783             ["open-ils.acq",
1784             "open-ils.acq.distribution_formula_application.ranged.retrieve"],
1785             {
1786                 "async": true,
1787                 "params": [self.authtoken, liId],
1788                 "onresponse": function(r) {
1789                     var dfa = openils.Util.readResponse(r);
1790                     if (dfa) {
1791                         total++;
1792                         self.realDfaCache[dfa.id()] = dfa;
1793                         self._drawDistribAppliedUnit(dfa);
1794                     }
1795                 },
1796                 "oncomplete": function() {
1797                     /* Reveal built table */
1798                     if (total) {
1799                         openils.Util.show(
1800                             "acq-lit-distrib-applied-tbody", "table-row-group"
1801                         );
1802                     }
1803                 }
1804             }
1805         );
1806     };
1807
1808     this._drawDistribAppliedUnit = function(dfa) {
1809         var new_row = false;
1810         var row = dojo.query(
1811             'tr[formula="' + dfa.formula().id() + '"]',
1812             "acq-lit-distrib-applied-tbody"
1813         )[0];
1814
1815         if (!row) {
1816             new_row = true;
1817             row = dojo.clone(this._appliedDistribTemplate);
1818             dojo.attr(row, "formula", dfa.formula().id());
1819             dojo.query("th", row)[0].innerHTML = dfa.formula().name();
1820         }
1821
1822         var td = dojo.query("td", row)[0];
1823
1824         dojo.create("span", {"id": "dfa-button-" + dfa.id()}, td, "last");
1825         dojo.create("span", {"id": "dfa-tip-" + dfa.id()}, td, "last");
1826
1827         if (new_row)
1828             dojo.place(row, "acq-lit-distrib-applied-tbody", "last");
1829
1830         new dijit.form.Button(
1831             {
1832                 "onClick": function() {
1833                     if (confirm(localeStrings.EXPLAIN_DFA_MGMT))
1834                         self.deleteDfa(dfa);
1835                 },
1836                 "label": "X",
1837                 /* XXX I /cannot/ make the following work in as a CSS class
1838                  * for some reason. So frustrating... */
1839                 "style": function(id) {
1840                      return (id > 0 ?
1841                         "font-weight: bold; color: #c00;" :
1842                         "color: #666;");
1843                      }(dfa.id()) + "margin: 0 6px;display: inline;"
1844             }, "dfa-button-" + dfa.id()
1845         );
1846         new dijit.Tooltip(
1847             {
1848                 "connectId": ["dfa-button-" + dfa.id()],
1849                 "label": dojo.string.substitute(
1850                     localeStrings.DFA_TIP, dfa.id() > 0 ? [
1851                         openils.User.formalName(dfa.creator()),
1852                         dojo.date.locale.format(
1853                             dojo.date.stamp.fromISOString(dfa.create_time()),
1854                             {"formatLength":"short"}
1855                         )
1856                     ] : [localeStrings.ITS_YOU, localeStrings.JUST_NOW]
1857                 )
1858             }, "dfa-tip-" + dfa.id()
1859         );
1860     }
1861
1862     this.deleteDfa = function(dfa) {
1863         if (dfa.id() > 0) { /* real */
1864             this.pcrud.eliminate(
1865                 dfa, {
1866                     "async": true,
1867                     "oncomplete": function() {
1868                         self._removeDistribApplied(dfa.id());
1869                         delete self.realDfaCache[dfa.id()];
1870                     }
1871                 }
1872             );
1873         } else { /* virtual */
1874             if (--(this.virtDfaCounts[dfa.formula().id()]) < 0)
1875             this.virtDfaCounts[dfa.formula().id()] = 0;
1876             /* hasn't been saved yet, so no need to do anything server side */
1877             this._removeDistribApplied(dfa.id());
1878         }
1879
1880     };
1881
1882     this._removeDistribApplied = function(dfaId) {
1883         var re = new RegExp("^dfa-\\w+-" + String(dfaId));
1884         dijit.registry.forEach(
1885             function(w) { if (re.test(w.id)) w.destroyRecursive(); }
1886         );
1887         this._removeDistribAppliedEmptyRows();
1888     };
1889
1890     this._removeAllDistribAppliedVirtual = function() {
1891         /* Unregister dijits */
1892         dijit.registry.forEach(
1893             function(w) { if (/^dfa-\w+--/.test(w.id)) w.destroyRecursive(); }
1894         );
1895         this._removeDistribAppliedEmptyRows();
1896     };
1897
1898     this._removeDistribAppliedEmptyRows = function() {
1899         /* Remove any rows with no DFA at all */
1900         dojo.query("tr[formula] td", "acq-lit-distrib-applied-tbody").forEach(
1901             function(o) {
1902                 if (o.childNodes.length < 1) dojo.destroy(o.parentNode);
1903             }
1904         );
1905     };
1906
1907     /**
1908      * Insert a new row into the distribution formula selection form
1909      */
1910     this._addDistribFormulaRow = function() {
1911         var self = this;
1912
1913         if (!self.distribForms) {
1914             // no formulas, hide the form
1915             openils.Util.hide('acq-lit-distrib-formula-table');
1916             return;
1917         }
1918
1919         if(!this.distribFormulaTemplate) 
1920             this.distribFormulaTemplate = 
1921                 dojo.byId('acq-lit-distrib-formula-tbody').removeChild(dojo.byId('acq-lit-distrib-form-row'));
1922
1923         var row = this.distribFormulaTemplate.cloneNode(true);
1924         dojo.place(row, "acq-lit-distrib-formula-tbody", "only");
1925
1926         this.dfSelector = new dijit.form.FilteringSelect(
1927             {"labelAttr": "dynLabel", "labelType": "html"},
1928             nodeByName("selector", row)
1929         );
1930         this._updateFormulaStore();
1931         this.dfSelector.fetchProperties =
1932             {"sort": [{"attribute": "use_count", "descending": true}]};
1933
1934         var apply = new dijit.form.Button(
1935             {"label": localeStrings.APPLY},
1936             nodeByName('set_button', row)
1937         ); 
1938
1939         var reset = new dijit.form.Button(
1940             {"label": localeStrings.RESET_FORMULAE, "disabled": true},
1941             nodeByName("reset_button", row)  
1942         );
1943
1944         dojo.connect(apply, 'onClick', 
1945             function() {
1946                 var form_id = self.dfSelector.attr("value");
1947                 if(!form_id) return;
1948                 self._applyDistribFormula(form_id);
1949                 reset.attr("disabled", false);
1950             }
1951         );
1952
1953         dojo.connect(reset, 'onClick', 
1954             function() {
1955                 self.restoreCopyFieldsBeforeDF();
1956                 self.virtDfaCounts = {};
1957                 self.virtDfaId = -1;
1958                 self.dfeOffset = 0;
1959                 self._updateFormulaStore();
1960                 self._removeAllDistribAppliedVirtual();
1961                 reset.attr("disabled", "true");
1962             }
1963         );
1964
1965     };
1966
1967     /**
1968      * Applies a distrib formula to the current set of copies
1969      */
1970     this._applyDistribFormula = function(formula) {
1971         if(!formula) return;
1972
1973         formula = this.distribForms.filter(
1974             function(form) { return form.id() == formula; }
1975         )[0];
1976
1977         var copyRows = dojo.query('tr', self.copyTbody);
1978
1979         if (this.dfeOffset >= copyRows.length) {
1980             alert(localeStrings.OUT_OF_COPIES);
1981             return;
1982         }
1983
1984         var entries_applied = 0;
1985         for(
1986             var rowIndex = this.dfeOffset;
1987             rowIndex < copyRows.length;
1988             rowIndex++
1989         ) {
1990             
1991             var row = copyRows[rowIndex];
1992             var copy_id = row.getAttribute('copy_id');
1993             var copyWidgets = this.copyWidgetCache[copy_id];
1994             var entryIndex = this.dfeOffset;
1995             var entry = null;
1996
1997             // find the correct entry for the current row
1998             dojo.forEach(formula.entries(), 
1999                 function(e) {
2000                     if(!entry) {
2001                         entryIndex += e.item_count();
2002                         if(entryIndex > rowIndex)
2003                             entry = e;
2004                     }
2005                 }
2006             );
2007
2008             if(entry) {
2009                 
2010                 //console.log("rowIndex = " + rowIndex + ", entry = " + entry.id() + ", entryIndex=" + 
2011                 //  entryIndex + ", owning_lib = " + entry.owning_lib() + ", location = " + entry.location());
2012     
2013                 entries_applied++;
2014                 this.saveCopyFieldsBeforeDF(copy_id);
2015                 this._copy_fields_for_acqdf.forEach(
2016                     function(field) {
2017                         if(entry[field]()) {
2018                             copyWidgets[field].attr('value', (entry[field]()));
2019                         }
2020                     }
2021                 );
2022             }
2023         }
2024
2025         if (entries_applied) {
2026             this.virtDfaCounts[formula.id()] =
2027                 ++(this.virtDfaCounts[formula.id()]) || 1;
2028             this._updateFormulaStore();
2029             this._drawDistribAppliedUnit(
2030                 function(df) {
2031                     var dfa = new acqdfa();
2032                     dfa.formula(df); dfa.id(self.virtDfaId--); return dfa;
2033                 }(formula)
2034             );
2035             this.dfeOffset += entries_applied;
2036         };
2037     };
2038
2039     /**
2040      * This function updates the DF store for the dropdown so that use_counts
2041      * can reflect DF applications from this session before they're saved
2042      * server-side.
2043      */
2044     this._updateFormulaStore = function() {
2045         this.dfSelector.store = new dojo.data.ItemFileReadStore(
2046             {
2047                 "data": self._labelFormulasWithCounts(
2048                     acqdf.toStoreData(self.distribForms)
2049                 )
2050             }
2051         );
2052     };
2053
2054     this.saveCopyFieldsBeforeDF = function(copy_id) {
2055         var self = this;
2056         if (!this.oldCopyWidgetCache[copy_id]) {
2057             var copyWidgets = this.copyWidgetCache[copy_id];
2058
2059             this.oldCopyWidgetCache[copy_id] = {};
2060             this._copy_fields_for_acqdf.forEach(
2061                 function(f) {
2062                     self.oldCopyWidgetCache[copy_id][f] =
2063                         copyWidgets[f].attr("value");
2064                 }
2065             );
2066         }
2067     };
2068
2069     this.restoreCopyFieldsBeforeDF = function() {
2070         var self = this;
2071         for (var copy_id in this.oldCopyWidgetCache) {
2072             this._copy_fields_for_acqdf.forEach(
2073                 function(f) {
2074                     self.copyWidgetCache[copy_id][f].attr(
2075                         "value", self.oldCopyWidgetCache[copy_id][f]
2076                     );
2077                 }
2078             );
2079         }
2080     };
2081
2082     this._labelFormulasWithCounts = function(store_data) {
2083         for (var key in store_data.items) {
2084             var obj = store_data.items[key];
2085             obj.use_count = Number(obj.use_count); /* needed for sorting */
2086
2087             if (this.virtDfaCounts[obj.id])
2088                 obj.use_count = obj.use_count + Number(this.virtDfaCounts[obj.id]);
2089
2090             obj.dynLabel = "<span class='acq-lit-distrib-form-use-count'>[" +
2091                 obj.use_count + "]</span>&nbsp; " + obj.name;
2092         }
2093         return store_data;
2094     };
2095
2096     /**
2097      * This method formerly would not refetch the DF formulas if they'd been
2098      * loaded already, but now it always re-fetches, since use_count changes.
2099      */
2100     /** TODO: port distrib-formula selector to autofieldwidget+pcrud/dojo store */
2101     this._fetchDistribFormulas = function(onload) {
2102         fieldmapper.standardRequest(
2103             ["open-ils.acq",
2104                 "open-ils.acq.distribution_formula.ranged.retrieve.atomic"],
2105             {
2106                 "async": true,
2107                 "params": [openils.User.authtoken, 0, 500],
2108                 "oncomplete": function(r) {
2109                     self.distribForms = openils.Util.readResponse(r);
2110                     if(!self.distribForms || self.distribForms.length == 0) {
2111                         self.distribForms = [];
2112                     }
2113                     self._addDistribFormulaRow();
2114                     onload();
2115                 }
2116             }
2117         );
2118     }
2119
2120     this._drawBatchCopyWidgets = function() {
2121         var row = this.copyBatchRow;
2122         dojo.forEach(liDetailBatchFields, 
2123             function(field) {
2124                 if(self.copyBatchRowDrawn) {
2125                     self.copyBatchWidgets[field].attr('value', null);
2126                 } else {
2127                     var args = self.afwCopyFieldArgs(field, "CREATE_PICKLIST");
2128                     args.parentNode = dojo.query('[name='+field+']', row)[0];
2129
2130                     var widget = new openils.widget.AutoFieldWidget(args);
2131                     widget.build(
2132                         function(w, ww) {
2133                             if (field == "fund" && w.store)
2134                                 self._ensureCSSFundClasses(w.store);
2135                             self.copyBatchWidgets[field] = w;
2136                         }
2137                     );
2138                     if (field == "fund") {
2139                         dojo.connect(
2140                             widget.widget, "onChange", function(val) {
2141                                 self._updateFundSelectorStyle(widget, val);
2142                             }
2143                         );
2144                     }
2145                 }
2146             }
2147         );
2148         this.copyBatchRowDrawn = true;
2149     };
2150
2151     this.batchCopyUpdate = function() {
2152         var self = this;
2153         for(var k in this.copyWidgetCache) {
2154             var cache = this.copyWidgetCache[k];
2155             dojo.forEach(liDetailBatchFields, function(f) {
2156                 var newval = self.copyBatchWidgets[f].attr('value');
2157                 if(newval) cache[f].attr('value', newval);
2158             });
2159         }
2160     };
2161
2162     this._drawCopies = function(li) {
2163         var self = this;
2164
2165         // this button sets the total number of copies for a given lineitem
2166         acqLitAddCopyCount.onClick = function() { 
2167             var count = acqLitCopyCountInput.attr('value');
2168
2169             // add new rows
2170             while(self.copyCount() < count)
2171                 self.addCopy(li); 
2172             
2173             // delete rows if necessary
2174             var diff = self.copyCount() - count;
2175             if(diff > 0) {
2176                 var rows = dojo.query('tr', self.copyTbody).reverse().slice(0, diff);
2177                 if(confirm(dojo.string.substitute(localeStrings.DELETE_LI_COPIES_CONFIRM, [diff]))) {
2178                     dojo.forEach(rows, function(row) {self.deleteCopy(row); });
2179                 } else {
2180                     acqLitCopyCountInput.attr('value', self.copyCount()+'');
2181                 }
2182             }
2183         }
2184
2185
2186         if(li.lineitem_details().length > 0) {
2187             dojo.forEach(li.lineitem_details(),
2188                 function(copy) {
2189                     self.addCopy(li, copy);
2190                 }
2191             );
2192         } else {
2193             self.addCopy(li);
2194         }
2195     };
2196
2197     this.copyCount = function() {
2198         var count = 0;
2199         for(var id in this.copyCache) {
2200             if(!this.copyCache[id].isdeleted())
2201                 count++;
2202         }
2203         return count;
2204     }
2205
2206     this.virtCopyId = -1;
2207     this.addCopy = function(li, copy) {
2208         var row = this.copyRow.cloneNode(true);
2209         this.copyTbody.appendChild(row);
2210         var self = this;
2211
2212         if(!copy) {
2213             copy = new fieldmapper.acqlid();
2214             copy.isnew(true);
2215             copy.id(this.virtCopyId--);
2216             copy.lineitem(li.id());
2217         }
2218
2219         this.copyCache[copy.id()] = copy;
2220         row.setAttribute('copy_id', copy.id());
2221         self.copyWidgetCache[copy.id()] = {};
2222
2223         acqLitCopyCountInput.attr('value', self.copyCount()+'');
2224
2225         var rcvr = copy.receiver();
2226         if (rcvr) {
2227             if (!userCache[rcvr]) {
2228                 if(rcvr == openils.User.user.id()) {
2229                     userCache[rcvr] = openils.User.user;
2230                 } else {
2231                     userCache[rcvr] = fieldmapper.standardRequest(
2232                         ['open-ils.actor', 'open-ils.actor.user.retrieve'],
2233                         {params: [openils.User.authtoken, rcvr]}
2234                     );
2235                 }
2236             }
2237             dojo.query('[name=receiver]', row)[0].innerHTML =  userCache[rcvr].usrname();
2238         }
2239
2240         dojo.forEach(liDetailFields,
2241             function(field) {
2242                 var searchFilter;
2243                 if (field == "fund") {
2244                     searchFilter = (copy.fund() ?
2245                         {"-or": {"active": "t", "id": copy.fund()}} :
2246                         {"active" : "t"});
2247                 } else {
2248                     searchFilter = null;
2249                 }
2250
2251                 var readOnly = false;
2252                 
2253                 // TODO: Add support for changing the owning_lib after real copies have been made.  
2254                 // owning_lib is order data as much as its item data
2255                 if(copy.eg_copy_id() && ['owning_lib', 'location', 'circ_modifier', 'cn_label', 'barcode'].indexOf(field) >= 0) {
2256                     readOnly = true;
2257                 }
2258
2259                 // TODO: add support for changing the fund after debits have been created
2260                 // Note: invoicing allows the change
2261                 if(copy.fund_debit() && field == 'fund') {
2262                     readOnly = true;
2263                 }
2264
2265
2266                 var widget = new openils.widget.AutoFieldWidget({
2267                     fmObject : copy,
2268                     fmField : field,
2269                     labelFormat : (field == 'fund') ? fundLabelFormat : null,
2270                     searchFormat : (field == 'fund') ? fundSearchFormat : null,
2271                     dijitArgs: {"labelType": (field == 'fund') ? "html" : null},
2272                     searchFilter : searchFilter,
2273                     noCache: (field == "fund"),
2274                     fmClass : 'acqlid',
2275                     parentNode : dojo.query('[name='+field+']', row)[0],
2276                     orgLimitPerms : ['CREATE_PICKLIST', 'CREATE_PURCHASE_ORDER'],
2277                     readOnly : readOnly,
2278                     orgDefaultsToWs : true
2279                 });
2280
2281                 widget.build(
2282                     // make sure we capture the value from any async widgets
2283                     function(w, ww) { 
2284
2285                         if (field == "fund" && w.store)
2286                             self._ensureCSSFundClasses(w.store);
2287
2288                         if(!readOnly) 
2289                             copy[field](ww.getFormattedValue()) 
2290
2291                         self.copyWidgetCache[copy.id()][field] = w;
2292
2293                         dojo.connect(w, 'onChange', 
2294                             function(val) { 
2295                                 if (field == "fund")
2296                                     self._updateFundSelectorStyle(widget, val);
2297
2298                                 if (!readOnly && (copy.isnew() || val != copy[field]())) {
2299                                     // prevent setting ischanged() automatically on widget load for existing copies
2300                                     copy[field](widget.getFormattedValue()) 
2301                                     copy.ischanged(true);
2302                                 }
2303                             }
2304                         );
2305                     }
2306                 );
2307             }
2308         );
2309
2310         this.updateLidState(copy, row);
2311     };
2312
2313     this._ensureCSSFundClass = function(id) {
2314         if (!this.fundStyleSheet) {
2315             dojo.create(
2316                 "style", {"type": "text/css"},
2317                 document.getElementsByTagName("head")[0], "last"
2318             );
2319             this.fundStyleSheet = document.styleSheets[
2320                 document.styleSheets.length - 1
2321             ];
2322         }
2323
2324         var cn = "fund_" + id;
2325         if (!this.haveFundClass[cn]) {
2326             fieldmapper.standardRequest(
2327                 ["open-ils.acq", "open-ils.acq.fund.check_balance_percentages"],
2328                 {
2329                     "params": [openils.User.authtoken, id],
2330                     "async": true,
2331                     "oncomplete": function(r) {
2332                         r = openils.Util.readResponse(r);
2333                         self.fundBalanceState[id] = r;
2334                         var style = "";
2335                         if (r[0] /* stop */)
2336                             style = fundStyles.stop;
2337                         else if (r[1] /* warning */)
2338                             style = fundStyles.warning;
2339                         self.fundStyleSheet.insertRule(
2340                             "." + cn + " { " + style + " }",
2341                             self.fundStyleSheet.cssRules.length
2342                         );
2343                         self.haveFundClass[cn] = true;
2344                     }
2345                 }
2346             );
2347         }
2348     };
2349
2350     this._ensureCSSFundClasses = function(store) {
2351         store.fetch({
2352             "query": {"id": "*"},
2353             "onItem": function(o) { self._ensureCSSFundClass(o.id[0]); }
2354         });
2355     };
2356
2357     this._updateFundSelectorStyle = function(widget, fund_id) {
2358         openils.Util.removeCSSClass(widget.widget.domNode, /fund_\d+/);
2359         openils.Util.addCSSClass(widget.widget.domNode, "fund_" + fund_id);
2360     };
2361
2362     this.updateLidState = function(copy, row) {
2363         var self = this;
2364
2365         if (typeof(row) == "undefined") {
2366             row = dojo.query('tr[copy_id="' + copy.id() + '"]', this.copyTbody)[0];
2367         }
2368
2369         // action links
2370         var recv_link = nodeByName("receive", row);
2371         var unrecv_link = nodeByName("unreceive", row);
2372         var del_link = nodeByName("delete", row);
2373         var cxl_link = nodeByName("cancel", row);
2374         var claim_link = nodeByName("claim", row);
2375         var cxl_reason_link = nodeByName("cancel_reason", row);
2376
2377         // by default, hide all the actions
2378         openils.Util.hide(del_link.parentNode);
2379         openils.Util.hide(recv_link);
2380         openils.Util.hide(unrecv_link);
2381         openils.Util.hide(cxl_link);
2382         openils.Util.hide(claim_link);
2383         openils.Util.hide(cxl_reason_link);
2384
2385         if (copy.id() > 0) { // real copies (LIDs)
2386
2387             if (copy.cancel_reason()) { 
2388
2389                 /* --------- cancelled -------------------------- */
2390
2391                 /* XXX the following may leak memory in a long lived table: 
2392                  * dijits may not get destroyed... not positive. revisit. */
2393                 var holds_reason = dojo.create(
2394                     "span", {
2395                         "style": "border-bottom: 1px dashed #000;",
2396                         "innerHTML": "Cancelled" /* XXX [sic] and i18n */
2397                     }, cxl_reason_link, "only"
2398                 );
2399                 new dijit.Tooltip(
2400                     {
2401                         "label": "<em>" + copy.cancel_reason().label() +
2402                             "</em><br />" + copy.cancel_reason().description(),
2403                         "connectId": [holds_reason]
2404                     }, dojo.create("span", null, cxl_reason_link, "last")
2405                 );
2406                 openils.Util.show(cxl_reason_link, "inline");
2407
2408             } else if (copy.recv_time()) { 
2409
2410                 /* --------- received -------------------------- */
2411
2412                 openils.Util.show(unrecv_link, "inline");
2413                 unrecv_link.onclick = function() {
2414                     if (confirm(localeStrings.UNRECEIVE_LID))
2415                         self.issueReceive(copy, /* rollback */ true);
2416                 };
2417
2418             } else if (this.liCache[copy.lineitem()].state() == 'on-order') {
2419                 
2420                 /* --------- on order -------------------------- */
2421
2422                 openils.Util.show(recv_link, 'inline');
2423                 openils.Util.show(cxl_link, "inline");
2424
2425                 recv_link.onclick = function() {
2426                     if (self.checkLiAlerts(copy.lineitem()))
2427                         self.issueReceive(copy);
2428                 };
2429
2430                 cxl_link.onclick = function() { self.cancelLid(copy.id()) };
2431
2432             } else {
2433
2434                 /* --------- pre-order copies  -------------------------- */
2435
2436                 del_link.onclick = function() { self.deleteCopy(row) };
2437                 openils.Util.show(del_link.parentNode);
2438
2439             }
2440
2441         } else { 
2442
2443             /* --------- virtual copies  -------------------------- */
2444
2445             del_link.onclick = function() { self.deleteCopy(row) };
2446             openils.Util.show(del_link.parentNode);
2447         }
2448     };
2449
2450     this.cancelLid = function(lid_id) {
2451         lidCancelDialog._lid_id = lid_id;
2452         openils.Util.show(lidCancelDialog.domNode.parentNode);
2453         lidCancelDialog.show();
2454         if (!lidCancelDialog._prepared) {
2455             var widget = new openils.widget.AutoFieldWidget({
2456                 "fmField": "cancel_reason",
2457                 "fmClass": "acqlid",
2458                 "parentNode": dojo.byId("acq-lit-lid-cancel-reason"),
2459                 "orgLimitPerms": ["CREATE_PURCHASE_ORDER"],
2460                 "forceSync": true
2461             });
2462             widget.build(
2463                 function(w, ww) {
2464                     acqLidCancelButton.onClick = function() {
2465                         if (w.attr("value")) {
2466                             if (confirm(localeStrings.LID_CANCEL_CONFIRM)) {
2467                                 self._cancelLid(
2468                                     lidCancelDialog._lid_id,
2469                                     w.attr("value")
2470                                 );
2471                             }
2472                             lidCancelDialog.hide();
2473                         }
2474                     };
2475                     lidCancelDialog._prepared = true;
2476                 }
2477             );
2478         }
2479     };
2480
2481     this._cancelLid = function(lid_id, reason) {
2482         fieldmapper.standardRequest(
2483             ["open-ils.acq", "open-ils.acq.lineitem_detail.cancel"], {
2484                 "params": [openils.User.authtoken, lid_id, reason],
2485                 "async": true,
2486                 "onresponse": function(r) {
2487                     if (r = openils.Util.readResponse(r)) {
2488                         if (r.lid) {
2489                             for (var id in r.lid) {
2490                                 /* actually this should only iterate once */
2491                                 self.copyCache[id].cancel_reason(
2492                                     r.lid[id].cancel_reason
2493                                 );
2494                                 self.updateLidState(self.copyCache[id]);
2495                             }
2496                         }
2497                     }
2498                 }
2499             }
2500         );
2501     };
2502
2503     this._confirmAlert = function(li, lin) {
2504         return confirm(
2505             dojo.string.substitute(
2506                 localeStrings.CONFIRM_LI_ALERT, [
2507                     (new openils.acq.Lineitem({"lineitem": li})).findAttr(
2508                         "title", "lineitem_marc_attr_definition"
2509                     ), (
2510                         /* XXX it's really better add a parameter and to adjust
2511                          * the format string rather than do this concatenation
2512                          * here, but if someone wants this for 2.2 in a hurry,
2513                          * we can sidestep the problem of updating the strings
2514                          * while the translators are working. */
2515                         "[" +
2516                         aou.findOrgUnit(lin.alert_text().owning_lib()).shortname() +
2517                         "] " +
2518                         lin.alert_text().code()
2519                     ),
2520                     lin.alert_text().description() || "",
2521                     lin.value()
2522                 ]
2523             )
2524         );
2525     };
2526
2527     this.checkLiAlerts = function(li_id) {
2528         var li = this.liCache[li_id];
2529
2530         var alert_notes = li.lineitem_notes().filter(
2531             function(o) { return Boolean(o.alert_text()); }
2532         );
2533
2534         /* this is _intentionally_ not done in a call to forEach() ... */
2535         for (var i = 0; i < alert_notes.length; i++) {
2536             if (this.noteAcks[alert_notes[i].id()])
2537                 continue;
2538             else if (!this._confirmAlert(li, alert_notes[i]))
2539                 return false;
2540             else
2541                 this.noteAcks[alert_notes[i].id()] = true;
2542         }
2543
2544         return true;
2545     };
2546
2547     this.deleteCopy = function(row) {
2548         var copy = this.copyCache[row.getAttribute('copy_id')];
2549         copy.isdeleted(true);
2550         if(copy.isnew())
2551             delete this.copyCache[copy.id()];
2552         this.copyTbody.removeChild(row);
2553     }
2554
2555     this._virtDfaCountsAsList = function() {
2556         var L = [];
2557         for (var key in this.virtDfaCounts) {
2558             for (var i = 0; i < this.virtDfaCounts[key]; i++)
2559                 L.push(key);
2560         }
2561         return L;
2562     }
2563
2564     this.confirmBreachedCopyFunds = function(copies) {
2565         var stop = 0, warning = 0;
2566         copies.forEach(
2567             function(o) {
2568                 if (o.fund()) {
2569                     var state = self.fundBalanceState[o.fund()];
2570                     if (state[0] /* stop */)
2571                         stop++;
2572                     else if (state[1] /* warning */)
2573                         warning++;
2574                 }
2575             }
2576         );
2577
2578         if (stop) {
2579             return confirm(localeStrings.CONFIRM_FUNDS_AT_STOP);
2580         } else if (warning) {
2581             return confirm(localeStrings.CONFIRM_FUNDS_AT_WARNING);
2582         }
2583         return true;
2584     };
2585
2586     this.saveCopyChanges = function(liId) {
2587         var self = this;
2588         var copies = [];
2589
2590
2591         var total = 0;
2592         for(var id in this.copyCache) {
2593             var c = this.copyCache[id];
2594             if(!c.isdeleted()) total++;
2595             if(c.isnew() || c.ischanged() || c.isdeleted()) {
2596                 if(c.id() < 0) c.id(null);
2597                 copies.push(c);
2598             }
2599         }
2600
2601
2602         dojo.byId('acq-lit-copy-count-label-' + liId).innerHTML = total;
2603
2604
2605         if (copies.length > 0) {
2606             if (!this.confirmBreachedCopyFunds(copies))
2607                 return;
2608
2609             if (typeof(this._copy_count_cb) == "function")
2610                 this._copy_count_cb(liId, total);
2611
2612             openils.Util.show("acq-lit-update-copies-progress");
2613             fieldmapper.standardRequest(
2614                 ['open-ils.acq', 'open-ils.acq.lineitem_detail.cud.batch'],
2615                 {   async: true,
2616                     params: [openils.User.authtoken, copies],
2617                     onresponse: function(r) {
2618                         var res = openils.Util.readResponse(r);
2619                         litUpdateCopiesProgress.update(res);
2620                     },
2621                     oncomplete: function() {
2622                         self.drawCopies(liId, true /* force_fetch */);
2623                         openils.Util.hide("acq-lit-update-copies-progress");
2624                     }
2625                 }
2626             );
2627         }
2628
2629         var dfa_list = this._virtDfaCountsAsList();
2630         if (dfa_list.length > 0) {
2631             fieldmapper.standardRequest(
2632                 ["open-ils.acq",
2633                 "open-ils.acq.distribution_formula.record_application"],
2634                 {
2635                     "async": true,
2636                     "params": [openils.User.authtoken, dfa_list, liId],
2637                     "onresponse": function(r) {
2638                         var res = openils.Util.readResponse(r);
2639                         if (res && res.length < dfa_list.length)
2640                             alert(localeStrings.DFA_NOT_ALL);
2641                     }
2642                 }
2643             );
2644             this.virtDfaCounts = {};
2645         }
2646
2647         if (this.inlineCopiesNeedingRefresh.indexOf(liId) < 0)
2648             this.inlineCopiesNeedingRefresh.push(liId);
2649     };
2650
2651     this._updateCreatePoPrepayCheckbox = function(prepay) {
2652         var prepay = openils.Util.isTrue(prepay);
2653         this._prepayRequiredByVendor = prepay;
2654         dijit.byId("acq-lit-po-prepay").attr("checked", prepay);
2655     };
2656
2657     this._confirmPoPrepaySituation = function() {
2658         var want_prepay = dijit.byId("acq-lit-po-prepay").attr("checked");
2659         if (want_prepay != this._prepayRequiredByVendor) {
2660             return confirm(
2661                 want_prepay ?
2662                     localeStrings.VENDOR_SAYS_PREPAY_NOT_NEEDED :
2663                     localeStrings.VENDOR_SAYS_PREPAY_NEEDED
2664             );
2665         } else {
2666             return true;
2667         }
2668     };
2669
2670     this.applySelectedLiAction = function(action) {
2671         var self = this;
2672         switch(action) {
2673
2674             case 'delete_selected':
2675                 this._deleteLiList(self.getSelected());
2676                 break;
2677
2678             case 'add_to_order':
2679                 addToPoDialog._get_li = dojo.hitch(
2680                     this,
2681                     function() { return this.getSelected(false, null, true); }
2682                 );
2683                 addToPoDialog.show();
2684                 break;
2685
2686             case 'create_order':
2687                 this._loadPOSelect();
2688                 acqLitPoCreateDialog.show();
2689                 break;
2690
2691             case 'save_picklist':
2692                 acqLitSavePlDialog.show();
2693                 break;
2694
2695             case 'selector_ready':
2696             case 'order_ready':
2697                 acqLitChangeLiStateDialog.attr('state', action.replace('_', '-'));
2698                 acqLitChangeLiStateDialog.show();
2699                 break;
2700
2701             case 'print_po':
2702                 this.printPO();
2703                 break;
2704
2705             case 'po_history':
2706                 location.href = oilsBasePath + '/acq/po/history/' + this.isPO;
2707                 break;
2708
2709             case 'batch_create_invoice':
2710                 this.batchCreateInvoice();
2711                 break;
2712
2713             case 'batch_link_invoice':
2714                 this.batchLinkInvoice();
2715                 break;
2716
2717             case 'receive_lineitems':
2718                 this.receiveSelectedLineitems();
2719                 break;
2720
2721             case 'rollback_receive_lineitems':
2722                 this.rollbackReceiveLineitems();
2723                 break;
2724
2725             case 'create_assets':
2726                 this.showAssetCreator();
2727                 break;
2728
2729             case 'export_attr_list':
2730                 this.chooseExportAttr();
2731                 break;
2732
2733             case 'batch_apply_funds':
2734                 this.applyBatchLiFunds();
2735                 break;
2736
2737             case 'add_brief_record':
2738                 if(this.isPO)
2739                     location.href = oilsBasePath + '/acq/picklist/brief_record?po=' + this.isPO;
2740                 else
2741                     location.href = oilsBasePath + '/acq/picklist/brief_record?pl=' + this.isPL;
2742
2743                 break;
2744
2745             case "cancel_lineitems":
2746                 this.maybeCancelLineitems();
2747                 break;
2748
2749             case "apply_claim_policy":
2750                 var li_list = this.getSelected();
2751                 this.claimPolicyPicker.attr("value", null);
2752                 liClaimPolicyDialog.show();
2753                 liClaimPolicySave.onClick = function() {
2754                     self.changeClaimPolicy(
2755                         li_list,
2756                         self.claimPolicyPicker.attr("value"),
2757                         function() {
2758                             li_list.forEach(
2759                                 function(li) {
2760                                     self.setClaimPolicyControl(li);
2761                                     self.reconsiderClaimControl(li);
2762                                 }
2763                             );
2764                             liClaimPolicyDialog.hide();
2765                         }
2766                     )
2767                 };
2768                 break;
2769         }
2770     };
2771
2772     this.changeClaimPolicy = function(li_list, value, callback) {
2773         li_list.forEach(
2774             function(li) { li.claim_policy(value); }
2775         );
2776         fieldmapper.standardRequest(
2777             ["open-ils.acq", "open-ils.acq.lineitem.update"], {
2778                 "params": [openils.User.authtoken, li_list],
2779                 "async": true,
2780                 "oncomplete": function(r) {
2781                     r = openils.Util.readResponse(r);
2782                     if (callback) callback(r);
2783                 }
2784             }
2785         );
2786     };
2787
2788     this.showAssetCreator = function(onAssetsCreated) {
2789         if(!this.isPO) return;
2790         var self = this;
2791     
2792         // first, let's see if this PO has any LI's that need to be merged/imported
2793         self.pcrud.search('jub', {purchase_order : this.isPO, eg_bib_id : null}, {
2794             id_list : true,
2795             oncomplete : function(r) {
2796                 var resp = openils.Util.readResponse(r);
2797                 if (resp && resp.length) {
2798                     // PO has some non-linked jubs.  
2799                     
2800                     self.show('asset-creator');
2801                     if(!self.vlAgent.loaded)
2802                         self.vlAgent.init();
2803
2804                     dojo.connect(assetCreatorButton, 'onClick', 
2805                         function() { self.createAssets(onAssetsCreated) });
2806
2807                 } else {
2808
2809                     // all jubs linked, move on to asset creation
2810                     self.createAssets(onAssetsCreated, true); 
2811                 }
2812             }
2813         });
2814     }
2815
2816     this.createAssets = function(onAssetsCreated, noVl) {
2817         this.show('acq-lit-progress-numbers');
2818         var self = this;
2819         var vlArgs = (noVl) ? {} : {vandelay : this.vlAgent.values()};
2820         fieldmapper.standardRequest(
2821             ['open-ils.acq', 'open-ils.acq.purchase_order.assets.create'],
2822             {   async: true,
2823                 params: [this.authtoken, this.isPO, vlArgs],
2824                 onresponse: function(r) {
2825                     var resp = openils.Util.readResponse(r);
2826                     self._updateProgressNumbers(resp, !Boolean(onAssetsCreated), onAssetsCreated);
2827                 }
2828             }
2829         );
2830     }
2831
2832     this.maybeCancelLineitems = function() {
2833         openils.Util.show("acq-lit-cancel-reason", "inline");
2834         if (!acqLitCancelLineitemsButton._prepared) {
2835             var widget = new openils.widget.AutoFieldWidget({
2836                 "fmField": "cancel_reason",
2837                 "fmClass": "jub",
2838                 "parentNode": dojo.byId("acq-lit-cancel-reason-selector"),
2839                 "orgLimitPerms": ["CREATE_PURCHASE_ORDER"],
2840                 "forceSync": true
2841             });
2842             widget.build(
2843                 function(w, ww) {
2844                     acqLitCancelLineitemsButton.onClick = function() {
2845                         if (w.attr("value")) {
2846                             if (confirm(localeStrings.LI_CANCEL_CONFIRM)) {
2847                                 self._cancelLineitems(w.attr("value"));
2848                             }
2849                             openils.Util.hide("acq-lit-cancel-reason");
2850                         }
2851                     };
2852                     acqLitCancelLineitemsButton._prepared = true;
2853                 }
2854             );
2855         }
2856     };
2857
2858     this._cancelLineitems = function(reason) {
2859         var id_list = this.getSelected().map(function(o) { return o.id(); });
2860         fieldmapper.standardRequest(
2861             ["open-ils.acq", "open-ils.acq.lineitem.cancel.batch"], {
2862                 "params": [openils.User.authtoken, id_list, reason],
2863                 "async": true,
2864                 "onresponse": function(r) {
2865                     if (r = openils.Util.readResponse(r)) {
2866                         if (r.li) {
2867                             for (var id in r.li) {
2868                                 self.liCache[id].state(r.li[id].state);
2869                                 self.liCache[id].cancel_reason(
2870                                     r.li[id].cancel_reason
2871                                 );
2872                                 self.updateLiState(self.liCache[id]);
2873                             }
2874                         }
2875                         if (r.lid && self.copyCache) {
2876                             for (var id in r.lid) {
2877                                 if (self.copyCache[id]) {
2878                                     self.copyCache[id].cancel_reason(
2879                                         r.lid[id].cancel_reason
2880                                     );
2881                                     self.updateLidState(self.copyCache[id]);
2882                                 }
2883                             }
2884                         }
2885                     }
2886                 }
2887             }
2888         );
2889     };
2890
2891     this.chooseExportAttr = function() {
2892         if (!acqLitExportAttrSelector._li_setup) {
2893             var self = this;
2894             acqLitExportAttrSelector.store = new dojo.data.ItemFileReadStore(
2895                 {
2896                     "data": acqlimad.toStoreData(
2897                         this.pcrud.search(
2898                             "acqlimad", {"code": li_exportable_attrs}
2899                         )
2900                     )
2901                 }
2902             );
2903             acqLitExportAttrSelector.setValue();
2904             acqLitExportAttrButton.onClick = function(){self.exportAttrList();};
2905             acqLitExportAttrSelector._li_setup = true;
2906         }
2907         openils.Util.show("acq-lit-export-attr-holder", "inline");
2908     };
2909
2910     this.exportAttrList = function() {
2911         var attr_def = acqLitExportAttrSelector.item;
2912         var li_list = this.getSelected();
2913         var value_list = li_list.map(
2914             function(li) {
2915                 return (new openils.acq.Lineitem({"lineitem": li})).findAttr(
2916                     attr_def.code, "lineitem_marc_attr_definition"
2917                 );
2918             }
2919         ).filter(function(attr) { return Boolean(attr); });
2920
2921         if (value_list.length > 0) {
2922             if (value_list.length < li_list.length) {
2923                 if (!confirm(
2924                     dojo.string.substitute(
2925                         localeStrings.EXPORT_SHORT_LIST, [attr_def.description]
2926                     )
2927                 )) {
2928                     return;
2929                 }
2930             }
2931             try {
2932                 openils.XUL.contentToFileSaveDialog(
2933                     value_list.join("\n"),
2934                     localeStrings.EXPORT_SAVE_DIALOG_TITLE
2935                 );
2936             } catch (E) {
2937                 alert(E);
2938             }
2939         } else {
2940             alert(dojo.string.substitute(
2941                 localeStrings.EXPORT_EMPTY_LIST, [attr_def.description]
2942             ));
2943         }
2944
2945         openils.Util.hide("acq-lit-export-attr-holder");
2946     };
2947
2948     this.printPO = function() {
2949         if(!this.isPO) return;
2950         progressDialog.show(true);
2951         fieldmapper.standardRequest(
2952             ['open-ils.acq', 'open-ils.acq.purchase_order.format'],
2953             {   async: true,
2954                 params: [this.authtoken, this.isPO, 'html'],
2955                 oncomplete: function(r) {
2956                     progressDialog.hide();
2957                     var evt = openils.Util.readResponse(r);
2958                     if(evt && evt.template_output()) {
2959                         openils.Util.printHtmlString(evt.template_output().data());
2960                     }
2961                 }
2962             }
2963         );
2964     };
2965
2966     this.batchCreateInvoice = function() {
2967         var liIds = this.getSelected(false, null, true /* id_list */)
2968         if (!liIds.length) return;
2969         var path = oilsBasePath + '/acq/invoice/view?create=1';
2970         dojo.forEach(liIds, function(li, idx) { path += '&attach_li=' + li });
2971         if (openils.XUL.isXUL())
2972             openils.XUL.newTabEasy(path, localeStrings.NEW_INVOICE, null, true);
2973         else
2974             location.href = path;
2975     };
2976
2977     this.batchLinkInvoice = function(create) {
2978         var liIds = this.getSelected(false, null, true /* id_list */)
2979         if (!liIds.length) return;
2980         if (!self.invoiceLinkDialogManager) {
2981             self.invoiceLinkDialogManager =
2982                 new InvoiceLinkDialogManager("li");
2983         }
2984         self.invoiceLinkDialogManager.target = liIds;
2985         acqLitLinkInvoiceDialog.show();
2986     };
2987
2988     this.receiveSelectedLineitems = function() {
2989         var li_list = this.getSelected();
2990
2991         if (!li_list.length) {
2992             alert(localeStrings.NO_LI_GENERAL);
2993             return;
2994         }
2995
2996         for (var i = 0; i < li_list.length; i++) {
2997             var li = li_list[i];
2998
2999             if (li.state() != "received" &&
3000                 !this.checkLiAlerts(li.id())) return;
3001         }
3002
3003         this.show('acq-lit-progress-numbers');
3004
3005         var self = this;
3006         fieldmapper.standardRequest(
3007             ['open-ils.acq', 'open-ils.acq.lineitem.receive.batch'],
3008             {   async: true,
3009                 params: [
3010                     this.authtoken,
3011                     li_list.map(function(li) { return li.id(); })
3012                 ],
3013                 onresponse : function(r) {
3014                     var resp = openils.Util.readResponse(r);
3015                     self._updateProgressNumbers(resp, true);
3016                 },
3017             }
3018         );
3019     };
3020
3021     this.issueReceive = function(obj, rollback) {
3022         var part =
3023             {"jub": "lineitem", "acqlid": "lineitem_detail"}[obj.classname];
3024         var method =
3025             "open-ils.acq." + part + ".receive" + (rollback ? ".rollback" : "");
3026
3027         progressDialog.show(true);
3028         fieldmapper.standardRequest(
3029             ["open-ils.acq", method], {
3030                 "async": true,
3031                 "params": [this.authtoken, obj.id()],
3032                 "onresponse": function(r) {
3033                     if (r = openils.Util.readResponse(r)) {
3034                         self.fetchClaimInfo(
3035                             part == "lineitem" ? obj.id() : obj.lineitem(),
3036                             /* force */ true,
3037                             function() { self.handleReceive(r); }
3038                         );
3039                         progressDialog.hide();
3040                     }
3041                 }
3042             }
3043         );
3044     };
3045
3046     /**
3047      * Handles the responses from receive and rollback ML calls.
3048      */
3049     this.handleReceive = function(resp) {
3050         if (resp) {
3051             if (resp.li) {
3052                 for (var li_id in resp.li) {
3053                     for (var key in resp.li[li_id])
3054                         self.liCache[li_id][key](resp.li[li_id][key]);
3055                     self.updateLiState(self.liCache[li_id]);
3056                 }
3057             }
3058             if (resp.po) {
3059                 if (typeof(self.poUpdateCallback) == "function")
3060                     self.poUpdateCallback(resp.po);
3061             }
3062             if (resp.lid) {
3063                 for (var lid_id in resp.lid) {
3064                     for (var key in resp.lid[lid_id])
3065                         self.copyCache[lid_id][key](resp.lid[lid_id][key]);
3066                     self.updateLidState(self.copyCache[lid_id]);
3067                 }
3068             }
3069         }
3070     };
3071
3072     this.rollbackReceiveLineitems = function() {
3073         var li_id_list = this.getSelected(false, null, true);
3074         if (!li_id_list.length) {
3075             alert(localeStrings.NO_LI_GENERAL);
3076             return;
3077         }
3078
3079         if (!confirm(localeStrings.ROLLBACK_LI_RECEIVE_CONFIRM)) return;
3080
3081         this.show('acq-lit-progress-numbers');
3082         var self = this;
3083
3084         fieldmapper.standardRequest(
3085             ['open-ils.acq', 'open-ils.acq.lineitem.receive.rollback.batch'],
3086             {   async: true,
3087                 params: [this.authtoken, li_id_list],
3088                 onresponse : function(r) {
3089                     var resp = openils.Util.readResponse(r);
3090                     self._updateProgressNumbers(resp, true);
3091                 },
3092             }
3093         );
3094     };
3095
3096     this._updateProgressNumbers = function(resp, reloadOnComplete, onComplete) {
3097         this.vlAgent.handleResponse(resp,
3098             function(resp, res) {
3099                 if(reloadOnComplete)
3100                      location.href = location.href;
3101                 if (onComplete)
3102                     onComplete(resp, res);
3103             }
3104         );
3105     }
3106
3107
3108     this._createPO = function(fields) {
3109         var wantall = (fields.create_from == "all");
3110
3111         /* If we're a picklist or purchase order already and the user wants
3112          * all lineitems, we might have pages' worth of lineitems haven't all
3113          * been loaded yet, so getSelected() won't find them.  The server,
3114          * however, should know about all our lineitems, so let's ask the
3115          * server for a complete list.
3116          */
3117
3118         if (wantall) {
3119             this.getSelected(
3120                 true, function(list) {
3121                     self._createPOFromLineitems(fields, list);
3122                 }, /* id_list */ true
3123             );
3124         } else {
3125             this._createPOFromLineitems(fields, this.getSelected(false, null, true /* id_list */));
3126         }
3127     };
3128
3129     this._createPOFromLineitems = function(fields, selected) {
3130         if (selected.length == 0) return;
3131         var self = this;
3132
3133         var po = new fieldmapper.acqpo();
3134         po.provider(this.createPoProviderSelector.attr("value"));
3135         po.ordering_agency(this.createPoAgencySelector.attr("value"));
3136         po.prepayment_required(fields.prepayment_required[0] ? true : false);
3137
3138         // if we're creating assets, delay the asset creation 
3139         // until after the PO is created.  This will allow us to 
3140         // use showAssetCreator() directly.
3141
3142         fieldmapper.standardRequest(
3143             ["open-ils.acq", "open-ils.acq.purchase_order.create"],
3144             {   async: true,
3145                 params: [
3146                     openils.User.authtoken, 
3147                     po, {lineitems : selected}
3148                 ],
3149                 onresponse : function(r) {
3150                     var resp = openils.Util.readResponse(r);
3151                     if (resp.complete) {
3152                         // self.isPO is needed for showAssetCreator();
3153                         self.isPO = resp.purchase_order.id(); 
3154                         var redir = oilsBasePath + "/acq/po/view/" + self.isPO;
3155                         if (fields.create_assets[0]) {
3156                             self.showAssetCreator(
3157                                 function() {location.href = redir}
3158                             );
3159                         } else {
3160                            location.href = redir;
3161                         }
3162                     }
3163                 }
3164             }
3165         );
3166     };
3167
3168
3169     this.batchFundWidget = null;
3170
3171     this.applyBatchLiFunds = function() {
3172
3173         var liIds = this.getSelected().map(function(li) { return li.id(); });
3174         if(liIds.length == 0) return; // warn?
3175
3176         var self = this;
3177         batchFundUpdateDialog.show();
3178
3179         if(!this.batchFundWidget) {
3180             this.batchFundWidget = new openils.widget.AutoFieldWidget({
3181                 fmClass : 'acqf',
3182                 selfReference : true,
3183                 labelFormat : fundLabelFormat,
3184                 searchFormat : fundSearchFormat,
3185                 searchFilter : {"active": "t"},
3186                 parentNode : dojo.byId('acq-lit-batch-fund-selector'),
3187                 orgLimitPerms : ['CREATE_PICKLIST', 'CREATE_PURCHASE_ORDER'],
3188                 dijitArgs : { "required": true, "labelType": "html" },
3189                 forceSync : true
3190             });
3191             this.batchFundWidget.build();
3192         }
3193
3194         dojo.connect(batchFundUpdateCancel, 'onClick', function() { batchFundUpdateDialog.hide(); });
3195         dojo.connect(batchFundUpdateSubmit, 'onClick', 
3196             function() { 
3197
3198                 // TODO: call .dry_run first to test thresholds
3199                 fieldmapper.standardRequest(
3200                     ['open-ils.acq', 'open-ils.acq.lineitem.fund.update.batch'],
3201                     {
3202                         params : [
3203                             openils.User.authtoken, 
3204                             liIds,
3205                             self.batchFundWidget.widget.attr('value')
3206                         ],
3207                         oncomplete : function(r) {
3208                             var resp = openils.Util.readResponse(r);
3209                             if(resp) {
3210                                 location.href = location.href;
3211                             }
3212                         }
3213                     }
3214                 )
3215             }
3216         );
3217     }
3218
3219     this._deleteLiList = function(list, idx) {
3220         if(idx == null) idx = 0;
3221         if(idx >= list.length) return;
3222
3223         var li = list[idx];
3224         var liId = li.id();
3225
3226         if (this.isPO && (li.state() == "on-order" || li.state() == "received")) {
3227             /* It makes little sense to delete a lineitem from a PO that has
3228              * already been marked 'on-order'.  Especially if EDI is in use,
3229              * such a purchase order will probably have already been shipped
3230              * off to a vendor, and mucking with it at this point could leave
3231              * your data in a bad state that doesn't jive with reality.
3232              *
3233              * I could see making this restriction even firmer.
3234              *
3235              * I could also see adjusting the li state comparisons, extending
3236              * the comparison to the PO's state, and/or providing functions
3237              * that house the logic for comparing states in a single location.
3238              *
3239              * Yes, this will be really annoying if you have selected a lot
3240              * of lineitems to cancel that have been ordered. You'll get a
3241              * confirm dialog for each one.
3242              */
3243
3244             if (!confirm(localeStrings.DEL_LI_FROM_PO)) {
3245                 self._deleteLiList(list, ++idx); /* move on to next in list */
3246                 return;
3247             }
3248         }
3249
3250         fieldmapper.standardRequest(
3251             ['open-ils.acq',
3252              this.isPO ? 'open-ils.acq.purchase_order.lineitem.delete' : 'open-ils.acq.picklist.lineitem.delete'],
3253             {   async: true,
3254                 params: [openils.User.authtoken, liId],
3255                 oncomplete: function(r) {
3256                     self.removeLineitem(liId);
3257                     self._deleteLiList(list, ++idx);
3258                 }
3259             }
3260         );
3261     }
3262
3263     this.editOrderMarc = function(li) {
3264
3265         /*  To run in Firefox directly, must set signed.applets.codebase_principal_support
3266             to true in about:config */
3267
3268         if(openils.XUL.isXUL()) {
3269             win = window.open('/xul/' + openils.XUL.buildId() + '/server/cat/marcedit.xul','','chrome');
3270         } else {
3271             win = window.open('/xul/server/cat/marcedit.xul','','chrome'); 
3272         }
3273         var self = this;
3274         win.xulG = {
3275             record : {marc : li.marc(), "rtype": "bre"},
3276             save : {
3277                 label: 'Save Record', // XXX I18N
3278                 func: function(xmlString) {
3279                     li.marc(xmlString);
3280                     fieldmapper.standardRequest(
3281                         ['open-ils.acq', 'open-ils.acq.lineitem.update'],
3282                         {   async: true,
3283                             params: [openils.User.authtoken, li],
3284                             oncomplete: function(r) {
3285                                 openils.Util.readResponse(r);
3286                                 win.close();
3287                                 self.drawInfo(li.id())
3288                             }
3289                         }
3290                     );
3291                 },
3292             },
3293             'lock_tab' : typeof xulG != 'undefined' ? (typeof xulG['lock_tab'] != 'undefined' ? xulG.lock_tab : undefined) : undefined,
3294             'unlock_tab' : typeof xulG != 'undefined' ? (typeof xulG['unlock_tab'] != 'undefined' ? xulG.unlock_tab : undefined) : undefined
3295         };
3296     }
3297
3298     this._savePl = function(values) {
3299         this.getSelected(
3300             (values.which == 'all'),
3301             function(list) { self._savePlFromLineitems(values, list); }
3302         );
3303     };
3304
3305     this._savePlFromLineitems = function(values, selected) {
3306         openils.Util.show("acq-lit-generic-progress");
3307
3308         if(values.new_name) {
3309             openils.acq.Picklist.create(
3310                 {name: values.new_name},
3311                 function(id) {
3312                     self._updateLiList(
3313                         id, selected, 0,
3314                         function() {
3315                             location.href =
3316                                 oilsBasePath + "/acq/picklist/view/" + id;
3317                         }
3318                     );
3319                 }
3320             );
3321         } else if(values.existing_pl) {
3322             // update lineitems to use an existing picklist
3323             self._updateLiList(
3324                 values.existing_pl, selected, 0,
3325                 function(){
3326                     location.href =
3327                         oilsBasePath + "/acq/picklist/view/" +
3328                         values.existing_pl;
3329                 }
3330             );
3331         }
3332     };
3333
3334     this._updateLiState = function(values, state) {
3335         progressDialog.show(true);
3336         this.getSelected(
3337             (values.which == 'all'),
3338             function(list) {
3339                 self._updateLiStateFromLineitems(values, state, list);
3340             }
3341         );
3342     };
3343
3344     this._updateLiStateFromLineitems = function(values, state, selected) {
3345         if(!selected.length) return;
3346         dojo.forEach(selected, function(li) {li.state(state);});
3347         self._updateLiList(null, selected, 0,
3348             // TODO consider inline updates for efficiency
3349             function() { location.href = location.href }
3350         );
3351     };
3352
3353     this._updateLiList = function(pl, list, idx, oncomplete) {
3354         if(idx >= list.length) return oncomplete();
3355         var li = list[idx];
3356         if(pl != null) li.picklist(pl);
3357         litGenericProgress.update({maximum: list.length, progress: idx});
3358         new openils.acq.Lineitem({lineitem:li}).update(
3359             function(r) {
3360                 self._updateLiList(pl, list, ++idx, oncomplete);
3361             }
3362         );
3363     }
3364
3365     this._loadPOSelect = function() {
3366         if (!this.createPoProviderSelector) {
3367             var widget = new openils.widget.AutoFieldWidget({
3368                 "fmField": "provider",
3369                 "fmClass": "acqpo",
3370                 "searchFilter": {"active": "t"},
3371                 "parentNode": dojo.byId("acq-lit-po-provider"),
3372                 "dijitArgs": {
3373                     "onChange": function() {
3374                         if (this.item) {
3375                             self._updateCreatePoPrepayCheckbox(
3376                                 this.item.prepayment_required()
3377                             );
3378                         }
3379                     }
3380                 }
3381             });
3382             widget.build(function(w) { self.createPoProviderSelector = w; });
3383         }
3384
3385         if (!this.createPoAgencySelector) {
3386             var widget = new openils.widget.AutoFieldWidget({
3387                 "fmField": "ordering_agency",
3388                 "fmClass": "acqpo",
3389                 "parentNode": dojo.byId("acq-lit-po-agency"),
3390                 "orgLimitPerms": ["CREATE_PURCHASE_ORDER"],
3391             });
3392             widget.build(function(w) { self.createPoAgencySelector = w; });
3393         }
3394     };
3395
3396     this.showRealCopyEditUI = function(li) {
3397         copyList = [];
3398         var self = this;
3399         this.volCache = {};
3400
3401         this._fetchLineitem(li.id(), 
3402             function(fullLi) {
3403                 li = self.liCache[li.id()] = fullLi;
3404
3405                 self.pcrud.search(
3406                     'acp', {
3407                         id : li.lineitem_details().map(
3408                             function(item) { return item.eg_copy_id() }
3409                         )
3410                     }, {
3411                         async : true,
3412                         oncomplete : function(r) {
3413                             try {
3414                                 var r_list = openils.Util.readResponse( r );
3415                                 for (var i = 0; i < r_list.length; i++) {
3416                                     var copy = r_list[i];
3417                                     var volId = copy.call_number();
3418                                     var volume = self.volCache[volId];
3419                                     if(!volume) {
3420                                         volume = self.volCache[volId] = self.pcrud.retrieve('acn', volId);
3421                                     }
3422                                     copy.call_number(volume);
3423                                     copyList.push(copy);
3424                                 }
3425                                 if (xulG) {
3426                                     xulG.volume_item_creator( { 'existing_copies' : copyList } );
3427                                 }
3428                             } catch(E) {
3429                                 alert('error in oncomplete: ' + E);
3430                             }
3431                         }
3432                     }
3433                 );
3434             }
3435         );
3436     },
3437
3438     this.drawBibFinder = function(li) {
3439
3440         var query = '';
3441         var liWrapper = new openils.acq.Lineitem({lineitem:li});
3442
3443         dojo.forEach(
3444             ['isbn', 'upc', 'issn', 'title', 'author'],
3445             function(field) {
3446                 var val = liWrapper.findAttr(field, 'lineitem_marc_attr_definition');
3447                 if(val) {
3448                     if(field == 'title' || field == 'author') {
3449                         query += field +':' + val + ' ';
3450                     } else {
3451                         query += 'identifier|' + field + ':' + val + ' ';
3452                     }
3453                 }
3454             }
3455         );
3456
3457         win = window.open(
3458             oilsBasePath + '/acq/lineitem/findbib?query=' + escape(query),
3459             '', 'resizable,scrollbars=1,chrome');
3460
3461         win.window.recordFound = function(bibId) { 
3462             win.close();
3463
3464             var attrs = li.attributes();
3465             li.attributes(null);
3466             li.eg_bib_id(bibId);
3467
3468             fieldmapper.standardRequest(
3469                 ["open-ils.acq", "open-ils.acq.lineitem.update"], 
3470                 {
3471                     "params": [openils.User.authtoken, li],
3472                     "async": true,
3473                     "oncomplete": function(r) {
3474                         if(openils.Util.readResponse(r)) {
3475                             location.href = location.href;
3476                         }
3477                     }
3478                 }
3479             );
3480         }
3481     }
3482 }
3483