]> git.evergreen-ils.org Git - Evergreen.git/blob - Open-ILS/web/js/ui/default/acq/common/li_table.js
Acq: more LI search interface improvements
[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.ProgressBar');
8 dojo.require('openils.User');
9 dojo.require('openils.Util');
10 dojo.require('openils.acq.Lineitem');
11 dojo.require('openils.acq.PO');
12 dojo.require('openils.acq.Picklist');
13 dojo.require('openils.widget.AutoFieldWidget');
14 dojo.require('dojo.data.ItemFileReadStore');
15 dojo.require('openils.widget.ProgressDialog');
16 dojo.require('openils.PermaCrud');
17 dojo.require('openils.XUL');
18
19 dojo.requireLocalization('openils.acq', 'acq');
20 var localeStrings = dojo.i18n.getLocalization('openils.acq', 'acq');
21 const XUL_OPAC_WRAPPER = 'chrome://open_ils_staff_client/content/cat/opac.xul';
22 var li_exportable_attrs = ["issn", "isbn", "upc"];
23
24 function nodeByName(name, context) {
25     return dojo.query('[name='+name+']', context)[0];
26 }
27
28
29 var liDetailBatchFields = ['fund', 'owning_lib', 'location', 'collection_code', 'circ_modifier', 'cn_label'];
30 var liDetailFields = liDetailBatchFields.concat(['barcode', 'note']);
31
32 function AcqLiTable() {
33
34     var self = this;
35     this.liCache = {};
36     this.plCache = {};
37     this.poCache = {};
38     this.dfaCache = [];
39     this.dfeOffset = 0;
40     this.toggleState = false;
41     this.tbody = dojo.byId('acq-lit-tbody');
42     this.selectors = [];
43     this.authtoken = openils.User.authtoken;
44     this.rowTemplate = this.tbody.removeChild(dojo.byId('acq-lit-row'));
45     this.copyTbody = dojo.byId('acq-lit-li-details-tbody');
46     this.copyRow = this.copyTbody.removeChild(dojo.byId('acq-lit-li-details-row'));
47     this.copyBatchRow = dojo.byId('acq-lit-li-details-batch-row');
48     this.copyBatchWidgets = {};
49     this.liNotesTbody = dojo.byId('acq-lit-notes-tbody');
50     this.liNotesRow = this.liNotesTbody.removeChild(dojo.byId('acq-lit-notes-row'));
51     this.realCopiesTbody = dojo.byId('acq-lit-real-copies-tbody');
52     this.realCopiesRow = this.realCopiesTbody.removeChild(dojo.byId('acq-lit-real-copies-row'));
53     this._copy_fields_for_acqdf = ['owning_lib', 'location'];
54
55     dojo.connect(acqLitLiActionsSelector, 'onChange', 
56         function() { 
57             self.applySelectedLiAction(this.attr('value')) 
58             acqLitLiActionsSelector.attr('value', '_');
59         });
60
61     acqLitCreatePoSubmit.onClick = function() {
62         acqLitPoCreateDialog.hide();
63         self._createPO(acqLitPoCreateDialog.getValues());
64     }
65
66     acqLitSavePlButton.onClick = function() {
67         acqLitSavePlDialog.hide();
68         self._savePl(acqLitSavePlDialog.getValues());
69     }
70
71     acqLitCancelLiStateButton.onClick = function() {
72         acqLitChangeLiStateDialog.hide();
73     }
74     acqLitSaveLiStateButton.onClick = function() {
75         acqLitChangeLiStateDialog.hide();
76         self._updateLiState(acqLitChangeLiStateDialog.getValues(), acqLitChangeLiStateDialog.attr('state'));
77     }
78
79
80     //dojo.byId('acq-lit-notes-new-button').onclick = function(){acqLitCreateLiNoteDialog.show();}
81
82     dojo.byId('acq-lit-select-toggle').onclick = function(){self.toggleSelect()};
83     dojo.byId('acq-lit-info-back-button').onclick = function(){self.show('list')};
84     dojo.byId('acq-lit-copies-back-button').onclick = function(){self.show('list')};
85     dojo.byId('acq-lit-notes-back-button').onclick = function(){self.show('list')};
86     dojo.byId('acq-lit-real-copies-back-button').onclick = function(){self.show('list')};
87
88     this.reset = function() {
89         while(self.tbody.childNodes[0])
90             self.tbody.removeChild(self.tbody.childNodes[0]);
91         self.selectors = [];
92     };
93     
94     this.setNext = function(handler) {
95         var link = dojo.byId('acq-lit-next');
96         if(handler) {
97             dojo.style(link, 'visibility', 'visible');
98             link.onclick = handler;
99         } else {
100             dojo.style(link, 'visibility', 'hidden');
101         }
102     };
103
104     this.setPrev = function(handler) {
105         var link = dojo.byId('acq-lit-prev');
106         if(handler) {
107             dojo.style(link, 'visibility', 'visible'); 
108             link.onclick = handler; 
109         } else {
110             dojo.style(link, 'visibility', 'hidden');
111         }
112     };
113
114     this.show = function(div) {
115         openils.Util.hide('acq-lit-table-div');
116         openils.Util.hide('acq-lit-info-div');
117         openils.Util.hide('acq-lit-li-details');
118         openils.Util.hide('acq-lit-notes-div');
119         openils.Util.hide('acq-lit-real-copies-div');
120         switch(div) {
121             case 'list':
122                 openils.Util.show('acq-lit-table-div');
123                 break;
124             case 'info':
125                 openils.Util.show('acq-lit-info-div');
126                 break;
127             case 'copies':
128                 openils.Util.show('acq-lit-li-details');
129                 break;
130             case 'real-copies':
131                 openils.Util.show('acq-lit-real-copies-div');
132                 break;
133             case 'notes':
134                 openils.Util.show('acq-lit-notes-div');
135                 break;
136             default:
137                 if(div) 
138                     openils.Util.show(div);
139         }
140     }
141
142     this.hide = function() {
143         this.show(null);
144     }
145
146     this.toggleSelect = function() {
147         if(self.toggleState) 
148             dojo.forEach(self.selectors, function(i){i.checked = false});
149         else 
150             dojo.forEach(self.selectors, function(i){i.checked = true});
151         self.toggleState = !self.toggleState;
152     };
153
154
155     /** @param all If true, assume all are selected */
156     this.getSelected = function(all) {
157         var selected = [];
158         dojo.forEach(self.selectors, 
159             function(i) { 
160                 if(i.checked || all)
161                     selected.push(self.liCache[i.parentNode.parentNode.getAttribute('li')]);
162             }
163         );
164         return selected;
165     };
166
167     this.setRowAttr = function(td, liWrapper, field, type) {
168         var val = liWrapper.findAttr(field, type || 'lineitem_marc_attr_definition') || '';
169         td.appendChild(document.createTextNode(val));
170     };
171
172     /**
173      * Inserts a single lineitem into the growing table of lineitems
174      * @param {Object} li The lineitem object to insert
175      */
176     this.addLineitem = function(li, skip_final_placement) {
177         this.liCache[li.id()] = li;
178
179         // sort the lineitem notes on edit_time
180         if(!li.lineitem_notes()) li.lineitem_notes([]);
181
182         var liWrapper = new openils.acq.Lineitem({lineitem:li});
183         var row = self.rowTemplate.cloneNode(true);
184         row.setAttribute('li', li.id());
185         var tds = dojo.query('[attr]', row);
186         dojo.forEach(tds, function(td) {self.setRowAttr(td, liWrapper, td.getAttribute('attr'), td.getAttribute('attr_type'));});
187         dojo.query('[name=source_label]', row)[0].appendChild(document.createTextNode(li.source_label()));
188
189         var isbn = liWrapper.findAttr('isbn', 'lineitem_marc_attr_definition');
190         if(isbn) {
191             // XXX media prefix for added content
192             dojo.query('[name=jacket]', row)[0].setAttribute('src', '/opac/extras/ac/jacket/small/' + isbn);
193         }
194
195         dojo.query('[attr=title]', row)[0].onclick = function() {self.drawInfo(li.id())};
196         dojo.query('[name=copieslink]', row)[0].onclick = function() {self.drawCopies(li.id())};
197         dojo.query('[name=notes_count]', row)[0].innerHTML = li.lineitem_notes().length;
198         dojo.query('[name=noteslink]', row)[0].onclick = function() {self.drawLiNotes(li)};
199
200         // show which PO this lineitem is a member of
201         if(li.purchase_order() && !this.isPO) {
202             var po = 
203                 this.poCache[li.purchase_order()] =
204                 this.poCache[li.purchase_order()] ||
205                 fieldmapper.standardRequest(
206                     ['open-ils.acq', 'open-ils.acq.purchase_order.retrieve'],
207                     {params: [
208                         this.authtoken, li.purchase_order(), {
209                             "flesh_price_summary": true,
210                             "flesh_lineitem_count": true
211                         }
212                     ]});
213             if(po && !this.isMeta) {
214                 openils.Util.show(nodeByName('po', row), 'inline');
215                 var link = nodeByName('po_link', row);
216                 link.setAttribute('href', oilsBasePath + '/acq/po/view/' + li.purchase_order());
217                 link.innerHTML = 'PO: ' + po.name(); // TODO i18n
218             }
219         }
220
221         // show which picklist this lineitem is a member of
222         if(li.picklist() && (this.isPO || this.isMeta)) {
223             var pl = 
224                 this.plCache[li.picklist()] = 
225                 this.plCache[li.picklist()] || 
226                 fieldmapper.standardRequest(
227                     ['open-ils.acq', 'open-ils.acq.picklist.retrieve'],
228                     {params: [this.authtoken, li.picklist()]});
229             if(pl) {
230                 openils.Util.show(nodeByName('pl', row), 'inline');
231                 var link = nodeByName('pl_link', row);
232                 link.setAttribute('href', oilsBasePath + '/acq/picklist/view/' + li.picklist());
233                 link.innerHTML = 'PL: '+pl.name(); // TODO i18n
234             }
235         }
236
237         var countNode = nodeByName('count', row);
238         var count = li.item_count() || 0;
239         if (typeof(this._copy_count_cb) == "function") {
240             this._copy_count_cb(li.id(), count);
241         }
242         countNode.innerHTML = count;
243         countNode.id = 'acq-lit-copy-count-label-' + li.id();
244
245         // lineitem state
246         nodeByName('li_state', row).innerHTML = li.state(); // TODO i18n state labels
247         openils.Util.addCSSClass(row, 'oils-acq-li-state-' + li.state());
248
249         // lineitem price
250         var priceInput = dojo.query('[name=price]', row)[0];
251         var priceData = liWrapper.getPrice();
252         priceInput.value = (priceData) ? priceData.price : '';
253         priceInput.onchange = function() { self.updateLiPrice(priceInput, li) };
254
255         var recv_link = dojo.query('[name=receive_link]', row)[0];
256
257         if(li.state() == 'on-order') {
258             recv_link.onclick = function() {
259                 self.receiveLi(li);
260                 openils.Util.hide(recv_link)
261             }
262         } else {
263             openils.Util.hide(recv_link);
264         }
265
266         // TODO we should allow editing before receipt, in which case the
267         // test should be "if 1 or more real (acp) copies exist
268         if(li.state() == 'received') {
269             var real_copies_link = dojo.query('[name=real_copies_link]', row)[0];
270             openils.Util.show(real_copies_link);
271             real_copies_link.onclick = function() {
272                 self.showRealCopies(li);
273             }
274         }
275
276         if (!skip_final_placement) {
277             self.tbody.appendChild(row);
278             self.selectors.push(dojo.query('[name=selectbox]', row)[0]);
279         } else {
280             return row;
281         }
282     };
283
284     /**
285      * Draws and shows the lineitem notes pane
286      */
287     this.drawLiNotes = function(li) {
288         var self = this;
289
290         li.lineitem_notes(
291             li.lineitem_notes().sort(
292                 function(a, b) { 
293                     if(a.edit_time() < b.edit_time()) return 1;
294                     return -1;
295                 }
296             )
297         );
298
299         while(this.liNotesTbody.childNodes[0])
300             this.liNotesTbody.removeChild(this.liNotesTbody.childNodes[0]);
301         this.show('notes');
302
303         acqLitCreateLiNoteSubmit.onClick = function() {
304             var value = acqLitCreateNoteText.attr('value');
305             if(!value) return;
306             var note = new fieldmapper.acqlin();
307             note.isnew(true);
308             note.value(value);
309             note.lineitem(li.id());
310             self.updateLiNotes(li, note);
311         }
312
313         dojo.byId('acq-lit-notes-save-button').onclick = function() {
314             self.updateLiNotes(li);
315         }
316
317         dojo.forEach(li.lineitem_notes(), function(note) { self.addLiNote(li, note) });
318     }
319
320     /**
321      * Draws a single lineitem note in the notes pane
322      */
323     this.addLiNote = function(li, note) {
324         if(note.isdeleted()) return;
325         var self = this;
326         var row = self.liNotesRow.cloneNode(true);
327         dojo.query('[name=value]', row)[0].innerHTML = note.value();
328
329         dojo.query('[name=delete]', row)[0].onclick = function() {
330             note.isdeleted(true);
331             self.liNotesTbody.removeChild(row);
332         };
333
334         if(note.edit_time()) {
335             dojo.query('[name=edit_time]', row)[0].innerHTML = 
336                 dojo.date.locale.format(
337                     dojo.date.stamp.fromISOString(note.edit_time()), 
338                     {formatLength:'short'});
339         }
340
341         self.liNotesTbody.appendChild(row);
342     }
343
344     /**
345      * Updates any new/changed/deleted notes on the server
346      */
347     this.updateLiNotes = function(li, newNote) {
348
349         var notes;
350         if(newNote) {
351             notes = [newNote];
352         } else {
353             notes = li.lineitem_notes().filter(
354                 function(note) {
355                     if(note.ischanged() || note.isnew() || note.isdeleted())
356                         return note;
357                 }
358             );
359         }
360
361         if(notes.length == 0) return;
362         progressDialog.show();
363
364         fieldmapper.standardRequest(
365             ['open-ils.acq', 'open-ils.acq.lineitem_note.cud.batch'],
366             {   async : true,
367                 params : [this.authtoken, notes],
368                 onresponse : function(r) {
369                     var resp = openils.Util.readResponse(r);
370
371                     if(resp.complete) {
372
373                         if(!newNote) {
374                             // remove the old changed notes
375                             var list = [];
376                             dojo.forEach(li.lineitem_notes(), 
377                                 function(note) {
378                                     if(!(note.ischanged() || note.isnew() || note.isdeleted()))
379                                         list.push(note);
380                                 }
381                             );
382                             li.lineitem_notes(list);
383                         }
384
385                         progressDialog.hide();
386                         self.drawLiNotes(li);
387                         return;
388                     }
389
390                     progressDialog.update(resp);
391                     var newnote = resp.note;
392
393                     if(!newnote.isdeleted()) {
394                         newnote.isnew(false);
395                         newnote.ischanged(false);
396                         li.lineitem_notes().push(newnote);
397                     }
398                 },
399             }
400         );
401     }
402
403     this.updateLiPrice = function(input, li) {
404
405         var price = input.value;
406         var liWrapper = new openils.acq.Lineitem({lineitem:li});
407         var oldPrice = liWrapper.getPrice() || null;
408
409         if(oldPrice) oldPrice = oldPrice.price;
410         if(price == oldPrice) return;
411
412         fieldmapper.standardRequest(
413             ['open-ils.acq', 'open-ils.acq.lineitem.price.set'],
414             {   async : true,
415                 params : [this.authtoken, li.id(), price],
416                 oncomplete : function(r) {
417                     openils.Util.readResponse(r);
418                 }
419             }
420         );
421     }
422
423     this.removeLineitem = function(liId) {
424         this.tbody.removeChild(dojo.query('[li='+liId+']', this.tbody)[0]);
425         delete this.liCache[liId];
426         //selected.push(self.liCache[i.parentNode.parentNode.getAttribute('li')]);
427     }
428
429     this.drawInfo = function(liId) {
430         this.show('info');
431         openils.acq.Lineitem.fetchAttrDefs(
432             function() { 
433                 self._fetchLineitem(liId, function(li){self._drawInfo(li);}); 
434             } 
435         );
436     };
437
438     this._fetchLineitem = function(liId, handler) {
439
440         var li = this.liCache[liId];
441         if(li && li.marc() && li.lineitem_details())
442             return handler(li);
443         
444         fieldmapper.standardRequest(
445             ['open-ils.acq', 'open-ils.acq.lineitem.retrieve'],
446             {   async: true,
447
448                 params: [self.authtoken, liId, {
449                     flesh_attrs: true,
450                     flesh_li_details: true,
451                     flesh_fund_debit: true }],
452
453                 oncomplete: function(r) {
454                     var li = openils.Util.readResponse(r);
455                     handler(li)
456                 }
457             }
458         );
459     };
460
461     this._drawInfo = function(li) {
462
463         acqLitEditOrderMarc.onClick = function() { self.editOrderMarc(li); }
464
465         if(li.eg_bib_id()) {
466             openils.Util.hide('acq-lit-marc-order-record-label');
467             openils.Util.hide(acqLitEditOrderMarc.domNode);
468             openils.Util.show('acq-lit-marc-real-record-label');
469         } else {
470             openils.Util.show('acq-lit-marc-order-record-label');
471             openils.Util.show(acqLitEditOrderMarc.domNode);
472             openils.Util.hide('acq-lit-marc-real-record-label');
473         }
474
475         this.drawMarcHTML(li);
476         this.infoTbody = dojo.byId('acq-lit-info-tbody');
477
478         if(!this.infoRow)
479             this.infoRow = this.infoTbody.removeChild(dojo.byId('acq-lit-info-row'));
480         while(this.infoTbody.childNodes[0])
481             this.infoTbody.removeChild(this.infoTbody.childNodes[0]);
482
483         for(var i = 0; i < li.attributes().length; i++) {
484             var attr = li.attributes()[i];
485             var row = this.infoRow.cloneNode(true);
486
487             var type = attr.attr_type().replace(/lineitem_(.*)_attr_definition/, '$1');
488             var name = openils.acq.Lineitem.attrDefs[type].filter(
489                 function(a) {
490                     return (a.code() == attr.attr_name());
491                 }
492             ).pop().description();
493
494             dojo.query('[name=label]', row)[0].appendChild(document.createTextNode(name));
495             dojo.query('[name=value]', row)[0].appendChild(document.createTextNode(attr.attr_value()));
496             this.infoTbody.appendChild(row);
497         }
498
499         if(li.eg_bib_id()) {
500             openils.Util.show('acq-lit-info-cat-link');
501             var link = dojo.byId('acq-lit-info-cat-link').getElementsByTagName('a')[0];
502
503             if(openils.XUL.isXUL()) {
504
505                 var makeRecTab = function() {
506                                     xulG.new_tab(
507                         XUL_OPAC_WRAPPER,
508                                             {tab_name: localeStrings.XUL_RECORD_DETAIL_PAGE, browser:false},
509                                             {
510                             no_xulG : false, 
511                             show_nav_buttons : true, 
512                             show_print_button : true, 
513                             opac_url : xulG.url_prefix(xulG.urls.opac_rdetail + '?r=' + li.eg_bib_id())
514                         }
515                     );
516                 }
517                 link.setAttribute('href', 'javascript:void(0);');
518                 link.onclick = makeRecTab;
519
520             } else {
521                 var href = link.getAttribute('href');
522                 if(href.match(/=$/))
523                     link.setAttribute('href',  href + li.eg_bib_id());
524             }
525         } else {
526             openils.Util.hide('acq-lit-info-cat-link');
527         }
528     };
529
530     this.drawMarcHTML = function(li) {
531         var params = [null, true, li.marc()];
532         if(li.eg_bib_id()) 
533             params = [li.eg_bib_id(), true];
534
535         fieldmapper.standardRequest(
536             ['open-ils.search', 'open-ils.search.biblio.record.html'],
537             {   async: true,
538                 params: params,
539                 oncomplete: function(r) {
540                     dojo.byId('acq-lit-marc-div').innerHTML = 
541                         openils.Util.readResponse(r);
542                 }
543             }
544         );
545     }
546
547     this.drawCopies = function(liId) {
548         this.show('copies');
549         var self = this;
550         this.copyCache = {};
551         this.copyWidgetCache = {};
552         this.oldCopyWidgetCache = {};
553         this.dfaCache = [];
554         this.dfeOffset = 0;
555
556         acqLitSaveCopies.onClick = function() { self.saveCopyChanges(liId) };
557         acqLitBatchUpdateCopies.onClick = function() { self.batchCopyUpdate() };
558         acqLitCopyCountInput.attr('value', '0');
559
560         while(this.copyTbody.childNodes[0])
561             this.copyTbody.removeChild(this.copyTbody.childNodes[0]);
562
563         this._drawBatchCopyWidgets();
564
565         this._fetchDistribFormulas(
566             function() {
567                 openils.acq.Lineitem.fetchAttrDefs(
568                     function() { 
569                         self._fetchLineitem(liId, function(li){self._drawCopies(li);}); 
570                     } 
571                 );
572             }
573         );
574     };
575
576     /**
577      * Insert a new row into the distribution formula selection form
578      */
579     this._addDistribFormulaRow = function() {
580         var self = this;
581
582         if(!self.distribFormulaStore) {
583             // no formulas, hide the form
584             openils.Util.hide('acq-lit-distrib-formula-tbody');
585             return;
586         }
587
588         if(!this.distribFormulaTemplate) 
589             this.distribFormulaTemplate = 
590                 dojo.byId('acq-lit-distrib-formula-tbody').removeChild(dojo.byId('acq-lit-distrib-form-row'));
591
592         var row = dojo.byId('acq-lit-distrib-formula-tbody').appendChild(this.distribFormulaTemplate.cloneNode(true));
593
594         var selector = new dijit.form.FilteringSelect(
595             {store : self.distribFormulaStore}, 
596             nodeByName('selector', row)
597         );
598
599         var apply = new dijit.form.Button(
600             {"label": localeStrings.APPLY},
601             nodeByName('set_button', row)
602         ); 
603
604         var reset = new dijit.form.Button(
605             {"label": localeStrings.RESET_FORMULAE, "disabled": true},
606             nodeByName("reset_button", row)  
607         );
608
609         dojo.connect(apply, 'onClick', 
610             function() {
611                 var form_id = selector.attr('value');
612                 if(!form_id) return;
613                 self._applyDistribFormula(form_id);
614                 reset.attr("disabled", false);
615             }
616         );
617
618         dojo.connect(reset, 'onClick', 
619             function() {
620                 self.restoreCopyFieldsBeforeDF();
621                 self.dfaCache = [];
622                 self.dfeOffset = 0;
623                 reset.attr("disabled", "true");
624             }
625         );
626
627     };
628
629     /**
630      * Applies a distrib formula to the current set of copies
631      */
632     this._applyDistribFormula = function(formula) {
633         if(!formula) return;
634
635         formula = this.distribForms.filter(
636             function(form) { return form.id() == formula; }
637         )[0];
638
639         var copyRows = dojo.query('tr', self.copyTbody);
640
641         if (this.dfeOffset >= copyRows.length) {
642             alert(localeStrings.OUT_OF_COPIES);
643             return;
644         }
645
646         var entries_applied = 0;
647         for(
648             var rowIndex = this.dfeOffset;
649             rowIndex < copyRows.length;
650             rowIndex++
651         ) {
652             
653             var row = copyRows[rowIndex];
654             var copy_id = row.getAttribute('copy_id');
655             var copyWidgets = this.copyWidgetCache[copy_id];
656             var entryIndex = this.dfeOffset;
657             var entry = null;
658
659             // find the correct entry for the current row
660             dojo.forEach(formula.entries(), 
661                 function(e) {
662                     if(!entry) {
663                         entryIndex += e.item_count();
664                         if(entryIndex > rowIndex)
665                             entry = e;
666                     }
667                 }
668             );
669
670             if(entry) {
671                 
672                 //console.log("rowIndex = " + rowIndex + ", entry = " + entry.id() + ", entryIndex=" + 
673                 //  entryIndex + ", owning_lib = " + entry.owning_lib() + ", location = " + entry.location());
674     
675                 entries_applied++;
676                 this.saveCopyFieldsBeforeDF(copy_id);
677                 this._copy_fields_for_acqdf.forEach(
678                     function(field) {
679                         if(entry[field]()) {
680                             copyWidgets[field].attr('value', (entry[field]()));
681                         }
682                     }
683                 );
684             }
685         }
686
687         if (entries_applied) {
688             this.dfaCache.push(formula.id());
689             this.dfeOffset += entries_applied;
690         };
691     };
692
693     this.saveCopyFieldsBeforeDF = function(copy_id) {
694         var self = this;
695         if (!this.oldCopyWidgetCache[copy_id]) {
696             var copyWidgets = this.copyWidgetCache[copy_id];
697
698             this.oldCopyWidgetCache[copy_id] = {};
699             this._copy_fields_for_acqdf.forEach(
700                 function(f) {
701                     self.oldCopyWidgetCache[copy_id][f] =
702                         copyWidgets[f].attr("value");
703                 }
704             );
705         }
706     };
707
708     this.restoreCopyFieldsBeforeDF = function() {
709         var self = this;
710         for (var copy_id in this.oldCopyWidgetCache) {
711             this._copy_fields_for_acqdf.forEach(
712                 function(f) {
713                     self.copyWidgetCache[copy_id][f].attr(
714                         "value", self.oldCopyWidgetCache[copy_id][f]
715                     );
716                 }
717             );
718         }
719     };
720
721     this._fetchDistribFormulas = function(onload) {
722         if(this.distribForms) {
723             onload();
724         } else {
725             var self = this;
726             fieldmapper.standardRequest(
727                 ['open-ils.acq', 'open-ils.acq.distribution_formula.ranged.retrieve.atomic'],
728                 {   async: true,
729                     params: [openils.User.authtoken],
730                     oncomplete: function(r) {
731                         self.distribForms = openils.Util.readResponse(r);
732                         if(!self.distribForms || self.distribForms.length == 0) {
733                             self.distribForms = [];
734                         } else {
735                             self.distribFormulaStore =
736                                 new dojo.data.ItemFileReadStore(
737                                     {data:acqdf.toStoreData(self.distribForms)}
738                                 );
739                         }
740                         self._addDistribFormulaRow();
741                         onload();
742                     }
743                 }
744             );
745         }
746     }
747
748     this._drawBatchCopyWidgets = function() {
749         var row = this.copyBatchRow;
750         dojo.forEach(liDetailBatchFields, 
751             function(field) {
752                 if(self.copyBatchRowDrawn) {
753                     self.copyBatchWidgets[field].attr('value', null);
754                 } else {
755                     var widget = new openils.widget.AutoFieldWidget({
756                         fmField : field,
757                         fmClass : 'acqlid',
758                         parentNode : dojo.query('[name='+field+']', row)[0],
759                         orgLimitPerms : ['CREATE_PICKLIST'],
760                         dijitArgs : {required:false},
761                         forceSync : true
762                     });
763                     widget.build(
764                         function(w, ww) {
765                             self.copyBatchWidgets[field] = w;
766                         }
767                     );
768                 }
769             }
770         );
771         this.copyBatchRowDrawn = true;
772     };
773
774     this.batchCopyUpdate = function() {
775         var self = this;
776         for(var k in this.copyWidgetCache) {
777             var cache = this.copyWidgetCache[k];
778             dojo.forEach(liDetailBatchFields, function(f) {
779                 var newval = self.copyBatchWidgets[f].attr('value');
780                 if(newval) cache[f].attr('value', newval);
781             });
782         }
783     };
784
785     this._drawCopies = function(li) {
786         var self = this;
787
788         // this button sets the total number of copies for a given lineitem
789         acqLitAddCopyCount.onClick = function() { 
790             var count = acqLitCopyCountInput.attr('value');
791
792             // add new rows
793             while(self.copyCount() < count)
794                 self.addCopy(li); 
795             
796             // delete rows if necessary
797             var diff = self.copyCount() - count;
798             if(diff > 0) {
799                 var rows = dojo.query('tr', self.copyTbody).reverse().slice(0, diff);
800                 if(confirm(dojo.string.substitute(localeStrings.DELETE_LI_COPIES_CONFIRM, [diff]))) {
801                     dojo.forEach(rows, function(row) {self.deleteCopy(row); });
802                 } else {
803                     acqLitCopyCountInput.attr('value', self.copyCount()+'');
804                 }
805             }
806         }
807
808
809         if(li.lineitem_details().length > 0) {
810             dojo.forEach(li.lineitem_details(),
811                 function(copy) {
812                     self.addCopy(li, copy);
813                 }
814             );
815         } else {
816             self.addCopy(li);
817         }
818     };
819
820     this.copyCount = function() {
821         var count = 0;
822         for(var id in this.copyCache) {
823             if(!this.copyCache[id].isdeleted())
824                 count++;
825         }
826         return count;
827     }
828
829     this.virtCopyId = -1;
830     this.addCopy = function(li, copy) {
831         var row = this.copyRow.cloneNode(true);
832         this.copyTbody.appendChild(row);
833         var self = this;
834
835         if(!copy) {
836             copy = new fieldmapper.acqlid();
837             copy.isnew(true);
838             copy.id(this.virtCopyId--);
839             copy.lineitem(li.id());
840         }
841
842         this.copyCache[copy.id()] = copy;
843         row.setAttribute('copy_id', copy.id());
844         self.copyWidgetCache[copy.id()] = {};
845
846         acqLitCopyCountInput.attr('value', self.copyCount()+'');
847
848         dojo.forEach(liDetailFields,
849             function(field) {
850                 var widget = new openils.widget.AutoFieldWidget({
851                     fmObject : copy,
852                     fmField : field,
853                     fmClass : 'acqlid',
854                     parentNode : dojo.query('[name='+field+']', row)[0],
855                     orgLimitPerms : ['CREATE_PICKLIST', 'CREATE_PURCHASE_ORDER'],
856                     readOnly : Boolean(copy.eg_copy_id())
857                 });
858                 widget.build(
859                     // make sure we capture the value from any async widgets
860                     function(w, ww) { 
861                         copy[field](ww.getFormattedValue()) 
862                         self.copyWidgetCache[copy.id()][field] = w;
863                     }
864                 );
865                 dojo.connect(widget.widget, 'onChange', 
866                     function(val) { 
867                         if(copy.isnew() || val != copy[field]()) {
868                             // prevent setting ischanged() automatically on widget load for existing copies
869                             copy[field](widget.getFormattedValue()) 
870                             copy.ischanged(true);
871                         }
872                     }
873                 );
874             }
875         );
876
877         var recv_link = dojo.query('[name=receive]', row)[0];
878         if(copy.recv_time()) {
879             openils.Util.hide(recv_link);
880         } else {
881             recv_link.onclick = function() {
882                 self.receiveLid(copy);
883                 openils.Util.hide(recv_link);
884             }
885         }
886
887         if(this.isPO) {
888             openils.Util.hide(dojo.query('[name=delete]', row)[0].parentNode);
889         } else {
890             dojo.query('[name=delete]', row)[0].onclick = 
891                 function() { self.deleteCopy(row) };
892         }
893     };
894
895     this.deleteCopy = function(row) {
896         var copy = this.copyCache[row.getAttribute('copy_id')];
897         copy.isdeleted(true);
898         if(copy.isnew())
899             delete this.copyCache[copy.id()];
900         this.copyTbody.removeChild(row);
901     }
902
903     this.saveCopyChanges = function(liId) {
904         var self = this;
905         var copies = [];
906
907
908         var total = 0;
909         for(var id in this.copyCache) {
910             var c = this.copyCache[id];
911             if(!c.isdeleted()) total++;
912             if(c.isnew() || c.ischanged() || c.isdeleted()) {
913                 if(c.id() < 0) c.id(null);
914                 copies.push(c);
915             }
916         }
917
918         if (typeof(this._copy_count_cb) == "function") {
919             this._copy_count_cb(liId, total);
920         }
921
922         dojo.byId('acq-lit-copy-count-label-' + liId).innerHTML = total;
923
924
925         if (copies.length > 0) {
926             openils.Util.show("acq-lit-update-copies-progress");
927             fieldmapper.standardRequest(
928                 ['open-ils.acq', 'open-ils.acq.lineitem_detail.cud.batch'],
929                 {   async: true,
930                     params: [openils.User.authtoken, copies],
931                     onresponse: function(r) {
932                         var res = openils.Util.readResponse(r);
933                         litUpdateCopiesProgress.update(res);
934                     },
935                     oncomplete: function() {
936                         self.drawCopies(liId);
937                         openils.Util.hide("acq-lit-update-copies-progress");
938                     }
939                 }
940             );
941         }
942
943         if (this.dfaCache.length > 0) {
944             var oldlength = this.dfaCache.length;
945
946             fieldmapper.standardRequest(
947                 ["open-ils.acq",
948                 "open-ils.acq.distribution_formula.record_application"],
949                 {
950                     "async": true,
951                     "params": [openils.User.authtoken, this.dfaCache, liId],
952                     "onresponse": function(r) {
953                         var res = openils.Util.readResponse(r);
954                         if (res && res.length != oldlength)
955                             alert(localeStrings.DFA_NOT_ALL);
956                     }
957                 }
958             );
959             this.dfaCache = [];
960         }
961     }
962
963     this.applySelectedLiAction = function(action) {
964         var self = this;
965         switch(action) {
966
967             case 'delete_selected':
968                 this._deleteLiList(self.getSelected());
969                 break;
970
971             case 'create_order':
972
973                 if(!this.createPoProviderSelector) {
974                     var widget = new openils.widget.AutoFieldWidget({
975                         fmField : 'provider',
976                         fmClass : 'acqpo',
977                         searchFilter: {"active": "t"},
978                         parentNode : dojo.byId('acq-lit-po-provider'),
979                     });
980                     widget.build(
981                         function(w) { self.createPoProviderSelector = w; }
982                     );
983                 }
984
985                 if(!this.createPoAgencySelector) {
986                     var widget = new openils.widget.AutoFieldWidget({
987                         fmField : 'ordering_agency',
988                         fmClass : 'acqpo',
989                         parentNode : dojo.byId('acq-lit-po-agency'),
990                         orgLimitPerms : ['CREATE_PURCHASE_ORDER'],
991                     });
992                     widget.build(
993                         function(w) { self.createPoAgencySelector = w; }
994                     );
995                 }
996
997          
998                 acqLitPoCreateDialog.show();
999                 break;
1000
1001             case 'save_picklist':
1002                 this._loadPLSelect();
1003                 acqLitSavePlDialog.show();
1004                 break;
1005
1006             case 'selector_ready':
1007             case 'order_ready':
1008                 acqLitChangeLiStateDialog.attr('state', action.replace('_', '-'));
1009                 acqLitChangeLiStateDialog.show();
1010                 break;
1011
1012             case 'print_po':
1013                 this.printPO();
1014                 break;
1015
1016             case 'receive_po':
1017                 this.receivePO();
1018                 break;
1019
1020             case 'rollback_receive_po':
1021                 this.rollbackPoReceive();
1022                 break;
1023
1024             case 'create_assets':
1025                 this.createAssets();
1026                 break;
1027
1028             case 'export_attr_list':
1029                 this.chooseExportAttr();
1030                 break;
1031
1032             case 'add_brief_record':
1033                 if(this.isPO)
1034                     location.href = oilsBasePath + '/acq/picklist/brief_record?po=' + this.isPO;
1035                 else
1036                     location.href = oilsBasePath + '/acq/picklist/brief_record?pl=' + this.isPL;
1037         }
1038     }
1039
1040     this.createAssets = function() {
1041         if(!this.isPO) return;
1042         if(!confirm(localeStrings.CREATE_PO_ASSETS_CONFIRM)) return;
1043         this.show('acq-lit-progress-numbers');
1044         var self = this;
1045         fieldmapper.standardRequest(
1046             ['open-ils.acq', 'open-ils.acq.purchase_order.assets.create'],
1047             {   async: true,
1048                 params: [this.authtoken, this.isPO],
1049                 onresponse: function(r) {
1050                     var resp = openils.Util.readResponse(r);
1051                     self._updateProgressNumbers(resp, true);
1052                 }
1053             }
1054         );
1055     }
1056
1057     this.chooseExportAttr = function() {
1058         if (!acqLitExportAttrSelector._li_setup) {
1059             var self = this;
1060             acqLitExportAttrSelector.store = new dojo.data.ItemFileReadStore(
1061                 {
1062                     "data": acqliad.toStoreData(
1063                         (new openils.PermaCrud()).search(
1064                             "acqliad", {"code": li_exportable_attrs}
1065                         )
1066                     )
1067                 }
1068             );
1069             acqLitExportAttrSelector.setValue();
1070             acqLitExportAttrButton.onClick = function(){self.exportAttrList();};
1071             acqLitExportAttrSelector._li_setup = true;
1072         }
1073         openils.Util.show("acq-lit-export-attr-holder", "inline");
1074     };
1075
1076     this.exportAttrList = function() {
1077         var attr_def = acqLitExportAttrSelector.item;
1078         var li_list = this.getSelected();
1079         var value_list = li_list.map(
1080             function(li) {
1081                 return (new openils.acq.Lineitem({"lineitem": li})).findAttr(
1082                     attr_def.code, "lineitem_marc_attr_definition"
1083                 );
1084             }
1085         ).filter(function(attr) { return Boolean(attr); });
1086
1087         if (value_list.length > 0) {
1088             if (value_list.length < li_list.length) {
1089                 if (!confirm(
1090                     dojo.string.substitute(
1091                         localeStrings.EXPORT_SHORT_LIST, [attr_def.description]
1092                     )
1093                 )) {
1094                     return;
1095                 }
1096             }
1097             try {
1098                 openils.XUL.contentToFileSaveDialog(
1099                     value_list.join("\n"),
1100                     localeStrings.EXPORT_SAVE_DIALOG_TITLE
1101                 );
1102             } catch (E) {
1103                 alert(E);
1104             }
1105         } else {
1106             alert(dojo.string.substitute(
1107                 localeStrings.EXPORT_EMPTY_LIST, [attr_def.description]
1108             ));
1109         }
1110
1111         openils.Util.hide("acq-lit-export-attr-holder");
1112     };
1113
1114     this.printPO = function() {
1115         if(!this.isPO) return;
1116         progressDialog.show(true);
1117         fieldmapper.standardRequest(
1118             ['open-ils.acq', 'open-ils.acq.purchase_order.format'],
1119             {   async: true,
1120                 params: [this.authtoken, this.isPO, 'html'],
1121                 oncomplete: function(r) {
1122                     progressDialog.hide();
1123                     var evt = openils.Util.readResponse(r);
1124                     if(evt && evt.template_output()) {
1125                         win = window.open('','', 'resizable,width=800,height=600,scrollbars=1');
1126                         win.document.body.innerHTML = evt.template_output().data();
1127                     }
1128                 }
1129             }
1130         );
1131     }
1132
1133
1134     this.receivePO = function() {
1135         if(!this.isPO) return;
1136         this.show('acq-lit-progress-numbers');
1137         var self = this;
1138         fieldmapper.standardRequest(
1139             ['open-ils.acq', 'open-ils.acq.purchase_order.receive'],
1140             {   async: true,
1141                 params: [this.authtoken, this.isPO],
1142                 onresponse : function(r) {
1143                     var resp = openils.Util.readResponse(r);
1144                     self._updateProgressNumbers(resp, true);
1145                 },
1146             }
1147         );
1148     }
1149
1150     this.receiveLi = function(li) {
1151         if(!this.isPO) return;
1152         progressDialog.show(true);
1153         fieldmapper.standardRequest(
1154             ['open-ils.acq', 'open-ils.acq.lineitem.receive'],
1155             {   async: true,
1156                 params: [this.authtoken, li.id()],
1157                 onresponse : function(r) {
1158                     var resp = openils.Util.readResponse(r);
1159                     progressDialog.hide();
1160                 },
1161             }
1162         );
1163     }
1164
1165     this.receiveLid = function(li) {
1166         if(!this.isPO) return;
1167         progressDialog.show(true);
1168         fieldmapper.standardRequest(
1169             ['open-ils.acq', 'open-ils.acq.lineitem_detail.receive'],
1170             {   async: true,
1171                 params: [this.authtoken, li.id()],
1172                 onresponse : function(r) {
1173                     var resp = openils.Util.readResponse(r);
1174                     progressDialog.hide();
1175                 },
1176             }
1177         );
1178     }
1179
1180     this.rollbackPoReceive = function() {
1181         if(!this.isPO) return;
1182         if(!confirm(localeStrings.ROLLBACK_PO_RECEIVE_CONFIRM)) return;
1183         this.show('acq-lit-progress-numbers');
1184         var self = this;
1185         fieldmapper.standardRequest(
1186             ['open-ils.acq', 'open-ils.acq.purchase_order.receive.rollback'],
1187             {   async: true,
1188                 params: [this.authtoken, this.isPO],
1189                 onresponse : function(r) {
1190                     var resp = openils.Util.readResponse(r);
1191                     self._updateProgressNumbers(resp, true);
1192                 },
1193             }
1194         );
1195     }
1196
1197     this._updateProgressNumbers = function(resp, reloadOnComplete) {
1198         if(!resp) return;
1199         dojo.byId('acq-pl-lit-li-processed').innerHTML = resp.li;
1200         dojo.byId('acq-pl-lit-lid-processed').innerHTML = resp.lid;
1201         dojo.byId('acq-pl-lit-debits-processed').innerHTML = resp.debits_accrued;
1202         dojo.byId('acq-pl-lit-bibs-processed').innerHTML = resp.bibs;
1203         dojo.byId('acq-pl-lit-indexed-processed').innerHTML = resp.indexed;
1204         dojo.byId('acq-pl-lit-copies-processed').innerHTML = resp.copies;
1205         if(resp.complete && reloadOnComplete) 
1206             location.href = location.href;
1207     }
1208
1209
1210     this._createPO = function(fields) {
1211         this.show('acq-lit-progress-numbers');
1212         var po = new fieldmapper.acqpo();
1213         po.provider(this.createPoProviderSelector.attr('value'));
1214         po.ordering_agency(this.createPoAgencySelector.attr('value'));
1215
1216         var selected = this.getSelected( (fields.create_from == 'all') );
1217         if(selected.length == 0) return;
1218
1219         var max = selected.length * 3;
1220
1221         var self = this;
1222         fieldmapper.standardRequest(
1223             ['open-ils.acq', 'open-ils.acq.purchase_order.create'],
1224             {   async: true,
1225                 params: [
1226                     openils.User.authtoken, 
1227                     po, 
1228                     {
1229                         lineitems : selected.map(function(li) { return li.id() }),
1230                         create_assets : fields.create_assets[0],
1231                     }
1232                 ],
1233
1234                 onresponse : function(r) {
1235                     var resp = openils.Util.readResponse(r);
1236                     self._updateProgressNumbers(resp);
1237                     if(resp.complete) 
1238                         location.href = oilsBasePath + '/eg/acq/po/view/' + resp.purchase_order.id();
1239                 }
1240             }
1241         );
1242     }
1243
1244     this._deleteLiList = function(list, idx) {
1245         if(idx == null) idx = 0;
1246         if(idx >= list.length) return;
1247         var liId = list[idx].id();
1248         fieldmapper.standardRequest(
1249             ['open-ils.acq', 'open-ils.acq.lineitem.delete'],
1250             {   async: true,
1251                 params: [openils.User.authtoken, liId],
1252                 oncomplete: function(r) {
1253                     self.removeLineitem(liId);
1254                     self._deleteLiList(list, ++idx);
1255                 }
1256             }
1257         );
1258     }
1259
1260     this.editOrderMarc = function(li) {
1261
1262         /*  To run in Firefox directly, must set signed.applets.codebase_principal_support
1263             to true in about:config */
1264
1265         if(!openils.XUL.enableXPConnect()) return;
1266
1267         if(openils.XUL.isXUL()) {
1268             win = window.open('/xul/' + openils.XUL.buildId() + '/server/cat/marcedit.xul');
1269         } else {
1270             win = window.open('/xul/server/cat/marcedit.xul'); 
1271         }
1272         var self = this;
1273         win.xulG = {
1274             record : {marc : li.marc()},
1275             save : {
1276                 label: 'Save Record', // XXX I18N
1277                 func: function(xmlString) {
1278                     li.marc(xmlString);
1279                     fieldmapper.standardRequest(
1280                         ['open-ils.acq', 'open-ils.acq.lineitem.update'],
1281                         {   async: true,
1282                             params: [openils.User.authtoken, li],
1283                             oncomplete: function(r) {
1284                                 openils.Util.readResponse(r);
1285                                 win.close();
1286                                 self.drawInfo(li.id())
1287                             }
1288                         }
1289                     );
1290                 },
1291             }
1292         };
1293     }
1294
1295     this._savePl = function(values) {
1296         var self = this;
1297         var selected = this.getSelected( (values.which == 'all') );
1298         openils.Util.show('acq-lit-generic-progress');
1299
1300         if(values.new_name) {
1301             openils.acq.Picklist.create(
1302                 {name: values.new_name}, 
1303                 function(id) {
1304                     self._updateLiList(id, selected, 0, 
1305                         function(){
1306                             location.href = oilsBasePath + '/eg/acq/picklist/view/' + id;
1307                         });
1308                 }
1309             );
1310         } else if(values.existing_pl) {
1311             // update lineitems to use an existing picklist
1312             self._updateLiList(values.existing_pl, selected, 0, 
1313                 function(){
1314                     location.href = oilsBasePath + '/eg/acq/picklist/view/' + values.existing_pl;
1315                 });
1316         }
1317     }
1318
1319     this._updateLiState = function(values, state) {
1320         var self = this;
1321         var selected = this.getSelected( (values.which == 'all') );
1322         if(!selected.length) return;
1323         dojo.forEach(selected, function(li) {li.state(state);});
1324         self._updateLiList(null, selected, 0, 
1325             // TODO consider inline updates for efficiency
1326             function() { location.href = location.href }
1327         );
1328     }
1329
1330     this._updateLiList = function(pl, list, idx, oncomplete) {
1331         if(idx >= list.length) return oncomplete();
1332         var li = list[idx];
1333         if(pl != null) li.picklist(pl);
1334         litGenericProgress.update({maximum: list.length, progress: idx});
1335         new openils.acq.Lineitem({lineitem:li}).update(
1336             function(r) {
1337                 self._updateLiList(pl, list, ++idx, oncomplete);
1338             }
1339         );
1340     }
1341
1342     this._loadPLSelect = function() {
1343         if(this._plSelectLoaded) return;
1344         var plList = [];
1345         function handleResponse(r) {
1346             plList.push(r.recv().content());
1347         }
1348         var method = 'open-ils.acq.picklist.user.retrieve';
1349         fieldmapper.standardRequest(
1350             ['open-ils.acq', method],
1351             {   async: true,
1352                 params: [this.authtoken],
1353                 onresponse: handleResponse,
1354                 oncomplete: function() {
1355                     self._plSelectLoaded = true;
1356                     acqLitAddExistingSelect.store = 
1357                         new dojo.data.ItemFileReadStore({data:acqpl.toStoreData(plList)});
1358                     acqLitAddExistingSelect.setValue();
1359                 }
1360             }
1361         );
1362     }
1363
1364     // grab the li-details for this lineitem, grab the linked copies and volumes, add them to the table
1365     this.showRealCopies = function(li) {
1366         while(this.realCopiesTbody.childNodes[0])
1367             this.realCopiesTbody.removeChild(this.realCopiesTbody.childNodes[0]);
1368         this.show('real-copies');
1369
1370         var pcrud = new openils.PermaCrud({authtoken : this.authtoken});
1371         this.realCopyList = [];
1372         this.volCache = {};
1373         var tabIndex = 1000;
1374         var self = this;
1375
1376         acqLitSaveRealCopies.onClick = function() {
1377             self.saveRealCopies();
1378         }
1379
1380         this._fetchLineitem(li.id(), 
1381             function(fullLi) {
1382                 li = self.liCache[li.id()] = fullLi;
1383
1384                 pcrud.search(
1385                     'acp', {
1386                         id : li.lineitem_details().map(
1387                             function(item) { return item.eg_copy_id() }
1388                         )
1389                     }, {
1390                         async : true,
1391                         streaming : true,
1392                         onresponse : function(r) {
1393                             var copy = openils.Util.readResponse(r);
1394                             var volId = copy.call_number();
1395                             var volume = self.volCache[volId];
1396                             if(!volume) {
1397                                 volume = self.volCache[volId] = pcrud.retrieve('acn', volId);
1398                             }
1399                             self.addRealCopy(volume, copy, tabIndex++);
1400                         }
1401                     }
1402                 );
1403             }
1404         );
1405     }
1406
1407     this.addRealCopy = function(volume, copy, tabIndex) {
1408         var row = this.realCopiesRow.cloneNode(true);
1409         this.realCopyList.push(copy);
1410
1411         var selectNode;
1412         dojo.forEach(
1413             ['owning_lib', 'location', 'circ_modifier', 'label', 'barcode'],
1414
1415             function(field) {
1416                 var isvol = (field == 'owning_lib' || field == 'label');
1417                 var widget = new openils.widget.AutoFieldWidget({
1418                     fmField : field,
1419                     fmObject : isvol ? volume : copy,
1420                     parentNode : nodeByName(field, row),
1421                     readOnly : (field != 'barcode'),
1422                 });
1423
1424                 var widgetDrawn = null;
1425
1426                 if(field == 'barcode') {
1427
1428                     widgetDrawn = function(w, ww) {
1429                         var node = w.domNode;
1430                         node.setAttribute('tabindex', ''+tabIndex);
1431
1432                         // on enter, select the next barcode input
1433                         dojo.connect(w, 'onKeyDown',
1434                             function(e) {
1435                                 if(e.keyCode == dojo.keys.ENTER) {
1436                                     var ti = node.getAttribute('tabindex');
1437                                     var nextNode = dojo.query('[tabindex=' + String(Number(ti) + 1) + ']', self.realCopiesTbody)[0];
1438                                     if(nextNode) nextNode.select();
1439                                 }
1440                             }
1441                         );
1442
1443                         dojo.connect(w, 'onChange', 
1444                             function(val) { 
1445                                 if(!val || val == copy.barcode()) return;
1446                                 copy.ischanged(true);
1447                                 copy.barcode(val);
1448                             }
1449                         );
1450
1451
1452                         if(self.realCopiesTbody.getElementsByTagName('TR').length == 0)
1453                             selectNode = node;
1454                     }
1455                 }
1456
1457                 widget.build(widgetDrawn);
1458             }
1459         );
1460
1461         this.realCopiesTbody.appendChild(row);
1462         if(selectNode) selectNode.select();
1463     };
1464
1465     this.saveRealCopies = function() {
1466         var pcrud = new openils.PermaCrud({authtoken : this.authtoken});
1467         progressDialog.show(true);
1468         var list = this.realCopyList.filter(function(copy) { return copy.ischanged(); });
1469         pcrud.update(list, {oncomplete: function() { 
1470             progressDialog.hide();
1471             self.show('list');
1472         }});
1473     }
1474 }