]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/web/js/ui/default/acq/common/li_table.js
More Invoice UI work. This covers:
[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('openils.User');
10 dojo.require('openils.Util');
11 dojo.require('openils.acq.Lineitem');
12 dojo.require('openils.acq.PO');
13 dojo.require('openils.acq.Picklist');
14 dojo.require('openils.widget.AutoFieldWidget');
15 dojo.require('dojo.data.ItemFileReadStore');
16 dojo.require('openils.widget.ProgressDialog');
17 dojo.require('openils.PermaCrud');
18 dojo.require('openils.XUL');
19
20 dojo.requireLocalization('openils.acq', 'acq');
21 var localeStrings = dojo.i18n.getLocalization('openils.acq', 'acq');
22 const XUL_OPAC_WRAPPER = 'chrome://open_ils_staff_client/content/cat/opac.xul';
23 var li_exportable_attrs = ["issn", "isbn", "upc"];
24
25 var fundLabelFormat = [
26     '<span class="fund_${0}">${1} (${2})</span>', 'id', 'code', 'year'
27 ];
28 var fundSearchFormat = ['${0} (${1})', 'code', 'year'];
29
30 function nodeByName(name, context) {
31     return dojo.query('[name='+name+']', context)[0];
32 }
33
34
35 var liDetailBatchFields = ['fund', 'owning_lib', 'location', 'collection_code', 'circ_modifier', 'cn_label'];
36 var liDetailFields = liDetailBatchFields.concat(['barcode', 'note']);
37 var fundStyles = {
38     "stop": "color: #c00; font-weight: bold;",
39     "warning": "color: #c93;"
40 };
41
42 function AcqLiTable() {
43
44     var self = this;
45     this.liCache = {};
46     this.plCache = {};
47     this.poCache = {};
48     this.relCache = {};
49     this.haveFundClass = {}
50     this.fundBalanceState = {};
51     this.realDfaCache = {};
52     this.virtDfaCounts = {};
53     this.virtDfaId = -1;
54     this.dfeOffset = 0;
55     this.toggleState = false;
56     this.tbody = dojo.byId('acq-lit-tbody');
57     this.selectors = [];
58     this.noteAcks = {};
59     this.authtoken = openils.User.authtoken;
60     this.pcrud = new openils.PermaCrud();
61     this.rowTemplate = this.tbody.removeChild(dojo.byId('acq-lit-row'));
62     this.copyTbody = dojo.byId('acq-lit-li-details-tbody');
63     this.copyRow = this.copyTbody.removeChild(dojo.byId('acq-lit-li-details-row'));
64     this.copyBatchRow = dojo.byId('acq-lit-li-details-batch-row');
65     this.copyBatchWidgets = {};
66     this.liNotesTbody = dojo.byId('acq-lit-notes-tbody');
67     this.liNotesRow = this.liNotesTbody.removeChild(dojo.byId('acq-lit-notes-row'));
68     this.realCopiesTbody = dojo.byId('acq-lit-real-copies-tbody');
69     this.realCopiesRow = this.realCopiesTbody.removeChild(dojo.byId('acq-lit-real-copies-row'));
70     this._copy_fields_for_acqdf = ['owning_lib', 'location'];
71
72     dojo.connect(acqLitLiActionsSelector, 'onChange', 
73         function() { 
74             self.applySelectedLiAction(this.attr('value')) 
75             acqLitLiActionsSelector.attr('value', '_');
76         });
77
78     acqLitCreatePoSubmit.onClick = function() {
79         if (self._confirmPoPrepaySituation()) {
80             acqLitPoCreateDialog.hide();
81             self._createPO(acqLitPoCreateDialog.getValues());
82         } else {
83             return false;
84         }
85     }
86
87     acqLitSavePlButton.onClick = function() {
88         acqLitSavePlDialog.hide();
89         self._savePl(acqLitSavePlDialog.getValues());
90     }
91
92     acqLitCancelLiStateButton.onClick = function() {
93         acqLitChangeLiStateDialog.hide();
94     }
95     acqLitSaveLiStateButton.onClick = function() {
96         acqLitChangeLiStateDialog.hide();
97         self._updateLiState(acqLitChangeLiStateDialog.getValues(), acqLitChangeLiStateDialog.attr('state'));
98     }
99
100
101     dojo.byId('acq-lit-select-toggle').onclick = function(){self.toggleSelect()};
102     dojo.byId('acq-lit-info-back-button').onclick = function(){self.show('list')};
103     dojo.byId('acq-lit-copies-back-button').onclick = function(){self.show('list')};
104     dojo.byId('acq-lit-notes-back-button').onclick = function(){self.show('list')};
105     dojo.byId('acq-lit-real-copies-back-button').onclick = function(){self.show('list')};
106
107     this.reset = function() {
108         while(self.tbody.childNodes[0])
109             self.tbody.removeChild(self.tbody.childNodes[0]);
110         self.selectors = [];
111         self.noteAcks = {};
112         self.relCache = {};
113     };
114     
115     this.setNext = function(handler) {
116         var link = dojo.byId('acq-lit-next');
117         if(handler) {
118             dojo.style(link, 'visibility', 'visible');
119             link.onclick = handler;
120         } else {
121             dojo.style(link, 'visibility', 'hidden');
122         }
123     };
124
125     this.setPrev = function(handler) {
126         var link = dojo.byId('acq-lit-prev');
127         if(handler) {
128             dojo.style(link, 'visibility', 'visible'); 
129             link.onclick = handler; 
130         } else {
131             dojo.style(link, 'visibility', 'hidden');
132         }
133     };
134
135     this.show = function(div) {
136         openils.Util.hide('acq-lit-table-div');
137         openils.Util.hide('acq-lit-info-div');
138         openils.Util.hide('acq-lit-li-details');
139         openils.Util.hide('acq-lit-notes-div');
140         openils.Util.hide('acq-lit-real-copies-div');
141         switch(div) {
142             case 'list':
143                 openils.Util.show('acq-lit-table-div');
144                 break;
145             case 'info':
146                 openils.Util.show('acq-lit-info-div');
147                 break;
148             case 'copies':
149                 openils.Util.show('acq-lit-li-details');
150                 break;
151             case 'real-copies':
152                 openils.Util.show('acq-lit-real-copies-div');
153                 break;
154             case 'notes':
155                 openils.Util.show('acq-lit-notes-div');
156                 break;
157             default:
158                 if(div) 
159                     openils.Util.show(div);
160         }
161     }
162
163     this.hide = function() {
164         this.show(null);
165     }
166
167     this.toggleSelect = function() {
168         if(self.toggleState) 
169             dojo.forEach(self.selectors, function(i){i.checked = false});
170         else 
171             dojo.forEach(self.selectors, function(i){i.checked = true});
172         self.toggleState = !self.toggleState;
173     };
174
175
176     /** @param all If true, assume all are selected */
177     this.getSelected = function(all) {
178         var selected = [];
179         dojo.forEach(self.selectors, 
180             function(i) { 
181                 if(i.checked || all)
182                     selected.push(self.liCache[i.parentNode.parentNode.getAttribute('li')]);
183             }
184         );
185         return selected;
186     };
187
188     this.setRowAttr = function(td, liWrapper, field, type) {
189         var val = liWrapper.findAttr(field, type || 'lineitem_marc_attr_definition') || '';
190         td.appendChild(document.createTextNode(val));
191     };
192
193     /**
194      * Inserts a single lineitem into the growing table of lineitems
195      * @param {Object} li The lineitem object to insert
196      */
197     this.addLineitem = function(li, skip_final_placement) {
198         this.liCache[li.id()] = li;
199
200         // sort the lineitem notes on edit_time
201         if(!li.lineitem_notes()) li.lineitem_notes([]);
202
203         var liWrapper = new openils.acq.Lineitem({lineitem:li});
204         var row = self.rowTemplate.cloneNode(true);
205         row.setAttribute('li', li.id());
206         var tds = dojo.query('[attr]', row);
207         dojo.forEach(tds, function(td) {self.setRowAttr(td, liWrapper, td.getAttribute('attr'), td.getAttribute('attr_type'));});
208         dojo.query('[name=source_label]', row)[0].appendChild(document.createTextNode(li.source_label()));
209
210         var isbn = liWrapper.findAttr('isbn', 'lineitem_marc_attr_definition');
211         if(isbn) {
212             // XXX media prefix for added content
213             dojo.query('[name=jacket]', row)[0].setAttribute('src', '/opac/extras/ac/jacket/small/' + isbn);
214         }
215
216         nodeByName("worksheet_link", row).href =
217             "/eg/acq/lineitem/worksheet/" + li.id();
218
219         dojo.query('[attr=title]', row)[0].onclick = function() {self.drawInfo(li.id())};
220         dojo.query('[name=copieslink]', row)[0].onclick = function() {self.drawCopies(li.id())};
221         dojo.query('[name=noteslink]', row)[0].onclick = function() {self.drawLiNotes(li)};
222
223         this.updateLiNotesCount(li, row);
224
225         // show which PO this lineitem is a member of
226         if(li.purchase_order() && !this.isPO) {
227             var po = 
228                 this.poCache[li.purchase_order()] =
229                 this.poCache[li.purchase_order()] ||
230                 fieldmapper.standardRequest(
231                     ['open-ils.acq', 'open-ils.acq.purchase_order.retrieve'],
232                     {params: [
233                         this.authtoken, li.purchase_order(), {
234                             "flesh_price_summary": true,
235                             "flesh_lineitem_count": true
236                         }
237                     ]});
238             if(po && !this.isMeta) {
239                 openils.Util.show(nodeByName('po', row), 'inline');
240                 var link = nodeByName('po_link', row);
241                 link.setAttribute('href', oilsBasePath + '/acq/po/view/' + li.purchase_order());
242                 link.innerHTML = 'PO: ' + po.name(); // TODO i18n
243             }
244         }
245
246         // show which picklist this lineitem is a member of
247         if(li.picklist() && (this.isPO || this.isMeta)) {
248             var pl = 
249                 this.plCache[li.picklist()] = 
250                 this.plCache[li.picklist()] || 
251                 fieldmapper.standardRequest(
252                     ['open-ils.acq', 'open-ils.acq.picklist.retrieve'],
253                     {params: [this.authtoken, li.picklist()]});
254             if(pl) {
255                 openils.Util.show(nodeByName('pl', row), 'inline');
256                 var link = nodeByName('pl_link', row);
257                 link.setAttribute('href', oilsBasePath + '/acq/picklist/view/' + li.picklist());
258                 link.innerHTML = 'PL: '+pl.name(); // TODO i18n
259             }
260         }
261
262         var countNode = nodeByName('count', row);
263         var count = li.item_count() || 0;
264         if (typeof(this._copy_count_cb) == "function") {
265             this._copy_count_cb(li.id(), count);
266         }
267         countNode.innerHTML = count;
268         countNode.id = 'acq-lit-copy-count-label-' + li.id();
269
270         // lineitem price
271         var priceInput = dojo.query('[name=price]', row)[0];
272         priceInput.value = li.estimated_unit_price() || '';
273         priceInput.onchange = function() { self.updateLiPrice(priceInput, li) };
274
275         // show either "mark received" or "unreceive" as appropriate
276         this.updateLiState(li, row);
277
278         if (!skip_final_placement) {
279             self.tbody.appendChild(row);
280             self.selectors.push(dojo.query('[name=selectbox]', row)[0]);
281         } else {
282             return row;
283         }
284     };
285
286     this.updateLiNotesCount = function(li, row) {
287         if (typeof(row) == "undefined")
288             row = dojo.query('tr[li="' + li.id() + '"]', "acq-lit-tbody")[0];
289
290         var has_notes = (li.lineitem_notes().filter(
291                 function(o) { return Boolean (o.alert_text()); }
292             ).length > 0);
293
294         /* U+2691 is the code point for a filled-in flag character */
295         nodeByName("notes_alert_flag", row).innerHTML =
296              has_notes ? "&#x2691;" : "";
297         nodeByName("noteslink", row).style.fontStyle =
298             has_notes ? "italic" : "normal";
299         nodeByName("notes_count", row).innerHTML = li.lineitem_notes().length;
300     };
301
302     /* XXX NOT related to _updateLiState(). rethink */
303     this.updateLiState = function(li, row) {
304         if (typeof(row) == "undefined")
305             row = dojo.query('tr[li="' + li.id() + '"]', "acq-lit-tbody")[0];
306
307         var actReceive = nodeByName("action_mark_recv", row);
308         var actUnRecv = nodeByName("action_mark_unrecv", row);
309         var actUpdateBarcodes = nodeByName("action_update_barcodes", row);
310         var actHoldingsMaint = nodeByName("action_holdings_maint", row);
311
312         var actNewInvoice = nodeByName('action_new_invoice', row);
313         var actLinkInvoice = nodeByName('action_link_invoice', row);
314         var actViewInvoice = nodeByName('action_view_invoice', row);
315
316         var state_cell = nodeByName("li_state", row);
317
318         if (li.state() == "cancelled") {
319             if (typeof li.cancel_reason() == "object") {
320                 var holds_state = dojo.create(
321                     "span", {
322                         "style": "border-bottom: 1px dashed #000;",
323                         "innerHTML": li.state()
324                     }, state_cell, "only"
325                 );
326                 new dijit.Tooltip(
327                     {
328                         "label": "<em>" + li.cancel_reason().label() +
329                             "</em><br />" + li.cancel_reason().description(),
330                         "connectId": [holds_state]
331                     }, dojo.create("span", null, state_cell, "last")
332                 );
333             } else {
334                 state_cell.innerHTML = li.state(); // TODO i18n state labels
335             }
336         } else {
337             state_cell.innerHTML = li.state(); // TODO i18n state labels
338         }
339
340
341         /* handle row coloring for based on LI state */
342         openils.Util.removeCSSClass(row, /^oils-acq-li-state-/);
343         openils.Util.addCSSClass(row, "oils-acq-li-state-" + li.state());
344
345         /* handle links that appear/disappear based on whether LI is received */
346         if (this.isPO) {
347             var self = this;
348             switch(li.state()) {
349                 case "on-order":
350                     actReceive.disabled = false;
351                     actReceive.onclick = function() {
352                         if (self.checkLiAlerts(li.id()))
353                             self.issueReceive(li);
354                         nodeByName("action_none", row).selected = true;
355                     };
356                     return;
357                 case "received":
358                     actUnRecv.disabled = false;
359                     actUnRecv.onclick = function() {
360                         if (confirm(localeStrings.UNRECEIVE_LI))
361                             self.issueReceive(li, /* rollback */ true);
362                         nodeByName("action_none", row).selected = true;
363                     };
364                     // TODO we should allow editing before receipt, in which case the
365                     // test should be "if 1 or more real (acp) copies exist
366                     actUpdateBarcodes.disabled = false;
367                     actUpdateBarcodes.onclick = function() {
368                         self.showRealCopyEditUI(li);
369                         nodeByName("action_none", row).selected = true;
370                     }
371                     actHoldingsMaint.disabled = false;
372                     actHoldingsMaint.onclick = self.generateMakeRecTab( li.eg_bib_id(), 'copy_browser', row );
373
374                     actNewInvoice.disabled = false;
375                     actLinkInvoice.disabled = false;
376                     actViewInvoice.disabled = false;
377
378                     actNewInvoice.onclick = function() {
379                         location.href = oilsBasePath + '/acq/invoice/view?create=1&attach_li=' + li.id();
380                         nodeByName("action_none", row).selected = true;
381                     };
382                     actLinkInvoice.onclick = function() {
383                         // TODO: show inv #/vendor entry then relocate to invoice attach page
384                         nodeByName("action_none", row).selected = true;
385                     };
386                     actViewInvoice.onclick = function() {
387                         // TODO: go to invoice search page, with lineitem filter = li.id()
388                         nodeByName("action_none", row).selected = true;
389                     };
390
391                     return;
392             }
393         }
394     };
395
396
397     this._setAlertStore = function() {
398         acqLitAlertAlertText.store = new dojo.data.ItemFileReadStore(
399             {
400                 "data": acqliat.toStoreData(
401                     this.pcrud.search(
402                         "acqliat", {"id": {"!=": null}}
403                     )
404                 )
405             }
406         );
407         acqLitAlertAlertText.setValue(); /* make the store "live" */
408         acqLitAlertAlertText._store_ready = true;
409     };
410
411     /**
412      * Draws and shows the lineitem notes pane
413      */
414     this.drawLiNotes = function(li) {
415         var self = this;
416
417         if (!acqLitAlertAlertText._store_ready)
418             this._setAlertStore();
419
420         li.lineitem_notes(
421             li.lineitem_notes().sort(
422                 function(a, b) { 
423                     if(a.edit_time() < b.edit_time()) return 1;
424                     return -1;
425                 }
426             )
427         );
428
429         while(this.liNotesTbody.childNodes[0])
430             this.liNotesTbody.removeChild(this.liNotesTbody.childNodes[0]);
431         this.show('notes');
432
433         acqLitCreateNoteSubmit.onClick = function() {
434             var value = acqLitCreateNoteText.attr('value');
435             if(!value) return;
436             var note = new fieldmapper.acqlin();
437             note.isnew(true);
438             note.vendor_public(
439                 Boolean(acqLitCreateNoteVendorPublic.attr('checked'))
440             );
441             note.value(value);
442             note.lineitem(li.id());
443
444             self.updateLiNotes(li, note);
445             acqLitCreateNoteVendorPublic.attr("checked", false);
446             acqLitCreateNoteText.attr("value", "");
447         }
448
449         acqLitCreateAlertSubmit.onClick = function() {
450             if (!acqLitAlertAlertText.item) {
451                 alert(localeStrings.ALERT_UNSELECTED);
452                 return;
453             }
454
455             var alert_text = new fieldmapper.acqliat().fromStoreItem(
456                 acqLitAlertAlertText.item
457             );
458             var value = acqLitAlertNoteValue.attr("value") || "";
459
460             var note = new fieldmapper.acqlin();
461             note.isnew(true);
462             note.lineitem(li.id());
463             note.value(value);
464             note.alert_text(alert_text);
465
466             self.updateLiNotes(li, note);
467         }
468
469         dojo.forEach(li.lineitem_notes(), function(note) { self.addLiNote(li, note) });
470     }
471
472     /**
473      * Draws a single lineitem note in the notes pane
474      */
475     this.addLiNote = function(li, note) {
476         if(note.isdeleted()) return;
477         var self = this;
478         var row = self.liNotesRow.cloneNode(true);
479         nodeByName("value", row).innerHTML = note.value();
480         var alert_node = nodeByName("alert_code", row);
481         if (note.alert_text()) {
482             alert_node.innerHTML = note.alert_text().code();
483             if (note.alert_text().description()) {
484                 new dijit.Tooltip(
485                     {
486                         "connectId": [alert_node],
487                         "label": note.alert_text().description()
488                     }, dojo.create("span", null, alert_node, "after")
489                 );
490             }
491         }
492
493         if (openils.Util.isTrue(note.vendor_public()))
494             nodeByName("vendor_public", row).innerHTML =
495                 localeStrings.VENDOR_PUBLIC;
496
497         nodeByName("delete", row).onclick = function() {
498             note.isdeleted(true);
499             self.liNotesTbody.removeChild(row);
500             self.updateLiNotes(li);
501         };
502
503         if(note.edit_time()) {
504             nodeByName("edit_time", row).innerHTML =
505                 dojo.date.locale.format(
506                     dojo.date.stamp.fromISOString(note.edit_time()), 
507                     {formatLength:'short'});
508         }
509
510         self.liNotesTbody.appendChild(row);
511     }
512
513     /**
514      * Updates any new/changed/deleted notes on the server
515      */
516     this.updateLiNotes = function(li, newNote) {
517
518         var notes;
519         if(newNote) {
520             notes = [newNote];
521         } else {
522             notes = li.lineitem_notes().filter(
523                 function(note) {
524                     if(note.ischanged() || note.isnew() || note.isdeleted())
525                         return note;
526                 }
527             );
528         }
529
530         if(notes.length == 0) return;
531         progressDialog.show();
532
533         fieldmapper.standardRequest(
534             ['open-ils.acq', 'open-ils.acq.lineitem_note.cud.batch'],
535             {   async : true,
536                 params : [this.authtoken, notes],
537                 onresponse : function(r) {
538                     var resp = openils.Util.readResponse(r);
539
540                     if(resp.complete) {
541
542                         if(!newNote) {
543                             // remove the old changed notes
544                             var list = [];
545                             dojo.forEach(li.lineitem_notes(), 
546                                 function(note) {
547                                     if(!(note.ischanged() || note.isnew() || note.isdeleted()))
548                                         list.push(note);
549                                 }
550                             );
551                             li.lineitem_notes(list);
552                         }
553
554                         progressDialog.hide();
555                         self.updateLiNotesCount(li);
556                         self.drawLiNotes(li);
557                         return;
558                     }
559
560                     progressDialog.update(resp);
561                     var newnote = resp.note;
562
563                     if(!newnote.isdeleted()) {
564                         newnote.isnew(false);
565                         newnote.ischanged(false);
566                         li.lineitem_notes().push(newnote);
567                     }
568                 },
569             }
570         );
571     }
572
573     this.updateLiPrice = function(input, li) {
574
575         var price = input.value;
576         if(Number(price) == Number(li.estimated_unit_price())) return;
577
578         fieldmapper.standardRequest(
579             ['open-ils.acq', 'open-ils.acq.lineitem.price.set'],
580             {   async : true,
581                 params : [this.authtoken, li.id(), price],
582                 oncomplete : function(r) {
583                     openils.Util.readResponse(r);
584                     li.estimated_unit_price(price); // update local copy
585                 }
586             }
587         );
588     }
589
590     this.removeLineitem = function(liId) {
591         this.tbody.removeChild(dojo.query('[li='+liId+']', this.tbody)[0]);
592         delete this.liCache[liId];
593         //selected.push(self.liCache[i.parentNode.parentNode.getAttribute('li')]);
594     }
595
596     this.drawInfo = function(liId) {
597         if (!this._isRelatedViewer) {
598             var d = dojo.byId("acq-lit-info-related");
599             if (!this.relCache[liId]) {
600                 fieldmapper.standardRequest(
601                     [
602                         "open-ils.acq",
603                         "open-ils.acq.lineitems_for_bib.by_lineitem_id.count"
604                     ], {
605                         "async": true,
606                         "params": [openils.User.authtoken, liId],
607                         "onresponse": function(r) {
608                             self.relCache[liId] = openils.Util.readResponse(r);
609                             nodeByName("related_number", d).innerHTML =
610                                 self.relCache[liId];
611                             openils.Util[
612                                 self.relCache[liId] >1 ? "show" : "hide"
613                             ](d);
614                         }
615                     }
616                 );
617             } else {
618                 nodeByName("related_number", d).innerHTML = this.relCache[liId];
619                 openils.Util[this.relCache[liId] > 1 ? "show" : "hide"](d);
620             }
621         }
622
623         this.show('info');
624         openils.acq.Lineitem.fetchAttrDefs(
625             function() { 
626                 self._fetchLineitem(liId, function(li){self._drawInfo(li);}); 
627             } 
628         );
629     };
630
631     this._fetchLineitem = function(liId, handler) {
632
633         var li = this.liCache[liId];
634         if(li && li.marc() && li.lineitem_details())
635             return handler(li);
636         
637         fieldmapper.standardRequest(
638             ['open-ils.acq', 'open-ils.acq.lineitem.retrieve'],
639             {   async: true,
640
641                 params: [self.authtoken, liId, {
642                     flesh_attrs: true,
643                     flesh_cancel_reason: true,
644                     flesh_li_details: true,
645                     flesh_fund_debit: true }],
646
647                 oncomplete: function(r) {
648                     var li = openils.Util.readResponse(r);
649                     handler(li)
650                 }
651             }
652         );
653     };
654
655     this._drawInfo = function(li) {
656
657         acqLitEditOrderMarc.onClick = function() { self.editOrderMarc(li); }
658
659         if(li.eg_bib_id()) {
660             openils.Util.hide('acq-lit-marc-order-record-label');
661             openils.Util.hide(acqLitEditOrderMarc.domNode);
662             openils.Util.show('acq-lit-marc-real-record-label');
663         } else {
664             openils.Util.show('acq-lit-marc-order-record-label');
665             openils.Util.show(acqLitEditOrderMarc.domNode);
666             openils.Util.hide('acq-lit-marc-real-record-label');
667         }
668
669         this.drawMarcHTML(li);
670         this.infoTbody = dojo.byId('acq-lit-info-tbody');
671
672         if(!this.infoRow)
673             this.infoRow = this.infoTbody.removeChild(dojo.byId('acq-lit-info-row'));
674         while(this.infoTbody.childNodes[0])
675             this.infoTbody.removeChild(this.infoTbody.childNodes[0]);
676
677         for(var i = 0; i < li.attributes().length; i++) {
678             var attr = li.attributes()[i];
679             var row = this.infoRow.cloneNode(true);
680
681             var type = attr.attr_type().replace(/lineitem_(.*)_attr_definition/, '$1');
682             var name = openils.acq.Lineitem.attrDefs[type].filter(
683                 function(a) {
684                     return (a.code() == attr.attr_name());
685                 }
686             ).pop().description();
687
688             dojo.query('[name=label]', row)[0].appendChild(document.createTextNode(name));
689             dojo.query('[name=value]', row)[0].appendChild(document.createTextNode(attr.attr_value()));
690             this.infoTbody.appendChild(row);
691         }
692
693         if (!this._isRelatedViewer) {
694             nodeByName("rel_link", dojo.byId("acq-lit-info-related")).href =
695                 "/eg/acq/lineitem/related/" + li.id();
696         }
697
698         if(li.eg_bib_id()) {
699
700             openils.Util.show('acq-lit-info-cat-link');
701             var link = dojo.byId('acq-lit-info-cat-link').getElementsByTagName('a')[0];
702
703             if(openils.XUL.isXUL()) {
704
705                 link.setAttribute('href', 'javascript:void(0);');
706                 link.onclick = this.generateMakeRecTab( li.eg_bib_id() );
707
708             } else {
709                 var href = link.getAttribute('href');
710                 if(href.match(/=$/))
711                     link.setAttribute('href',  href + li.eg_bib_id());
712             }
713         } else {
714             openils.Util.hide('acq-lit-info-cat-link');
715         }
716     };
717
718     this.generateMakeRecTab = function(bib_id,default_view, row) {
719         return function() {
720             xulG.new_tab(
721                 XUL_OPAC_WRAPPER,
722                 {tab_name: localeStrings.XUL_RECORD_DETAIL_PAGE, browser:false},
723                 {
724                     no_xulG : false, 
725                     show_nav_buttons : true, 
726                     show_print_button : true, 
727                     opac_url : xulG.url_prefix(xulG.urls.opac_rdetail + '?r=' + bib_id),
728                     default_view : default_view
729                 }
730             );
731
732             if(row) nodeByName("action_none", row).selected = true;
733         }
734     };
735
736     this.drawMarcHTML = function(li) {
737         var params = [null, true, li.marc()];
738         if(li.eg_bib_id()) 
739             params = [li.eg_bib_id(), true];
740
741         fieldmapper.standardRequest(
742             ['open-ils.search', 'open-ils.search.biblio.record.html'],
743             {   async: true,
744                 params: params,
745                 oncomplete: function(r) {
746                     dojo.byId('acq-lit-marc-div').innerHTML = 
747                         openils.Util.readResponse(r);
748                 }
749             }
750         );
751     }
752
753     this.drawCopies = function(liId) {
754         this.show('copies');
755         var self = this;
756         this.copyCache = {};
757         this.copyWidgetCache = {};
758         this.oldCopyWidgetCache = {};
759         this.virtDfaCounts = {};
760         this.realDfaCache = {};
761         this.dfeOffset = 0;
762
763         acqLitSaveCopies.onClick = function() { self.saveCopyChanges(liId) };
764         acqLitBatchUpdateCopies.onClick = function() { self.batchCopyUpdate() };
765         acqLitCopyCountInput.attr('value', '0');
766
767         while(this.copyTbody.childNodes[0])
768             this.copyTbody.removeChild(this.copyTbody.childNodes[0]);
769
770         this._drawBatchCopyWidgets();
771
772         this._drawDistribApplied(liId);
773
774         this._fetchDistribFormulas(
775             function() {
776                 openils.acq.Lineitem.fetchAttrDefs(
777                     function() { 
778                         self._fetchLineitem(liId, function(li){self._drawCopies(li);}); 
779                     } 
780                 );
781             }
782         );
783     };
784
785     this._saveDistribAppliedTemplates = function() {
786         if (!this._appliedDistribTemplate) {
787             this._appliedDistribTemplate =
788                 dojo.byId("acq-lit-distrib-applied-tbody").
789                     removeChild(dojo.byId("acq-lit-distrib-applied-row"));
790             dojo.attr(this._appliedDistribTemplate, "id");
791         }
792     };
793
794     this._drawDistribApplied = function(liId) {
795         /* Build this table while hidden to prevent rendering artifacts */
796         openils.Util.hide("acq-lit-distrib-applied-tbody");
797
798         this._saveDistribAppliedTemplates();
799
800         /* Remove any rows in the table from previous populations */
801         dojo.query("tr[formula]", "acq-lit-distrib-applied-tbody").
802             forEach(dojo.destroy);
803
804         /* Unregister all dijits previously created (for some reason this isn't
805          * covered by the above destroy calls). */
806         dijit.registry.forEach(
807             function(w) { if (/^dfa-/.test(w.id)) w.destroyRecursive(); }
808         );
809
810         /* Populate the table with our liId */
811         var total = 0;
812         fieldmapper.standardRequest(
813             ["open-ils.acq",
814             "open-ils.acq.distribution_formula_application.ranged.retrieve"],
815             {
816                 "async": true,
817                 "params": [self.authtoken, liId],
818                 "onresponse": function(r) {
819                     var dfa = openils.Util.readResponse(r);
820                     if (dfa) {
821                         total++;
822                         self.realDfaCache[dfa.id()] = dfa;
823                         self._drawDistribAppliedUnit(dfa);
824                     }
825                 },
826                 "oncomplete": function() {
827                     /* Reveal built table */
828                     if (total) {
829                         openils.Util.show(
830                             "acq-lit-distrib-applied-tbody", "table-row-group"
831                         );
832                     }
833                 }
834             }
835         );
836     };
837
838     this._drawDistribAppliedUnit = function(dfa) {
839         var new_row = false;
840         var row = dojo.query(
841             'tr[formula="' + dfa.formula().id() + '"]',
842             "acq-lit-distrib-applied-tbody"
843         )[0];
844
845         if (!row) {
846             new_row = true;
847             row = dojo.clone(this._appliedDistribTemplate);
848             dojo.attr(row, "formula", dfa.formula().id());
849             dojo.query("th", row)[0].innerHTML = dfa.formula().name();
850         }
851
852         var td = dojo.query("td", row)[0];
853
854         dojo.create("span", {"id": "dfa-button-" + dfa.id()}, td, "last");
855         dojo.create("span", {"id": "dfa-tip-" + dfa.id()}, td, "last");
856
857         if (new_row)
858             dojo.place(row, "acq-lit-distrib-applied-tbody", "last");
859
860         new dijit.form.Button(
861             {
862                 "onClick": function() {
863                     if (confirm(localeStrings.EXPLAIN_DFA_MGMT))
864                         self.deleteDfa(dfa);
865                 },
866                 "label": "X",
867                 /* XXX I /cannot/ make the following work in as a CSS class
868                  * for some reason. So frustrating... */
869                 "style": function(id) {
870                      return (id > 0 ?
871                         "font-weight: bold; color: #c00;" :
872                         "color: #666;");
873                      }(dfa.id()) + "margin: 0 6px;display: inline;"
874             }, "dfa-button-" + dfa.id()
875         );
876         new dijit.Tooltip(
877             {
878                 "connectId": ["dfa-button-" + dfa.id()],
879                 "label": dojo.string.substitute(
880                     localeStrings.DFA_TIP, dfa.id() > 0 ? [
881                         openils.User.formalName(dfa.creator()),
882                         dojo.date.locale.format(
883                             dojo.date.stamp.fromISOString(dfa.create_time()),
884                             {"formatLength":"short"}
885                         )
886                     ] : [localeStrings.ITS_YOU, localeStrings.JUST_NOW]
887                 )
888             }, "dfa-tip-" + dfa.id()
889         );
890     }
891
892     this.deleteDfa = function(dfa) {
893         if (dfa.id() > 0) { /* real */
894             this.pcrud.eliminate(
895                 dfa, {
896                     "async": true,
897                     "oncomplete": function() {
898                         self._removeDistribApplied(dfa.id());
899                         delete self.realDfaCache[dfa.id()];
900                     }
901                 }
902             );
903         } else { /* virtual */
904             if (--(this.virtDfaCounts[dfa.formula().id()]) < 0)
905             this.virtDfaCounts[dfa.formula().id()] = 0;
906             /* hasn't been saved yet, so no need to do anything server side */
907             this._removeDistribApplied(dfa.id());
908         }
909
910     };
911
912     this._removeDistribApplied = function(dfaId) {
913         var re = new RegExp("^dfa-\\w+-" + String(dfaId));
914         dijit.registry.forEach(
915             function(w) { if (re.test(w.id)) w.destroyRecursive(); }
916         );
917         this._removeDistribAppliedEmptyRows();
918     };
919
920     this._removeAllDistribAppliedVirtual = function() {
921         /* Unregister dijits */
922         dijit.registry.forEach(
923             function(w) { if (/^dfa-\w+--/.test(w.id)) w.destroyRecursive(); }
924         );
925         this._removeDistribAppliedEmptyRows();
926     };
927
928     this._removeDistribAppliedEmptyRows = function() {
929         /* Remove any rows with no DFA at all */
930         dojo.query("tr[formula] td", "acq-lit-distrib-applied-tbody").forEach(
931             function(o) {
932                 if (o.childNodes.length < 1) dojo.destroy(o.parentNode);
933             }
934         );
935     };
936
937     /**
938      * Insert a new row into the distribution formula selection form
939      */
940     this._addDistribFormulaRow = function() {
941         var self = this;
942
943         if (!self.distribForms) {
944             // no formulas, hide the form
945             openils.Util.hide('acq-lit-distrib-formula-tbody');
946             return;
947         }
948
949         if(!this.distribFormulaTemplate) 
950             this.distribFormulaTemplate = 
951                 dojo.byId('acq-lit-distrib-formula-tbody').removeChild(dojo.byId('acq-lit-distrib-form-row'));
952
953         var row = this.distribFormulaTemplate.cloneNode(true);
954         dojo.place(row, "acq-lit-distrib-formula-tbody", "only");
955
956         this.dfSelector = new dijit.form.FilteringSelect(
957             {"labelAttr": "dynLabel", "labelType": "html"},
958             nodeByName("selector", row)
959         );
960         this._updateFormulaStore();
961         this.dfSelector.fetchProperties =
962             {"sort": [{"attribute": "use_count", "descending": true}]};
963
964         var apply = new dijit.form.Button(
965             {"label": localeStrings.APPLY},
966             nodeByName('set_button', row)
967         ); 
968
969         var reset = new dijit.form.Button(
970             {"label": localeStrings.RESET_FORMULAE, "disabled": true},
971             nodeByName("reset_button", row)  
972         );
973
974         dojo.connect(apply, 'onClick', 
975             function() {
976                 var form_id = self.dfSelector.attr("value");
977                 if(!form_id) return;
978                 self._applyDistribFormula(form_id);
979                 reset.attr("disabled", false);
980             }
981         );
982
983         dojo.connect(reset, 'onClick', 
984             function() {
985                 self.restoreCopyFieldsBeforeDF();
986                 self.virtDfaCounts = {};
987                 self.virtDfaId = -1;
988                 self.dfeOffset = 0;
989                 self._updateFormulaStore();
990                 self._removeAllDistribAppliedVirtual();
991                 reset.attr("disabled", "true");
992             }
993         );
994
995     };
996
997     /**
998      * Applies a distrib formula to the current set of copies
999      */
1000     this._applyDistribFormula = function(formula) {
1001         if(!formula) return;
1002
1003         formula = this.distribForms.filter(
1004             function(form) { return form.id() == formula; }
1005         )[0];
1006
1007         var copyRows = dojo.query('tr', self.copyTbody);
1008
1009         if (this.dfeOffset >= copyRows.length) {
1010             alert(localeStrings.OUT_OF_COPIES);
1011             return;
1012         }
1013
1014         var entries_applied = 0;
1015         for(
1016             var rowIndex = this.dfeOffset;
1017             rowIndex < copyRows.length;
1018             rowIndex++
1019         ) {
1020             
1021             var row = copyRows[rowIndex];
1022             var copy_id = row.getAttribute('copy_id');
1023             var copyWidgets = this.copyWidgetCache[copy_id];
1024             var entryIndex = this.dfeOffset;
1025             var entry = null;
1026
1027             // find the correct entry for the current row
1028             dojo.forEach(formula.entries(), 
1029                 function(e) {
1030                     if(!entry) {
1031                         entryIndex += e.item_count();
1032                         if(entryIndex > rowIndex)
1033                             entry = e;
1034                     }
1035                 }
1036             );
1037
1038             if(entry) {
1039                 
1040                 //console.log("rowIndex = " + rowIndex + ", entry = " + entry.id() + ", entryIndex=" + 
1041                 //  entryIndex + ", owning_lib = " + entry.owning_lib() + ", location = " + entry.location());
1042     
1043                 entries_applied++;
1044                 this.saveCopyFieldsBeforeDF(copy_id);
1045                 this._copy_fields_for_acqdf.forEach(
1046                     function(field) {
1047                         if(entry[field]()) {
1048                             copyWidgets[field].attr('value', (entry[field]()));
1049                         }
1050                     }
1051                 );
1052             }
1053         }
1054
1055         if (entries_applied) {
1056             this.virtDfaCounts[formula.id()] =
1057                 ++(this.virtDfaCounts[formula.id()]) || 1;
1058             this._updateFormulaStore();
1059             this._drawDistribAppliedUnit(
1060                 function(df) {
1061                     var dfa = new acqdfa();
1062                     dfa.formula(df); dfa.id(self.virtDfaId--); return dfa;
1063                 }(formula)
1064             );
1065             this.dfeOffset += entries_applied;
1066         };
1067     };
1068
1069     /**
1070      * This function updates the DF store for the dropdown so that use_counts
1071      * can reflect DF applications from this session before they're saved
1072      * server-side.
1073      */
1074     this._updateFormulaStore = function() {
1075         this.dfSelector.store = new dojo.data.ItemFileReadStore(
1076             {
1077                 "data": self._labelFormulasWithCounts(
1078                     acqdf.toStoreData(self.distribForms)
1079                 )
1080             }
1081         );
1082     };
1083
1084     this.saveCopyFieldsBeforeDF = function(copy_id) {
1085         var self = this;
1086         if (!this.oldCopyWidgetCache[copy_id]) {
1087             var copyWidgets = this.copyWidgetCache[copy_id];
1088
1089             this.oldCopyWidgetCache[copy_id] = {};
1090             this._copy_fields_for_acqdf.forEach(
1091                 function(f) {
1092                     self.oldCopyWidgetCache[copy_id][f] =
1093                         copyWidgets[f].attr("value");
1094                 }
1095             );
1096         }
1097     };
1098
1099     this.restoreCopyFieldsBeforeDF = function() {
1100         var self = this;
1101         for (var copy_id in this.oldCopyWidgetCache) {
1102             this._copy_fields_for_acqdf.forEach(
1103                 function(f) {
1104                     self.copyWidgetCache[copy_id][f].attr(
1105                         "value", self.oldCopyWidgetCache[copy_id][f]
1106                     );
1107                 }
1108             );
1109         }
1110     };
1111
1112     this._labelFormulasWithCounts = function(store_data) {
1113         for (var key in store_data.items) {
1114             var obj = store_data.items[key];
1115             obj.use_count = Number(obj.use_count); /* needed for sorting */
1116
1117             if (this.virtDfaCounts[obj.id])
1118                 obj.use_count = obj.use_count + Number(this.virtDfaCounts[obj.id]);
1119
1120             obj.dynLabel = "<span class='acq-lit-distrib-form-use-count'>[" +
1121                 obj.use_count + "]</span>&nbsp; " + obj.name;
1122         }
1123         return store_data;
1124     };
1125
1126     /**
1127      * This method formerly would not refetch the DF formulas if they'd been
1128      * loaded already, but now it always re-fetches, since use_count changes.
1129      */
1130     this._fetchDistribFormulas = function(onload) {
1131         fieldmapper.standardRequest(
1132             ["open-ils.acq",
1133                 "open-ils.acq.distribution_formula.ranged.retrieve.atomic"],
1134             {
1135                 "async": true,
1136                 "params": [openils.User.authtoken],
1137                 "oncomplete": function(r) {
1138                     self.distribForms = openils.Util.readResponse(r);
1139                     if(!self.distribForms || self.distribForms.length == 0) {
1140                         self.distribForms = [];
1141                     }
1142                     self._addDistribFormulaRow();
1143                     onload();
1144                 }
1145             }
1146         );
1147     }
1148
1149     this._drawBatchCopyWidgets = function() {
1150         var row = this.copyBatchRow;
1151         dojo.forEach(liDetailBatchFields, 
1152             function(field) {
1153                 if(self.copyBatchRowDrawn) {
1154                     self.copyBatchWidgets[field].attr('value', null);
1155                 } else {
1156                     var widget = new openils.widget.AutoFieldWidget({
1157                         fmField : field,
1158                         fmClass : 'acqlid',
1159                         labelFormat : (field == 'fund') ? fundLabelFormat : null,
1160                         searchFormat : (field == 'fund') ? fundSearchFormat : null,
1161                         searchFilter : (field == 'fund') ? {"active": "t"} : null,
1162                         parentNode : dojo.query('[name='+field+']', row)[0],
1163                         orgLimitPerms : ['CREATE_PICKLIST'],
1164                         dijitArgs : {
1165                             "required": false,
1166                             "labelType": (field == "fund") ? "html" : null
1167                         },
1168                         noCache: (field == "fund"),
1169                         forceSync : true
1170                     });
1171                     widget.build(
1172                         function(w, ww) {
1173                             if (field == "fund" && w.store)
1174                                 self._ensureCSSFundClasses(w.store);
1175                             self.copyBatchWidgets[field] = w;
1176                         }
1177                     );
1178                     if (field == "fund") {
1179                         dojo.connect(
1180                             widget.widget, "onChange", function(val) {
1181                                 self._updateFundSelectorStyle(widget, val);
1182                             }
1183                         );
1184                     }
1185                 }
1186             }
1187         );
1188         this.copyBatchRowDrawn = true;
1189     };
1190
1191     this.batchCopyUpdate = function() {
1192         var self = this;
1193         for(var k in this.copyWidgetCache) {
1194             var cache = this.copyWidgetCache[k];
1195             dojo.forEach(liDetailBatchFields, function(f) {
1196                 var newval = self.copyBatchWidgets[f].attr('value');
1197                 if(newval) cache[f].attr('value', newval);
1198             });
1199         }
1200     };
1201
1202     this._drawCopies = function(li) {
1203         var self = this;
1204
1205         // this button sets the total number of copies for a given lineitem
1206         acqLitAddCopyCount.onClick = function() { 
1207             var count = acqLitCopyCountInput.attr('value');
1208
1209             // add new rows
1210             while(self.copyCount() < count)
1211                 self.addCopy(li); 
1212             
1213             // delete rows if necessary
1214             var diff = self.copyCount() - count;
1215             if(diff > 0) {
1216                 var rows = dojo.query('tr', self.copyTbody).reverse().slice(0, diff);
1217                 if(confirm(dojo.string.substitute(localeStrings.DELETE_LI_COPIES_CONFIRM, [diff]))) {
1218                     dojo.forEach(rows, function(row) {self.deleteCopy(row); });
1219                 } else {
1220                     acqLitCopyCountInput.attr('value', self.copyCount()+'');
1221                 }
1222             }
1223         }
1224
1225
1226         if(li.lineitem_details().length > 0) {
1227             dojo.forEach(li.lineitem_details(),
1228                 function(copy) {
1229                     self.addCopy(li, copy);
1230                 }
1231             );
1232         } else {
1233             self.addCopy(li);
1234         }
1235     };
1236
1237     this.copyCount = function() {
1238         var count = 0;
1239         for(var id in this.copyCache) {
1240             if(!this.copyCache[id].isdeleted())
1241                 count++;
1242         }
1243         return count;
1244     }
1245
1246     this.virtCopyId = -1;
1247     this.addCopy = function(li, copy) {
1248         var row = this.copyRow.cloneNode(true);
1249         this.copyTbody.appendChild(row);
1250         var self = this;
1251
1252         if(!copy) {
1253             copy = new fieldmapper.acqlid();
1254             copy.isnew(true);
1255             copy.id(this.virtCopyId--);
1256             copy.lineitem(li.id());
1257         }
1258
1259         this.copyCache[copy.id()] = copy;
1260         row.setAttribute('copy_id', copy.id());
1261         self.copyWidgetCache[copy.id()] = {};
1262
1263         acqLitCopyCountInput.attr('value', self.copyCount()+'');
1264
1265         dojo.forEach(liDetailFields,
1266             function(field) {
1267                 var searchFilter;
1268                 if (field == "fund") {
1269                     searchFilter = (copy.fund() ?
1270                         {"-or": {"active": "t", "id": copy.fund()}} :
1271                         {"active" : "t"});
1272                 } else {
1273                     searchFilter = null;
1274                 }
1275                 var widget = new openils.widget.AutoFieldWidget({
1276                     fmObject : copy,
1277                     fmField : field,
1278                     labelFormat : (field == 'fund') ? fundLabelFormat : null,
1279                     searchFormat : (field == 'fund') ? fundSearchFormat : null,
1280                     dijitArgs: {"labelType": (field == 'fund') ? "html" : null},
1281                     searchFilter : searchFilter,
1282                     noCache: (field == "fund"),
1283                     fmClass : 'acqlid',
1284                     parentNode : dojo.query('[name='+field+']', row)[0],
1285                     orgLimitPerms : ['CREATE_PICKLIST', 'CREATE_PURCHASE_ORDER'],
1286                     readOnly : Boolean(copy.eg_copy_id())
1287                 });
1288                 widget.build(
1289                     // make sure we capture the value from any async widgets
1290                     function(w, ww) { 
1291                         if (field == "fund" && w.store)
1292                             self._ensureCSSFundClasses(w.store);
1293                         copy[field](ww.getFormattedValue()) 
1294                         self.copyWidgetCache[copy.id()][field] = w;
1295                     }
1296                 );
1297                 dojo.connect(widget.widget, 'onChange', 
1298                     function(val) { 
1299                         if (field == "fund")
1300                             self._updateFundSelectorStyle(widget, val);
1301
1302                         if (copy.isnew() || val != copy[field]()) {
1303                             // prevent setting ischanged() automatically on widget load for existing copies
1304                             copy[field](widget.getFormattedValue()) 
1305                             copy.ischanged(true);
1306                         }
1307                     }
1308                 );
1309             }
1310         );
1311
1312         this.updateLidState(copy, row);
1313     };
1314
1315     this._ensureCSSFundClass = function(id) {
1316         if (!this.fundStyleSheet) {
1317             dojo.create(
1318                 "style", {"type": "text/css"},
1319                 document.getElementsByTagName("head")[0], "last"
1320             );
1321             this.fundStyleSheet = document.styleSheets[
1322                 document.styleSheets.length - 1
1323             ];
1324         }
1325
1326         var cn = "fund_" + id;
1327         if (!this.haveFundClass[cn]) {
1328             fieldmapper.standardRequest(
1329                 ["open-ils.acq", "open-ils.acq.fund.check_balance_percentages"],
1330                 {
1331                     "params": [openils.User.authtoken, id],
1332                     "async": true,
1333                     "oncomplete": function(r) {
1334                         r = openils.Util.readResponse(r);
1335                         self.fundBalanceState[id] = r;
1336                         var style = "";
1337                         if (r[0] /* stop */)
1338                             style = fundStyles.stop;
1339                         else if (r[1] /* warning */)
1340                             style = fundStyles.warning;
1341                         self.fundStyleSheet.insertRule(
1342                             "." + cn + " { " + style + " }",
1343                             self.fundStyleSheet.cssRules.length
1344                         );
1345                         self.haveFundClass[cn] = true;
1346                     }
1347                 }
1348             );
1349         }
1350     };
1351
1352     this._ensureCSSFundClasses = function(store) {
1353         store.fetch({
1354             "query": {"id": "*"},
1355             "onItem": function(o) { self._ensureCSSFundClass(o.id[0]); }
1356         });
1357     };
1358
1359     this._updateFundSelectorStyle = function(widget, fund_id) {
1360         openils.Util.removeCSSClass(widget.widget.domNode, /fund_\d+/);
1361         openils.Util.addCSSClass(widget.widget.domNode, "fund_" + fund_id);
1362     };
1363
1364     this.updateLidState = function(copy, row) {
1365         if (typeof(row) == "undefined") {
1366             row = dojo.query(
1367                 'tr[copy_id="' + copy.id() + '"]', this.copyTbody
1368             )[0];
1369         }
1370
1371         var self = this;
1372         var recv_link = nodeByName("receive", row);
1373         var unrecv_link = nodeByName("unreceive", row);
1374         var del_link = nodeByName("delete", row);
1375         var cxl_link = nodeByName("cancel", row);
1376         var cxl_reason_link = nodeByName("cancel_reason", row);
1377
1378         if (copy.cancel_reason()) {
1379             openils.Util.hide(del_link.parentNode);
1380             openils.Util.hide(recv_link);
1381             openils.Util.hide(unrecv_link);
1382             openils.Util.hide(cxl_link);
1383
1384             /* XXX the following may leak memory in a long lived table: dijits may not get destroyed... not positive. revisit. */
1385             var holds_reason = dojo.create(
1386                 "span", {
1387                     "style": "border-bottom: 1px dashed #000;",
1388                     "innerHTML": "Cancelled" /* XXX [sic] and i18n */
1389                 }, cxl_reason_link, "only"
1390             );
1391             new dijit.Tooltip(
1392                 {
1393                     "label": "<em>" + copy.cancel_reason().label() +
1394                         "</em><br />" + copy.cancel_reason().description(),
1395                     "connectId": [holds_reason]
1396                 }, dojo.create("span", null, cxl_reason_link, "last")
1397             );
1398             openils.Util.show(cxl_reason_link, "inline");
1399         } else if (this.isPO) {
1400             openils.Util.hide(del_link.parentNode);
1401             openils.Util.hide(cxl_reason_link);
1402
1403             /* Avoid showing (un)receive links, cancel links, for virt copies */
1404             if (copy.id() > 0) {
1405                 if(copy.recv_time()) {
1406                     openils.Util.hide(cxl_link);
1407                     openils.Util.hide(recv_link);
1408                     openils.Util.show(unrecv_link, "inline");
1409                     unrecv_link.onclick = function() {
1410                         if (confirm(localeStrings.UNRECEIVE_LID))
1411                             self.issueReceive(copy, /* rollback */ true);
1412                     };
1413                 } else {
1414                     openils.Util.hide(unrecv_link);
1415                     openils.Util.show(recv_link, "inline");
1416                     openils.Util.show(cxl_link, "inline");
1417                     recv_link.onclick = function() {
1418                         if (self.checkLiAlerts(copy.lineitem()))
1419                             self.issueReceive(copy);
1420                     };
1421                     cxl_link.onclick = function() {
1422                         self.cancelLid(copy.id());
1423                     };
1424                 }
1425             } else {
1426                 openils.Util.hide(cxl_link);
1427                 openils.Util.hide(unrecv_link);
1428                 openils.Util.hide(recv_link);
1429             }
1430         } else {
1431             openils.Util.hide(unrecv_link);
1432             openils.Util.hide(recv_link);
1433             openils.Util.hide(cxl_reason_link);
1434
1435             del_link.onclick = function() { self.deleteCopy(row) };
1436             openils.Util.show(del_link.parentNode);
1437         }
1438     }
1439
1440     this.cancelLid = function(lid_id) {
1441         lidCancelDialog._lid_id = lid_id;
1442         openils.Util.show(lidCancelDialog.domNode.parentNode);
1443         lidCancelDialog.show();
1444         if (!lidCancelDialog._prepared) {
1445             var widget = new openils.widget.AutoFieldWidget({
1446                 "fmField": "cancel_reason",
1447                 "fmClass": "acqlid",
1448                 "parentNode": dojo.byId("acq-lit-lid-cancel-reason"),
1449                 "orgLimitPerms": ["CREATE_PURCHASE_ORDER"],
1450                 "forceSync": true
1451             });
1452             widget.build(
1453                 function(w, ww) {
1454                     acqLidCancelButton.onClick = function() {
1455                         if (w.attr("value")) {
1456                             if (confirm(localeStrings.LID_CANCEL_CONFIRM)) {
1457                                 self._cancelLid(
1458                                     lidCancelDialog._lid_id,
1459                                     w.attr("value")
1460                                 );
1461                             }
1462                             lidCancelDialog.hide();
1463                         }
1464                     };
1465                     lidCancelDialog._prepared = true;
1466                 }
1467             );
1468         }
1469     };
1470
1471     this._cancelLid = function(lid_id, reason) {
1472         fieldmapper.standardRequest(
1473             ["open-ils.acq", "open-ils.acq.lineitem_detail.cancel"], {
1474                 "params": [openils.User.authtoken, lid_id, reason],
1475                 "async": true,
1476                 "onresponse": function(r) {
1477                     if (r = openils.Util.readResponse(r)) {
1478                         if (r.lid) {
1479                             for (var id in r.lid) {
1480                                 /* actually this should only iterate once */
1481                                 self.copyCache[id].cancel_reason(
1482                                     r.lid[id].cancel_reason
1483                                 );
1484                                 self.updateLidState(self.copyCache[id]);
1485                             }
1486                         }
1487                     }
1488                 }
1489             }
1490         );
1491     };
1492
1493     this._confirmAlert = function(li, lin) {
1494         return confirm(
1495             dojo.string.substitute(
1496                 localeStrings.CONFIRM_LI_ALERT, [
1497                     (new openils.acq.Lineitem({"lineitem": li})).findAttr(
1498                         "title", "lineitem_marc_attr_definition"
1499                     ),
1500                     lin.alert_text().code(),
1501                     lin.alert_text().description() || "",
1502                     lin.value()
1503                 ]
1504             )
1505         );
1506     };
1507
1508     this.checkLiAlerts = function(li_id) {
1509         var li = this.liCache[li_id];
1510
1511         var alert_notes = li.lineitem_notes().filter(
1512             function(o) { return Boolean(o.alert_text()); }
1513         );
1514
1515         /* this is _intentionally_ not done in a call to forEach() ... */
1516         for (var i = 0; i < alert_notes.length; i++) {
1517             if (this.noteAcks[alert_notes[i].id()])
1518                 continue;
1519             else if (!this._confirmAlert(li, alert_notes[i]))
1520                 return false;
1521             else
1522                 this.noteAcks[alert_notes[i].id()] = true;
1523         }
1524
1525         return true;
1526     };
1527
1528     this.deleteCopy = function(row) {
1529         var copy = this.copyCache[row.getAttribute('copy_id')];
1530         copy.isdeleted(true);
1531         if(copy.isnew())
1532             delete this.copyCache[copy.id()];
1533         this.copyTbody.removeChild(row);
1534     }
1535
1536     this._virtDfaCountsAsList = function() {
1537         var L = [];
1538         for (var key in this.virtDfaCounts) {
1539             for (var i = 0; i < this.virtDfaCounts[key]; i++)
1540                 L.push(key);
1541         }
1542         return L;
1543     }
1544
1545     this.confirmBreachedCopyFunds = function(copies) {
1546         var stop = 0, warning = 0;
1547         copies.forEach(
1548             function(o) {
1549                 if (o.fund()) {
1550                     var state = self.fundBalanceState[o.fund()];
1551                     if (state[0] /* stop */)
1552                         stop++;
1553                     else if (state[1] /* warning */)
1554                         warning++;
1555                 }
1556             }
1557         );
1558
1559         if (stop) {
1560             return confirm(localeStrings.CONFIRM_FUNDS_AT_STOP);
1561         } else if (warning) {
1562             return confirm(localeStrings.CONFIRM_FUNDS_AT_WARNING);
1563         }
1564         return true;
1565     };
1566
1567     this.saveCopyChanges = function(liId) {
1568         var self = this;
1569         var copies = [];
1570
1571
1572         var total = 0;
1573         for(var id in this.copyCache) {
1574             var c = this.copyCache[id];
1575             if(!c.isdeleted()) total++;
1576             if(c.isnew() || c.ischanged() || c.isdeleted()) {
1577                 if(c.id() < 0) c.id(null);
1578                 copies.push(c);
1579             }
1580         }
1581
1582
1583         dojo.byId('acq-lit-copy-count-label-' + liId).innerHTML = total;
1584
1585
1586         if (copies.length > 0) {
1587             if (!this.confirmBreachedCopyFunds(copies))
1588                 return;
1589
1590             if (typeof(this._copy_count_cb) == "function")
1591                 this._copy_count_cb(liId, total);
1592
1593             openils.Util.show("acq-lit-update-copies-progress");
1594             fieldmapper.standardRequest(
1595                 ['open-ils.acq', 'open-ils.acq.lineitem_detail.cud.batch'],
1596                 {   async: true,
1597                     params: [openils.User.authtoken, copies],
1598                     onresponse: function(r) {
1599                         var res = openils.Util.readResponse(r);
1600                         litUpdateCopiesProgress.update(res);
1601                     },
1602                     oncomplete: function() {
1603                         self.drawCopies(liId);
1604                         openils.Util.hide("acq-lit-update-copies-progress");
1605                     }
1606                 }
1607             );
1608         }
1609
1610         var dfa_list = this._virtDfaCountsAsList();
1611         if (dfa_list.length > 0) {
1612             fieldmapper.standardRequest(
1613                 ["open-ils.acq",
1614                 "open-ils.acq.distribution_formula.record_application"],
1615                 {
1616                     "async": true,
1617                     "params": [openils.User.authtoken, dfa_list, liId],
1618                     "onresponse": function(r) {
1619                         var res = openils.Util.readResponse(r);
1620                         if (res && res.length < dfa_list.length)
1621                             alert(localeStrings.DFA_NOT_ALL);
1622                     }
1623                 }
1624             );
1625             this.virtDfaCounts = {};
1626         }
1627     }
1628
1629     this._updateCreatePoPrepayCheckbox = function(prepay) {
1630         var prepay = openils.Util.isTrue(prepay);
1631         this._prepayRequiredByVendor = prepay;
1632         dijit.byId("acq-lit-po-prepay").attr("checked", prepay);
1633     };
1634
1635     this._confirmPoPrepaySituation = function() {
1636         var want_prepay = dijit.byId("acq-lit-po-prepay").attr("checked");
1637         if (want_prepay != this._prepayRequiredByVendor) {
1638             return confirm(
1639                 want_prepay ?
1640                     localeStrings.VENDOR_SAYS_PREPAY_NOT_NEEDED :
1641                     localeStrings.VENDOR_SAYS_PREPAY_NEEDED
1642             );
1643         } else {
1644             return true;
1645         }
1646     };
1647
1648     this.applySelectedLiAction = function(action) {
1649         var self = this;
1650         switch(action) {
1651
1652             case 'delete_selected':
1653                 this._deleteLiList(self.getSelected());
1654                 break;
1655
1656             case 'create_order':
1657
1658                 if(!this.createPoProviderSelector) {
1659                     var widget = new openils.widget.AutoFieldWidget({
1660                         fmField : 'provider',
1661                         fmClass : 'acqpo',
1662                         searchFilter: {"active": "t"},
1663                         parentNode : dojo.byId('acq-lit-po-provider'),
1664                         dijitArgs : {
1665                             "onChange": function() {
1666                                 if (this.item) {
1667                                     self._updateCreatePoPrepayCheckbox(
1668                                         this.item.prepayment_required
1669                                     );
1670                                 }
1671                             }
1672                         }
1673                     });
1674                     widget.build(
1675                         function(w) { self.createPoProviderSelector = w; }
1676                     );
1677                 }
1678
1679                 if(!this.createPoAgencySelector) {
1680                     var widget = new openils.widget.AutoFieldWidget({
1681                         fmField : 'ordering_agency',
1682                         fmClass : 'acqpo',
1683                         parentNode : dojo.byId('acq-lit-po-agency'),
1684                         orgLimitPerms : ['CREATE_PURCHASE_ORDER'],
1685                     });
1686                     widget.build(
1687                         function(w) { self.createPoAgencySelector = w; }
1688                     );
1689                 }
1690
1691          
1692                 acqLitPoCreateDialog.show();
1693                 break;
1694
1695             case 'save_picklist':
1696                 this._loadPLSelect();
1697                 acqLitSavePlDialog.show();
1698                 break;
1699
1700             case 'selector_ready':
1701             case 'order_ready':
1702                 acqLitChangeLiStateDialog.attr('state', action.replace('_', '-'));
1703                 acqLitChangeLiStateDialog.show();
1704                 break;
1705
1706             case 'print_po':
1707                 this.printPO();
1708                 break;
1709
1710             case 'receive_po':
1711                 this.receivePO();
1712                 break;
1713
1714             case 'rollback_receive_po':
1715                 this.rollbackPoReceive();
1716                 break;
1717
1718             case 'create_assets':
1719                 this.createAssets();
1720                 break;
1721
1722             case 'export_attr_list':
1723                 this.chooseExportAttr();
1724                 break;
1725
1726             case 'add_brief_record':
1727                 if(this.isPO)
1728                     location.href = oilsBasePath + '/acq/picklist/brief_record?po=' + this.isPO;
1729                 else
1730                     location.href = oilsBasePath + '/acq/picklist/brief_record?pl=' + this.isPL;
1731
1732                 break;
1733
1734             case "cancel_lineitems":
1735                 this.maybeCancelLineitems();
1736                 break;
1737         }
1738     }
1739
1740     this.createAssets = function() {
1741         if(!this.isPO) return;
1742         if(!confirm(localeStrings.CREATE_PO_ASSETS_CONFIRM)) return;
1743         this.show('acq-lit-progress-numbers');
1744         var self = this;
1745         fieldmapper.standardRequest(
1746             ['open-ils.acq', 'open-ils.acq.purchase_order.assets.create'],
1747             {   async: true,
1748                 params: [this.authtoken, this.isPO],
1749                 onresponse: function(r) {
1750                     var resp = openils.Util.readResponse(r);
1751                     self._updateProgressNumbers(resp, true);
1752                 }
1753             }
1754         );
1755     }
1756
1757     this.maybeCancelLineitems = function() {
1758         openils.Util.show("acq-lit-cancel-reason", "inline");
1759         if (!acqLitCancelLineitemsButton._prepared) {
1760             var widget = new openils.widget.AutoFieldWidget({
1761                 "fmField": "cancel_reason",
1762                 "fmClass": "jub",
1763                 "parentNode": dojo.byId("acq-lit-cancel-reason-selector"),
1764                 "orgLimitPerms": ["CREATE_PURCHASE_ORDER"],
1765                 "forceSync": true
1766             });
1767             widget.build(
1768                 function(w, ww) {
1769                     acqLitCancelLineitemsButton.onClick = function() {
1770                         if (w.attr("value")) {
1771                             if (confirm(localeStrings.LI_CANCEL_CONFIRM)) {
1772                                 self._cancelLineitems(w.attr("value"));
1773                             }
1774                             openils.Util.hide("acq-lit-cancel-reason");
1775                         }
1776                     };
1777                     acqLitCancelLineitemsButton._prepared = true;
1778                 }
1779             );
1780         }
1781     };
1782
1783     this._cancelLineitems = function(reason) {
1784         var id_list = this.getSelected().map(function(o) { return o.id(); });
1785         fieldmapper.standardRequest(
1786             ["open-ils.acq", "open-ils.acq.lineitem.cancel.batch"], {
1787                 "params": [openils.User.authtoken, id_list, reason],
1788                 "async": true,
1789                 "onresponse": function(r) {
1790                     if (r = openils.Util.readResponse(r)) {
1791                         if (r.li) {
1792                             for (var id in r.li) {
1793                                 self.liCache[id].state(r.li[id].state);
1794                                 self.liCache[id].cancel_reason(
1795                                     r.li[id].cancel_reason
1796                                 );
1797                                 self.updateLiState(self.liCache[id]);
1798                             }
1799                         }
1800                         if (r.lid && self.copyCache) {
1801                             for (var id in r.lid) {
1802                                 if (self.copyCache[id]) {
1803                                     self.copyCache[id].cancel_reason(
1804                                         r.lid[id].cancel_reason
1805                                     );
1806                                     self.updateLidState(self.copyCache[id]);
1807                                 }
1808                             }
1809                         }
1810                     }
1811                 }
1812             }
1813         );
1814     };
1815
1816     this.chooseExportAttr = function() {
1817         if (!acqLitExportAttrSelector._li_setup) {
1818             var self = this;
1819             acqLitExportAttrSelector.store = new dojo.data.ItemFileReadStore(
1820                 {
1821                     "data": acqliad.toStoreData(
1822                         this.pcrud.search(
1823                             "acqliad", {"code": li_exportable_attrs}
1824                         )
1825                     )
1826                 }
1827             );
1828             acqLitExportAttrSelector.setValue();
1829             acqLitExportAttrButton.onClick = function(){self.exportAttrList();};
1830             acqLitExportAttrSelector._li_setup = true;
1831         }
1832         openils.Util.show("acq-lit-export-attr-holder", "inline");
1833     };
1834
1835     this.exportAttrList = function() {
1836         var attr_def = acqLitExportAttrSelector.item;
1837         var li_list = this.getSelected();
1838         var value_list = li_list.map(
1839             function(li) {
1840                 return (new openils.acq.Lineitem({"lineitem": li})).findAttr(
1841                     attr_def.code, "lineitem_marc_attr_definition"
1842                 );
1843             }
1844         ).filter(function(attr) { return Boolean(attr); });
1845
1846         if (value_list.length > 0) {
1847             if (value_list.length < li_list.length) {
1848                 if (!confirm(
1849                     dojo.string.substitute(
1850                         localeStrings.EXPORT_SHORT_LIST, [attr_def.description]
1851                     )
1852                 )) {
1853                     return;
1854                 }
1855             }
1856             try {
1857                 openils.XUL.contentToFileSaveDialog(
1858                     value_list.join("\n"),
1859                     localeStrings.EXPORT_SAVE_DIALOG_TITLE
1860                 );
1861             } catch (E) {
1862                 alert(E);
1863             }
1864         } else {
1865             alert(dojo.string.substitute(
1866                 localeStrings.EXPORT_EMPTY_LIST, [attr_def.description]
1867             ));
1868         }
1869
1870         openils.Util.hide("acq-lit-export-attr-holder");
1871     };
1872
1873     this.printPO = function() {
1874         if(!this.isPO) return;
1875         progressDialog.show(true);
1876         fieldmapper.standardRequest(
1877             ['open-ils.acq', 'open-ils.acq.purchase_order.format'],
1878             {   async: true,
1879                 params: [this.authtoken, this.isPO, 'html'],
1880                 oncomplete: function(r) {
1881                     progressDialog.hide();
1882                     var evt = openils.Util.readResponse(r);
1883                     if(evt && evt.template_output()) {
1884                         win = window.open('','', 'resizable,width=800,height=600,scrollbars=1');
1885                         win.document.body.innerHTML = evt.template_output().data();
1886                     }
1887                 }
1888             }
1889         );
1890     }
1891
1892
1893     this.receivePO = function() {
1894         if (!this.isPO) return;
1895
1896         for (var id in this.liCache) {
1897             /* assumption: liCache reflects exactly the
1898              * set of LIs that belong to our PO */
1899             if (this.liCache[id].state() != "received" &&
1900                 !this.checkLiAlerts(id)) return;
1901         }
1902
1903         this.show('acq-lit-progress-numbers');
1904         var self = this;
1905         fieldmapper.standardRequest(
1906             ['open-ils.acq', 'open-ils.acq.purchase_order.receive'],
1907             {   async: true,
1908                 params: [this.authtoken, this.isPO],
1909                 onresponse : function(r) {
1910                     var resp = openils.Util.readResponse(r);
1911                     self._updateProgressNumbers(resp, true);
1912                 },
1913             }
1914         );
1915     }
1916
1917     this.issueReceive = function(obj, rollback) {
1918         /* (For now) there shall be no marking LI or LIDs (un)received
1919          * except from the actual "view PO" interface. */
1920         if (!this.isPO) return;
1921
1922         var part =
1923             {"jub": "lineitem", "acqlid": "lineitem_detail"}[obj.classname];
1924         var method =
1925             "open-ils.acq." + part + ".receive" + (rollback ? ".rollback" : "");
1926
1927         progressDialog.show(true);
1928         fieldmapper.standardRequest(
1929             ["open-ils.acq", method], {
1930                 "async": true,
1931                 "params": [this.authtoken, obj.id()],
1932                 "onresponse": function(r) {
1933                     self.handleReceive(openils.Util.readResponse(r));
1934                     progressDialog.hide();
1935                 }
1936             }
1937         );
1938     };
1939
1940     /**
1941      * Handles the responses from receive and rollback ML calls.
1942      */
1943     this.handleReceive = function(resp) {
1944         if (resp) {
1945             if (resp.li) {
1946                 for (var li_id in resp.li) {
1947                     for (var key in resp.li[li_id])
1948                         self.liCache[li_id][key](resp.li[li_id][key]);
1949                     self.updateLiState(self.liCache[li_id]);
1950                 }
1951             }
1952             if (resp.po) {
1953                 if (typeof(self.poUpdateCallback) == "function")
1954                     self.poUpdateCallback(resp.po);
1955             }
1956             if (resp.lid) {
1957                 for (var lid_id in resp.lid) {
1958                     for (var key in resp.lid[lid_id])
1959                         self.copyCache[lid_id][key](resp.lid[lid_id][key]);
1960                     self.updateLidState(self.copyCache[lid_id]);
1961                 }
1962             }
1963         }
1964     };
1965
1966     this.rollbackPoReceive = function() {
1967         if(!this.isPO) return;
1968         if(!confirm(localeStrings.ROLLBACK_PO_RECEIVE_CONFIRM)) return;
1969         this.show('acq-lit-progress-numbers');
1970         var self = this;
1971         fieldmapper.standardRequest(
1972             ['open-ils.acq', 'open-ils.acq.purchase_order.receive.rollback'],
1973             {   async: true,
1974                 params: [this.authtoken, this.isPO],
1975                 onresponse : function(r) {
1976                     var resp = openils.Util.readResponse(r);
1977                     self._updateProgressNumbers(resp, true);
1978                 },
1979             }
1980         );
1981     }
1982
1983     this._updateProgressNumbers = function(resp, reloadOnComplete) {
1984         if(!resp) return;
1985         dojo.byId('acq-pl-lit-li-processed').innerHTML = resp.li;
1986         dojo.byId('acq-pl-lit-lid-processed').innerHTML = resp.lid;
1987         dojo.byId('acq-pl-lit-debits-processed').innerHTML = resp.debits_accrued;
1988         dojo.byId('acq-pl-lit-bibs-processed').innerHTML = resp.bibs;
1989         dojo.byId('acq-pl-lit-indexed-processed').innerHTML = resp.indexed;
1990         dojo.byId('acq-pl-lit-copies-processed').innerHTML = resp.copies;
1991         if(resp.complete && reloadOnComplete) 
1992             location.href = location.href;
1993     }
1994
1995
1996     this._createPO = function(fields) {
1997         this.show('acq-lit-progress-numbers');
1998         var po = new fieldmapper.acqpo();
1999         po.provider(this.createPoProviderSelector.attr('value'));
2000         po.ordering_agency(this.createPoAgencySelector.attr('value'));
2001         po.prepayment_required(fields.prepayment_required[0] ? true : false);
2002
2003         var selected = this.getSelected( (fields.create_from == 'all') );
2004         if(selected.length == 0) return;
2005
2006         var max = selected.length * 3;
2007
2008         var self = this;
2009         fieldmapper.standardRequest(
2010             ['open-ils.acq', 'open-ils.acq.purchase_order.create'],
2011             {   async: true,
2012                 params: [
2013                     openils.User.authtoken, 
2014                     po, 
2015                     {
2016                         lineitems : selected.map(function(li) { return li.id() }),
2017                         create_assets : fields.create_assets[0],
2018                     }
2019                 ],
2020
2021                 onresponse : function(r) {
2022                     var resp = openils.Util.readResponse(r);
2023                     self._updateProgressNumbers(resp);
2024                     if(resp.complete) 
2025                         location.href = oilsBasePath + '/eg/acq/po/view/' + resp.purchase_order.id();
2026                 }
2027             }
2028         );
2029     }
2030
2031     this._deleteLiList = function(list, idx) {
2032         if(idx == null) idx = 0;
2033         if(idx >= list.length) return;
2034         var liId = list[idx].id();
2035         fieldmapper.standardRequest(
2036             ['open-ils.acq', 'open-ils.acq.lineitem.delete'],
2037             {   async: true,
2038                 params: [openils.User.authtoken, liId],
2039                 oncomplete: function(r) {
2040                     self.removeLineitem(liId);
2041                     self._deleteLiList(list, ++idx);
2042                 }
2043             }
2044         );
2045     }
2046
2047     this.editOrderMarc = function(li) {
2048
2049         /*  To run in Firefox directly, must set signed.applets.codebase_principal_support
2050             to true in about:config */
2051
2052         if(!openils.XUL.enableXPConnect()) return;
2053
2054         if(openils.XUL.isXUL()) {
2055             win = window.open('/xul/' + openils.XUL.buildId() + '/server/cat/marcedit.xul');
2056         } else {
2057             win = window.open('/xul/server/cat/marcedit.xul'); 
2058         }
2059         var self = this;
2060         win.xulG = {
2061             record : {marc : li.marc()},
2062             save : {
2063                 label: 'Save Record', // XXX I18N
2064                 func: function(xmlString) {
2065                     li.marc(xmlString);
2066                     fieldmapper.standardRequest(
2067                         ['open-ils.acq', 'open-ils.acq.lineitem.update'],
2068                         {   async: true,
2069                             params: [openils.User.authtoken, li],
2070                             oncomplete: function(r) {
2071                                 openils.Util.readResponse(r);
2072                                 win.close();
2073                                 self.drawInfo(li.id())
2074                             }
2075                         }
2076                     );
2077                 },
2078             }
2079         };
2080     }
2081
2082     this._savePl = function(values) {
2083         var self = this;
2084         var selected = this.getSelected( (values.which == 'all') );
2085         openils.Util.show('acq-lit-generic-progress');
2086
2087         if(values.new_name) {
2088             openils.acq.Picklist.create(
2089                 {name: values.new_name}, 
2090                 function(id) {
2091                     self._updateLiList(id, selected, 0, 
2092                         function(){
2093                             location.href = oilsBasePath + '/eg/acq/picklist/view/' + id;
2094                         });
2095                 }
2096             );
2097         } else if(values.existing_pl) {
2098             // update lineitems to use an existing picklist
2099             self._updateLiList(values.existing_pl, selected, 0, 
2100                 function(){
2101                     location.href = oilsBasePath + '/eg/acq/picklist/view/' + values.existing_pl;
2102                 });
2103         }
2104     }
2105
2106     this._updateLiState = function(values, state) {
2107         var self = this;
2108         var selected = this.getSelected( (values.which == 'all') );
2109         if(!selected.length) return;
2110         dojo.forEach(selected, function(li) {li.state(state);});
2111         self._updateLiList(null, selected, 0, 
2112             // TODO consider inline updates for efficiency
2113             function() { location.href = location.href }
2114         );
2115     }
2116
2117     this._updateLiList = function(pl, list, idx, oncomplete) {
2118         if(idx >= list.length) return oncomplete();
2119         var li = list[idx];
2120         if(pl != null) li.picklist(pl);
2121         litGenericProgress.update({maximum: list.length, progress: idx});
2122         new openils.acq.Lineitem({lineitem:li}).update(
2123             function(r) {
2124                 self._updateLiList(pl, list, ++idx, oncomplete);
2125             }
2126         );
2127     }
2128
2129     this._loadPLSelect = function() {
2130         if(this._plSelectLoaded) return;
2131         var plList = [];
2132         function handleResponse(r) {
2133             plList.push(r.recv().content());
2134         }
2135         var method = 'open-ils.acq.picklist.user.retrieve';
2136         fieldmapper.standardRequest(
2137             ['open-ils.acq', method],
2138             {   async: true,
2139                 params: [this.authtoken],
2140                 onresponse: handleResponse,
2141                 oncomplete: function() {
2142                     self._plSelectLoaded = true;
2143                     acqLitAddExistingSelect.store = 
2144                         new dojo.data.ItemFileReadStore({data:acqpl.toStoreData(plList)});
2145                     acqLitAddExistingSelect.setValue();
2146                 }
2147             }
2148         );
2149     }
2150
2151     this.showRealCopyEditUI = function(li) {
2152         copyList = [];
2153         var self = this;
2154         this.volCache = {};
2155
2156         this._fetchLineitem(li.id(), 
2157             function(fullLi) {
2158                 li = self.liCache[li.id()] = fullLi;
2159
2160                 self.pcrud.search(
2161                     'acp', {
2162                         id : li.lineitem_details().map(
2163                             function(item) { return item.eg_copy_id() }
2164                         )
2165                     }, {
2166                         async : true,
2167                         oncomplete : function(r) {
2168                             try {
2169                                 var r_list = openils.Util.readResponse( r );
2170                                 for (var i = 0; i < r_list.length; i++) {
2171                                     var copy = r_list[i];
2172                                     var volId = copy.call_number();
2173                                     var volume = self.volCache[volId];
2174                                     if(!volume) {
2175                                         volume = self.volCache[volId] = self.pcrud.retrieve('acn', volId);
2176                                     }
2177                                     copy.call_number(volume);
2178                                     copyList.push(copy);
2179                                 }
2180                                 if (xulG) {
2181                                     // If we need to, we can pass in an update_copy function to handle the update instead of volume_item_creator
2182                                     xulG.volume_item_creator( { 'existing_copies' : copyList } );
2183                                 }
2184                             } catch(E) {
2185                                 alert('error in oncomplete: ' + E);
2186                             }
2187                         }
2188                     }
2189                 );
2190             }
2191         );
2192     }
2193
2194     
2195     /*
2196     this.saveRealCopies = function() {
2197         progressDialog.show(true);
2198         var list = this.realCopyList.filter(function(copy) { return copy.ischanged(); });
2199         this.pcrud.update(list, {oncomplete: function() { 
2200             progressDialog.hide();
2201             self.show('list');
2202         }});
2203     }
2204
2205     // grab the li-details for this lineitem, grab the linked copies and volumes, add them to the table
2206     this.showRealCopies = function(li) {
2207         while(this.realCopiesTbody.childNodes[0])
2208             this.realCopiesTbody.removeChild(this.realCopiesTbody.childNodes[0]);
2209         this.show('real-copies');
2210
2211         this.realCopyList = [];
2212         this.volCache = {};
2213         var tabIndex = 1000;
2214         var self = this;
2215
2216         acqLitSaveRealCopies.onClick = function() {
2217             self.saveRealCopies();
2218         }
2219
2220         this._fetchLineitem(li.id(), 
2221             function(fullLi) {
2222                 li = self.liCache[li.id()] = fullLi;
2223
2224                 self.pcrud.search(
2225                     'acp', {
2226                         id : li.lineitem_details().map(
2227                             function(item) { return item.eg_copy_id() }
2228                         )
2229                     }, {
2230                         async : true,
2231                         streaming : true,
2232                         onresponse : function(r) {
2233                             var copy = openils.Util.readResponse(r);
2234                             var volId = copy.call_number();
2235                             var volume = self.volCache[volId];
2236                             if(!volume) {
2237                                 volume = self.volCache[volId] = self.pcrud.retrieve('acn', volId);
2238                             }
2239                             self.addRealCopy(volume, copy, tabIndex++);
2240                         }
2241                     }
2242                 );
2243             }
2244         );
2245     }
2246
2247     this.addRealCopy = function(volume, copy, tabIndex) {
2248         var row = this.realCopiesRow.cloneNode(true);
2249         this.realCopyList.push(copy);
2250
2251         var selectNode;
2252         dojo.forEach(
2253             ['owning_lib', 'location', 'circ_modifier', 'label', 'barcode'],
2254
2255             function(field) {
2256                 var isvol = (field == 'owning_lib' || field == 'label');
2257                 var widget = new openils.widget.AutoFieldWidget({
2258                     fmField : field,
2259                     fmObject : isvol ? volume : copy,
2260                     parentNode : nodeByName(field, row),
2261                     readOnly : (field != 'barcode'),
2262                 });
2263
2264                 var widgetDrawn = null;
2265
2266                 if(field == 'barcode') {
2267
2268                     widgetDrawn = function(w, ww) {
2269                         var node = w.domNode;
2270                         node.setAttribute('tabindex', ''+tabIndex);
2271
2272                         // on enter, select the next barcode input
2273                         dojo.connect(w, 'onKeyDown',
2274                             function(e) {
2275                                 if(e.keyCode == dojo.keys.ENTER) {
2276                                     var ti = node.getAttribute('tabindex');
2277                                     var nextNode = dojo.query('[tabindex=' + String(Number(ti) + 1) + ']', self.realCopiesTbody)[0];
2278                                     if(nextNode) nextNode.select();
2279                                 }
2280                             }
2281                         );
2282
2283                         dojo.connect(w, 'onChange', 
2284                             function(val) { 
2285                                 if(!val || val == copy.barcode()) return;
2286                                 copy.ischanged(true);
2287                                 copy.barcode(val);
2288                             }
2289                         );
2290
2291
2292                         if(self.realCopiesTbody.getElementsByTagName('TR').length == 0)
2293                             selectNode = node;
2294                     }
2295                 }
2296
2297                 widget.build(widgetDrawn);
2298             }
2299         );
2300
2301         this.realCopiesTbody.appendChild(row);
2302         if(selectNode) selectNode.select();
2303     };
2304     */
2305
2306 }