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