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