From: Tiffany Little Date: Fri, 28 May 2021 15:29:53 +0000 (-0400) Subject: LP1929749 ACQ Load MARC Order Records port X-Git-Url: https://old-git.evergreen-ils.org/?a=commitdiff_plain;h=aef28ecab96c771261d69677f3a4d4334b19bcdf;p=working%2FEvergreen.git LP1929749 ACQ Load MARC Order Records port [Note that this includes some work by Galen Charlton to reconcile the picklist module with work done by Bill Erickson for LP#1929741] Signed-off-by: Tiffany Little Signed-off-by: Galen Charlton Signed-off-by: Ruth Frasur Signed-off-by: Bill Erickson Signed-off-by: Jane Sandberg --- diff --git a/Open-ILS/src/eg2/src/app/staff/acq/picklist/picklist.module.ts b/Open-ILS/src/eg2/src/app/staff/acq/picklist/picklist.module.ts index f39c96f313..91ee36c819 100644 --- a/Open-ILS/src/eg2/src/app/staff/acq/picklist/picklist.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/acq/picklist/picklist.module.ts @@ -6,18 +6,22 @@ import {HoldingsModule} from '@eg/staff/share/holdings/holdings.module'; import {PicklistRoutingModule} from './routing.module'; import {PicklistComponent} from './picklist.component'; import {PicklistSummaryComponent} from './summary.component'; +import {HttpClientModule} from '@angular/common/http'; +import {UploadComponent} from './upload.component'; @NgModule({ declarations: [ PicklistComponent, - PicklistSummaryComponent + PicklistSummaryComponent, + UploadComponent ], imports: [ StaffCommonModule, CatalogCommonModule, LineitemModule, HoldingsModule, - PicklistRoutingModule + PicklistRoutingModule, + HttpClientModule ], providers: [] }) diff --git a/Open-ILS/src/eg2/src/app/staff/acq/picklist/routing.module.ts b/Open-ILS/src/eg2/src/app/staff/acq/picklist/routing.module.ts index d943cab01c..1d304cbaca 100644 --- a/Open-ILS/src/eg2/src/app/staff/acq/picklist/routing.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/acq/picklist/routing.module.ts @@ -8,11 +8,17 @@ import {LineitemCopiesComponent} from '../lineitem/copies.component'; import {LineitemWorksheetComponent} from '../lineitem/worksheet.component'; import {BriefRecordComponent} from '../lineitem/brief-record.component'; import {LineitemHistoryComponent} from '../lineitem/history.component'; +import {UploadComponent} from './upload.component'; +import {VandelayService} from '@eg/staff/cat/vandelay/vandelay.service'; +import {PicklistUploadService} from './upload.service' const routes: Routes = [{ path: 'brief-record', component: BriefRecordComponent }, { + path: 'upload', + component: UploadComponent +}, { path: ':picklistId', component: PicklistComponent, children : [{ @@ -39,7 +45,7 @@ const routes: Routes = [{ @NgModule({ imports: [RouterModule.forChild(routes)], exports: [RouterModule], - providers: [] + providers: [VandelayService, PicklistUploadService] }) export class PicklistRoutingModule {} diff --git a/Open-ILS/src/eg2/src/app/staff/acq/picklist/upload.component.html b/Open-ILS/src/eg2/src/app/staff/acq/picklist/upload.component.html new file mode 100644 index 0000000000..3feac3fe55 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/picklist/upload.component.html @@ -0,0 +1,305 @@ + + + + + +
+
+ +
+
+ + + + +
+
+
+ +
+
+ + +
+
+ + + +
+
+ +

Purchase Order

+
+
+ +
+ +
+ + +
+ +
+ +
+
+ +
+
+
+
+ +
+
+ + +
+ +
+ +
+ +
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ + +
+
+ + +

Upload Settings

+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+
+ +
+
+ +
+
+
+
+ +
+
+ + +
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +

This Upload

+
+
+ +
+
+ + +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ + Importing {{importSelection().recordIds.length}} Record(s) + + Importing Queue {{importSelection().queue.name()}} +
+
+ +
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+ +
+ +
+
+
+ Queue +
+
+
\ No newline at end of file diff --git a/Open-ILS/src/eg2/src/app/staff/acq/picklist/upload.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/picklist/upload.component.ts new file mode 100644 index 0000000000..9ccd375cab --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/picklist/upload.component.ts @@ -0,0 +1,582 @@ +import {Component, OnInit, AfterViewInit, Input, + ViewChild, OnDestroy} from '@angular/core'; +import {Subject} from 'rxjs'; +import {tap} from 'rxjs/operators'; +import {IdlObject} from '@eg/core/idl.service'; +import {NetService} from '@eg/core/net.service'; +import {EventService} from '@eg/core/event.service'; +import {OrgService} from '@eg/core/org.service'; +import {AuthService} from '@eg/core/auth.service'; +import {ToastService} from '@eg/share/toast/toast.service'; +import {ComboboxComponent, + ComboboxEntry} from '@eg/share/combobox/combobox.component'; +import {VandelayImportSelection, + VANDELAY_UPLOAD_PATH} from '@eg/staff/cat/vandelay/vandelay.service'; +import {HttpClient, HttpRequest, HttpEventType} from '@angular/common/http'; +import {HttpResponse, HttpErrorResponse} from '@angular/common/http'; +import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component'; +import {AlertDialogComponent} from '@eg/share/dialog/alert.component'; +import {ServerStoreService} from '@eg/core/server-store.service'; +import {PicklistUploadService} from './upload.service'; +import {OrgSelectComponent} from '@eg/share/org-select/org-select.component' + + +const TEMPLATE_SETTING_NAME = 'eg.acq.picklist.upload.templates'; + +const TEMPLATE_ATTRS = [ + 'createPurchaseOrder', + 'activatePurchaseOrder', + 'orderingAgency', + 'selectedFiscalYear', + 'loadItems', + 'selectedBibSource', + 'selectedMatchSet', + 'mergeOnExact', + 'importNonMatching', + 'mergeOnBestMatch', + 'mergeOnSingleMatch', + 'selectedMergeProfile', + 'selectedFallThruMergeProfile', + 'minQualityRatio' +]; + +interface ImportOptions { + session_key: string; + overlay_map?: {[qrId: number]: /* breId */ number}; + // import_no_match?: boolean; + // auto_overlay_exact?: boolean; + // auto_overlay_best_match?: boolean; + // auto_overlay_1match?: boolean; + // merge_profile?: any; + // fall_through_merge_profile?: any; +// match_quality_ratio: number; + // match_set: number; + // bib_source: number; + exit_early: boolean; +} + +@Component({ + templateUrl: './upload.component.html' +}) +export class UploadComponent implements OnInit, AfterViewInit, OnDestroy { + + recordType: string; + selectedQueue: ComboboxEntry; // freetext enabled + + // used for applying a default queue ID value when we have + // a load-time queue before the queue combobox entries exist. + startQueueId: number; + + activeQueueId: number; + orderingAgency: number; + selectedFiscalYear: number; + selectedSelectionList: number; + selectedBibSource: number; + selectedProvider: number; + selectedMatchSet: number; + importDefId: number; + selectedMergeProfile: number; + selectedFallThruMergeProfile: number; + selectedFile: File; + + defaultMatchSet: string; + + createPurchaseOrder: boolean; + activatePurchaseOrder: boolean; + loadItems: boolean; + + importNonMatching: boolean; + mergeOnExact: boolean; + mergeOnSingleMatch: boolean; + mergeOnBestMatch: boolean; + minQualityRatio: number; + + // True after the first upload, then remains true. + showProgress: boolean; + + // Upload in progress. + isUploading: boolean; + + // True only after successful upload + uploadComplete: boolean; + + // Upload / processsing session key + // Generated by the server + sessionKey: string; + + + selectedTemplate: string; + formTemplates: {[name: string]: any}; + newTemplateName: string; + + @ViewChild('fileSelector', { static: false }) private fileSelector; + @ViewChild('uploadProgress', { static: true }) + private uploadProgress: ProgressInlineComponent; + + // Need these refs so values can be applied via external stimuli + @ViewChild('formTemplateSelector', { static: true }) + private formTemplateSelector: ComboboxComponent; + @ViewChild('bibSourceSelector', { static: true }) + private bibSourceSelector: ComboboxComponent; + @ViewChild('providerSelector', {static: true}) + private providerSelector: ComboboxComponent; + @ViewChild('fiscalYearSelector', { static: true }) + private fiscalYearSelector: ComboboxComponent; + @ViewChild('selectionListSelector', { static: true }) + private selectionListSelector: ComboboxComponent; + @ViewChild('matchSetSelector', { static: true }) + private matchSetSelector: ComboboxComponent; + @ViewChild('mergeProfileSelector', { static: true }) + private mergeProfileSelector: ComboboxComponent; + @ViewChild('fallThruMergeProfileSelector', { static: true }) + private fallThruMergeProfileSelector: ComboboxComponent; + @ViewChild('dupeQueueAlert', { static: true }) + private dupeQueueAlert: AlertDialogComponent; + + constructor( + private http: HttpClient, + private toast: ToastService, + private evt: EventService, + private net: NetService, + private auth: AuthService, + private org: OrgService, + private store: ServerStoreService, + private vlagent: PicklistUploadService + ) { + this.applyDefaults(); + } + + applyDefaults() { + this.minQualityRatio = 0; + this.selectedBibSource = 1; // default to system local + this.recordType = 'bib'; + this.formTemplates = {}; +//To-do add default for fiscal year + if (this.vlagent.importSelection) { + + if (!this.vlagent.importSelection.queue) { + // Incomplete import selection, clear it. + this.vlagent.importSelection = null; + return; + } + + const queue = this.vlagent.importSelection.queue; + this.recordType = queue.queue_type(); + this.selectedMatchSet = queue.match_set(); + + // This will be propagated to selectedQueue as a combobox + // entry via the combobox + this.startQueueId = queue.id(); + + + } + } + + ngOnInit() {} + + ngAfterViewInit() { + this.loadStartupData(); + } + + ngOnDestroy() { + // Always clear the import selection when navigating away from + // the import page. + this.clearSelection(); + } + + importSelection(): VandelayImportSelection { + return this.vlagent.importSelection; + } + + loadStartupData(): Promise { + + + const promises = [ + this.vlagent.getMergeProfiles(), + this.vlagent.getAllQueues('bib'), + this.vlagent.getMatchSets('bib'), + this.vlagent.getBibSources(), + this.vlagent.getFiscalYears(), + this.vlagent.getProvidersList(), + this.vlagent.getSelectionLists(), + this.vlagent.getItemImportDefs(), + this.org.settings(['vandelay.default_match_set']).then( + s => this.defaultMatchSet = s['vandelay.default_match_set']), + this.loadTemplates() + ]; + + return Promise.all(promises); + } + + orgOnChange(org: IdlObject) { + this.orderingAgency = org.id() + } + + loadTemplates() { + this.store.getItem(TEMPLATE_SETTING_NAME).then( + templates => { + this.formTemplates = templates || {}; + + Object.keys(this.formTemplates).forEach(name => { + if (this.formTemplates[name].default) { + this.selectedTemplate = name; + } + }); + } + ); + } + + formatTemplateEntries(): ComboboxEntry[] { + const entries = []; + + Object.keys(this.formTemplates || {}).forEach( + name => entries.push({id: name, label: name})); + + return entries; + } + + // Format typeahead data sets + formatEntries(etype: string): ComboboxEntry[] { + const rtype = this.recordType; + let list; + + switch (etype) { + case 'bibSources': + return (this.vlagent.bibSources || []).map( + s => { + return {id: s.id(), label: s.source()}; + }); + + case 'providersList': + return (this.vlagent.providersList || []).map( + p => { + return {id: p.id(), label: p.code()}; + }); + + case 'fiscalYears': + return (this.vlagent.fiscalYears || []).map( + fy => { + return {id: fy.id(), label: fy.year()}; + }); + break; + + case 'selectionLists': + list = this.vlagent.selectionLists; + break; + + case 'activeQueues': + list = (this.vlagent.allQueues[rtype] || []) + .filter(q => q.complete() === 'f'); + break; + + case 'matchSets': + list = this.vlagent.matchSets['bib']; + break; + + + case 'importItemDefs': + list = this.vlagent.importItemAttrDefs; + break; + + case 'mergeProfiles': + list = this.vlagent.mergeProfiles; + break; + } + + return (list || []).map(item => { + return {id: item.id(), label: item.name()}; + }); + } + + selectEntry($event: ComboboxEntry, etype: string) { + const id = $event ? $event.id : null; + + switch (etype) { + case 'recordType': + this.recordType = id; + break; + + case 'providersList': + this.selectedProvider = id; + break; + + case 'bibSources': + this.selectedBibSource = id; + break; + + case 'fiscalYears': + this.selectedFiscalYear = id; + break; + + case 'selectionLists': + this.selectedSelectionList = id; + break; + + case 'matchSets': + this.selectedMatchSet = id; + break; + + + case 'mergeProfiles': + this.selectedMergeProfile = id; + break; + + case 'FallThruMergeProfile': + this.selectedFallThruMergeProfile = id; + break; + } + } + + fileSelected($event) { + this.selectedFile = $event.target.files[0]; + } + + // Required form data varies depending on context. + hasNeededData(): boolean { + if (this.vlagent.importSelection) { + return this.importActionSelected(); + } else { + return this.selectedQueue && + Boolean(this.recordType) && Boolean(this.selectedFile); + } + } + + importActionSelected(): boolean { + return this.importNonMatching + || this.mergeOnExact + || this.mergeOnSingleMatch + || this.mergeOnBestMatch; + } + + // 1. create queue if necessary + // 2. upload MARC file + // 3. Enqueue MARC records + // 4. Import records + upload() { + this.sessionKey = null; + this.showProgress = true; + this.isUploading = true; + this.uploadComplete = false; + this.resetProgressBars(); + + this.resolveQueue() + .then( + queueId => { + this.activeQueueId = queueId; + return this.uploadFile(); + }, + err => Promise.reject('queue create failed') + ).then( + ok => this.processUpload(), + err => Promise.reject('process spool failed') + ).then( + ok => { + this.isUploading = false; + this.uploadComplete = true; + }, + err => { + console.log('file upload failed: ', err); + this.isUploading = false; + this.resetProgressBars(); + + } + ); + } + + resetProgressBars() { + this.uploadProgress.update({value: 0, max: 1}); + } + + // Extract selected queue ID or create a new queue when requested. + resolveQueue(): Promise { + + if (this.selectedQueue.freetext) { + // Free text queue selector means create a new entry. + // TODO: first check for name dupes + + return this.vlagent.createQueue( + this.selectedQueue.label, + this.recordType, + this.importDefId, + this.selectedMatchSet, + ).then( + id => id, + err => { + const evt = this.evt.parse(err); + if (evt) { + if (evt.textcode.match(/QUEUE_EXISTS/)) { + this.dupeQueueAlert.open(); + } else { + alert(evt); // server error + } + } + + return Promise.reject('Queue Create Failed'); + } + ); + } else { + return Promise.resolve(this.selectedQueue.id); + } + } + + uploadFile(): Promise { + + if (this.vlagent.importSelection) { + // Nothing to upload when processing pre-queued records. + return Promise.resolve(); + } + + const formData: FormData = new FormData(); + + formData.append('ses', this.auth.token()); + formData.append('marc_upload', + this.selectedFile, this.selectedFile.name); + + if (this.selectedBibSource) { + formData.append('bib_source', '' + this.selectedBibSource); + } + + const req = new HttpRequest('POST', VANDELAY_UPLOAD_PATH, formData, + {reportProgress: true, responseType: 'text'}); + + return this.http.request(req).pipe(tap( + evt => { + if (evt.type === HttpEventType.UploadProgress) { + this.uploadProgress.update( + {value: evt.loaded, max: evt.total}); + + } else if (evt instanceof HttpResponse) { + this.sessionKey = evt.body as string; + console.log( + 'vlagent file uploaded OK with key ' + this.sessionKey); + } + }, + + (err: HttpErrorResponse) => { + console.error(err); + this.toast.danger(err.error); + } + )).toPromise(); + } + + processUpload(): Promise { + + if (this.vlagent.importSelection) { + return Promise.resolve(); + } + + let spoolType = this.recordType; + + const vandelayOptions = { + import_no_match: this.importNonMatching, + auto_overlay_exact: this.mergeOnExact, + auto_overlay_best_match: this.mergeOnBestMatch, + auto_overlay_1match: this.mergeOnSingleMatch, + merge_profile: this.selectedMergeProfile, + fall_through_merge_profile: this.selectedFallThruMergeProfile, + match_quality_ratio: this.minQualityRatio, + bib_source: this.selectedBibSource, + create_assets: this.loadItems, + queue_name: this.selectedQueue.label + + } + + + const args = { + + provider: this.selectedProvider, + ordering_agency: this.orderingAgency, + create_po: this.createPurchaseOrder, + activate_po: this.activatePurchaseOrder, + fiscal_year: this.selectedFiscalYear, + picklist: this.selectedSelectionList, + vandelay: vandelayOptions + } + + + const method = `open-ils.acq.process_upload_records`; + + return new Promise((resolve, reject) => { + this.net.request( + 'open-ils.acq', method, + this.auth.token(), this.sessionKey, args + ).subscribe( + tracker => { + const e = this.evt.parse(tracker); + if (e) { console.error(e); return reject(); } + } + ); + }); + } + + clearSelection() { + this.vlagent.importSelection = null; + this.startQueueId = null; + } + + openQueue() { + console.log('opening queue ' + this.activeQueueId); + } + + saveTemplate() { + + const template = {}; + TEMPLATE_ATTRS.forEach(key => template[key] = this[key]); + + console.debug('Saving import profile', template); + + this.formTemplates[this.selectedTemplate] = template; + return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates); + } + + markTemplateDefault() { + + Object.keys(this.formTemplates).forEach( + name => delete this.formTemplates.default + ); + + this.formTemplates[this.selectedTemplate].default = true; + + return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates); + } + + templateSelectorChange(entry: ComboboxEntry) { + + if (!entry) { + this.selectedTemplate = ''; + return; + } + + this.selectedTemplate = entry.label; // label == name + + if (entry.freetext) { + // User is entering a new template name. + // Nothing to apply. + return; + } + + // User selected an existing template, apply it to the form. + + const template = this.formTemplates[entry.id]; + + // Copy the template values into "this" + TEMPLATE_ATTRS.forEach(key => this[key] = template[key]); + + // Some values must be manually passed to the combobox'es + + this.bibSourceSelector.applyEntryId(this.selectedBibSource); + this.matchSetSelector.applyEntryId(this.selectedMatchSet); + this.providerSelector.applyEntryId(this.selectedProvider); + this.fiscalYearSelector.applyEntryId(this.selectedFiscalYear); + this.mergeProfileSelector.applyEntryId(this.selectedMergeProfile); + this.fallThruMergeProfileSelector + .applyEntryId(this.selectedFallThruMergeProfile); + } + + deleteTemplate() { + delete this.formTemplates[this.selectedTemplate]; + this.formTemplateSelector.selected = null; + return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates); + } +} + diff --git a/Open-ILS/src/eg2/src/app/staff/acq/picklist/upload.service.ts b/Open-ILS/src/eg2/src/app/staff/acq/picklist/upload.service.ts new file mode 100644 index 0000000000..a5861660cd --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/acq/picklist/upload.service.ts @@ -0,0 +1,277 @@ +import {Injectable} from '@angular/core'; +import {Observable} from 'rxjs'; +import {tap, map} from 'rxjs/operators'; +import {HttpClient} from '@angular/common/http'; +import {saveAs} from 'file-saver'; +import {IdlService, IdlObject} from '@eg/core/idl.service'; +import {OrgService} from '@eg/core/org.service'; +import {NetService} from '@eg/core/net.service'; +import {AuthService} from '@eg/core/auth.service'; +import {PcrudService} from '@eg/core/pcrud.service'; +import {PermService} from '@eg/core/perm.service'; +import {EventService} from '@eg/core/event.service'; +import {ProgressDialogComponent} from '@eg/share/dialog/progress.component'; +import {VandelayImportSelection, VANDELAY_EXPORT_PATH} from '@eg/staff/cat/vandelay/vandelay.service' + + +@Injectable() +export class PicklistUploadService { + + allQueues: {[qtype: string]: IdlObject[]}; + attrDefs: {[atype: string]: IdlObject[]}; + bibSources: IdlObject[]; + matchSets: {[stype: string]: IdlObject[]}; + importItemAttrDefs: IdlObject[]; + mergeProfiles: IdlObject[]; + providersList: IdlObject[]; + fiscalYears: IdlObject[]; + selectionLists: IdlObject[]; + + // Used for tracking records between the queue page and + // the import page. Fields managed externally. + importSelection: VandelayImportSelection; + + // Track the last grid offset in the queue page so we + // can return the user to the same page of data after + // going to the matches page. + queuePageOffset: number; + + constructor( + private http: HttpClient, + private idl: IdlService, + private org: OrgService, + private evt: EventService, + private net: NetService, + private auth: AuthService, + private pcrud: PcrudService, + private perm: PermService + ) { + this.attrDefs = {}; + this.allQueues = {}; + this.matchSets = {}; + this.importSelection = null; + this.queuePageOffset = 0; + } + + getAttrDefs(dtype: string): Promise { + if (this.attrDefs[dtype]) { + return Promise.resolve(this.attrDefs[dtype]); + } + const cls = (dtype === 'bib') ? 'vqbrad' : 'vqarad'; + const orderBy = {}; + orderBy[cls] = 'id'; + return this.pcrud.retrieveAll(cls, + {order_by: orderBy}, {atomic: true}).toPromise() + .then(list => { + this.attrDefs[dtype] = list; + return list; + }); + } + + getMergeProfiles(): Promise { + if (this.mergeProfiles) { + return Promise.resolve(this.mergeProfiles); + } + + const owners = this.org.ancestors(this.auth.user().ws_ou(), true); + return this.pcrud.search('vmp', + {owner: owners}, {order_by: {vmp: ['name']}}, {atomic: true}) + .toPromise().then(profiles => { + this.mergeProfiles = profiles; + return profiles; + }); + } + + getProvidersList(): Promise { + if (this.providersList) { + return Promise.resolve(this.providersList); + } + + const owners = this.org.ancestors(this.auth.user().ws_ou(), true); + return this.pcrud.search('acqpro', + {owner: owners}, {order_by: {acqpro: ['code']}}, {atomic: true}) + .toPromise().then(providers => { + this.providersList = providers; + return providers; + }); + } + + getSelectionLists(): Promise { + if (this.selectionLists) { + return Promise.resolve(this.selectionLists); + } + + const owners = this.auth.user().id(); + return this.pcrud.search('acqpl', + {owner: owners}, {order_by: {acqpl: ['name']}}, {atomic: true}) + .toPromise().then(lists => { + this.selectionLists = lists; + return lists; + }); + } + // Returns a promise resolved with the list of queues. + getAllQueues(qtype: string): Promise { + if (this.allQueues[qtype]) { + return Promise.resolve(this.allQueues[qtype]); + } else { + this.allQueues[qtype] = []; + } + + // could be a big list, invoke in streaming mode + return this.net.request( + 'open-ils.vandelay', + `open-ils.vandelay.${qtype}_queue.owner.retrieve`, + this.auth.token() + ).pipe(tap( + queue => this.allQueues[qtype].push(queue) + )).toPromise().then(() => this.allQueues[qtype]); + } + + getBibSources(): Promise { + if (this.bibSources) { + return Promise.resolve(this.bibSources); + } + + return this.pcrud.retrieveAll('cbs', + {order_by: {cbs: 'id'}}, + {atomic: true} + ).toPromise().then(sources => { + this.bibSources = sources; + return sources; + }); + } + + getFiscalYears(): Promise { + if (this.fiscalYears) { + return Promise.resolve(this.fiscalYears); + } + + return this.pcrud.retrieveAll('acqfy', + {order_by: {acqfy: 'year'}}, + {atomic: true} + ).toPromise().then(years => { + this.fiscalYears = years; + return years; + }); + } + + getItemImportDefs(): Promise { + if (this.importItemAttrDefs) { + return Promise.resolve(this.importItemAttrDefs); + } + + const owners = this.org.ancestors(this.auth.user().ws_ou(), true); + return this.pcrud.search('viiad', {owner: owners}, {}, {atomic: true}) + .toPromise().then(defs => { + this.importItemAttrDefs = defs; + return defs; + }); + } + + // todo: differentiate between biblio and authority a la queue api + getMatchSets(mtype: string): Promise { + + const mstype = mtype.match(/bib/) ? 'biblio' : 'authority'; + + if (this.matchSets[mtype]) { + return Promise.resolve(this.matchSets[mtype]); + } else { + this.matchSets[mtype] = []; + } + + const owners = this.org.ancestors(this.auth.user().ws_ou(), true); + + return this.pcrud.search('vms', + {owner: owners, mtype: mstype}, {}, {atomic: true}) + .toPromise().then(sets => { + this.matchSets[mtype] = sets; + return sets; + }); + } + + + + + + + // Create a queue and return the ID of the new queue via promise. + createQueue( + queueName: string, + recordType: string, + importDefId: number, + matchSet: number): Promise { + + const method = `open-ils.vandelay.${recordType}_queue.create`; + + let qType = recordType; + if (recordType.match(/acq/)) { + qType = 'bib'; + } + + return new Promise((resolve, reject) => { + this.net.request( + 'open-ils.vandelay', method, + this.auth.token(), queueName, null, qType, + matchSet, importDefId + ).subscribe(queue => { + const e = this.evt.parse(queue); + if (e) { + reject(e); + } else { + // createQueue is always called after queues have + // been fetched and cached. + this.allQueues[qType].push(queue); + resolve(queue.id()); + } + }); + }); + } + + getQueuedRecords(queueId: number, queueType: string, + options?: any, limitToMatches?: boolean): Observable { + + const qtype = queueType.match(/bib/) ? 'bib' : 'auth'; + + let method = + `open-ils.vandelay.${qtype}_queue.records.retrieve`; + + if (limitToMatches) { + method = + `open-ils.vandelay.${qtype}_queue.records.matches.retrieve`; + } + + return this.net.request('open-ils.vandelay', + method, this.auth.token(), queueId, options); + } + + // Download a queue as a MARC file. + exportQueue(queue: IdlObject, nonImported?: boolean) { + + const etype = queue.queue_type().match(/auth/) ? 'auth' : 'bib'; + + let url = + `${VANDELAY_EXPORT_PATH}?type=bib&queueid=${queue.id()}`; + + let saveName = queue.name(); + + if (nonImported) { + url += '&nonimported=1'; + saveName += '_nonimported'; + } + + saveName += '.mrc'; + + this.http.get(url, {responseType: 'text'}).subscribe( + data => { + saveAs( + new Blob([data], {type: 'application/octet-stream'}), + saveName + ); + }, + err => { + console.error(err); + } + ); + } +} + diff --git a/Open-ILS/src/eg2/src/app/staff/nav.component.html b/Open-ILS/src/eg2/src/app/staff/nav.component.html index 95bd530a64..29890189b6 100644 --- a/Open-ILS/src/eg2/src/app/staff/nav.component.html +++ b/Open-ILS/src/eg2/src/app/staff/nav.component.html @@ -283,10 +283,10 @@ - - Load MARC Order Records - + routerLink="/staff/acq/picklist/upload"> + + Load MARC Order Records + diff --git a/Open-ILS/src/sql/Pg/950.data.seed-values.sql b/Open-ILS/src/sql/Pg/950.data.seed-values.sql index 7935ff97d3..271443f9ad 100644 --- a/Open-ILS/src/sql/Pg/950.data.seed-values.sql +++ b/Open-ILS/src/sql/Pg/950.data.seed-values.sql @@ -22470,3 +22470,13 @@ $TEMPLATE$ $TEMPLATE$ ); + +INSERT INTO config.workstation_setting_type (name, grp, datatype, label) +VALUES ( + 'eg.acq.picklist.upload.templates', 'acq', 'object', + oils_i18n_gettext( + 'eg.acq.picklist.upload.templates', + 'Picklist Upload Form Templates', + 'cwst', 'label' + ) +); diff --git a/Open-ILS/src/sql/Pg/upgrade/XXXX.data.picklist_uploader_templates.sql b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.picklist_uploader_templates.sql new file mode 100644 index 0000000000..7b1e98d417 --- /dev/null +++ b/Open-ILS/src/sql/Pg/upgrade/XXXX.data.picklist_uploader_templates.sql @@ -0,0 +1,15 @@ +BEGIN; + +SELECT evergreen.upgrade_deps_block_check('TODO', :eg_version); + +INSERT INTO config.workstation_setting_type (name, grp, datatype, label) +VALUES ( + 'eg.acq.picklist.upload.templates','acq','object', + oils_i18n_gettext( + 'eg.acq.picklist.upload.templates', + 'Acq Picklist Uploader Templates', + 'cwst','label' + ) +); + +COMMIT; \ No newline at end of file diff --git a/Open-ILS/src/templates/staff/navbar.tt2 b/Open-ILS/src/templates/staff/navbar.tt2 index f241676135..50b7928e94 100644 --- a/Open-ILS/src/templates/staff/navbar.tt2 +++ b/Open-ILS/src/templates/staff/navbar.tt2 @@ -414,7 +414,7 @@
  • - + [% l('Load MARC Order Records') %]