<class id="acqliat" controller="open-ils.cstore open-ils.pcrud" oils_obj:fieldmapper="acq::lineitem_alert_text" oils_persist:tablename="acq.lineitem_alert_text" reporter:label="Line Item Alert Text">
<fields oils_persist:primary="id" oils_persist:sequence="acq.lineitem_alert_text_id_seq">
- <field reporter:label="Alert Text ID" name="id" reporter:datatype="id"/>
+ <field reporter:label="Alert Text ID" name="id" reporter:datatype="id" reporter:selector="code"/>
<field reporter:label="Code" name="code" reporter:datatype="text"/>
<field reporter:label="Description" name="description" reporter:datatype="text"/>
<field reporter:label="Owning Library" name="owning_lib" reporter:datatype="link"/>
}
}
- recType: string;
+ get recordId(): number {
+ return this.recId;
+ }
+
+ private _recordXml: string;
+ @Input() set recordXml(xml: string) {
+ this._recordXml = xml;
+ if (this.initDone) {
+ this.collectData();
+ }
+ }
+
+ get recordXml(): string {
+ return this._recordXml;
+ }
+
+ private _recordType: string;
@Input() set recordType(rtype: string) {
- this.recType = rtype;
+ this._recordType = rtype;
+ }
+
+ get recordType(): string {
+ return this._recordType;
}
constructor(
) {}
ngOnInit() {
- this.initDone = true;
- this.collectData();
+ this.collectData().then(_ => this.initDone = true);
}
- collectData() {
- if (!this.recId) { return; }
+ collectData(): Promise<any> {
+ if (!this.recordId && !this.recordXml) { return Promise.resolve(); }
let service = 'open-ils.search';
let method = 'open-ils.search.biblio.record.html';
- const params: any[] = [this.recId];
+ let params: any[] = [this.recordId];
- switch (this.recType) {
+ switch (this.recordType) {
case 'authority':
method = 'open-ils.search.authority.to_html';
break;
}
- this.net.requestWithParamList(service, method, params)
+ // Bib/auth variants support generating HTML directly from MARC XML
+ if (!this.recordId && (
+ this.recordType === 'bib' || this.recordType === 'authority')) {
+ params = [null, null, this.recordXml];
+ }
+
+ return this.net.requestWithParamList(service, method, params)
.toPromise().then(html => this.injectHtml(html));
}
</span>
</ng-template>
-<div class="d-flex">
- <input type="text"
- class="form-control"
- [id]="domId"
- [ngClass]="{
- 'text-success font-italic font-weight-bold': selected && selected.freetext,
- 'form-control-sm': smallFormControl
- }"
- [placeholder]="placeholder"
- [name]="name"
- [disabled]="isDisabled"
- [required]="isRequired"
- [(ngModel)]="selected"
- [ngbTypeahead]="filter"
- [resultTemplate]="getResultTemplate()"
- [inputFormatter]="formatDisplayString"
- (click)="onClick($event)"
- (blur)="onBlur()"
- (keyup.arrowdown)="onClick($event)"
- container="body"
- (selectItem)="selectorChanged($event)"
- #instance="ngbTypeahead"/>
- <div class="d-flex flex-column icons" (click)="openMe($event)">
- <span class="material-icons">keyboard_arrow_up</span>
- <span class="material-icons">keyboard_arrow_down</span>
+<ng-container *ngIf="readOnly && selected">
+ <ng-container *ngTemplateOutlet="getResultTemplate();context:{result: selected}">
+ </ng-container>
+</ng-container>
+
+<ng-container *ngIf="!readOnly">
+ <div class="d-flex">
+ <input type="text"
+ class="form-control"
+ [id]="domId"
+ [ngClass]="{
+ 'text-success font-italic font-weight-bold': selected && selected.freetext,
+ 'form-control-sm': smallFormControl
+ }"
+ [placeholder]="placeholder"
+ [name]="name"
+ [disabled]="isDisabled"
+ [required]="isRequired"
+ [(ngModel)]="selected"
+ [ngbTypeahead]="filter"
+ [resultTemplate]="getResultTemplate()"
+ [inputFormatter]="formatDisplayString"
+ (click)="onClick($event)"
+ (blur)="onBlur()"
+ container="body"
+ (selectItem)="selectorChanged($event)"
+ #instance="ngbTypeahead"/>
+ <div class="d-flex flex-column icons" (click)="openMe($event)">
+ <span class="material-icons">keyboard_arrow_up</span>
+ <span class="material-icons">keyboard_arrow_down</span>
+ </div>
</div>
-</div>
+</ng-container>
click$: Subject<string>;
entrylist: ComboboxEntry[];
- @ViewChild('instance', { static: true }) instance: NgbTypeahead;
- @ViewChild('defaultDisplayTemplate', { static: true}) defaultDisplayTemplate: TemplateRef<any>;
+ @ViewChild('instance', {static: false}) instance: NgbTypeahead;
+ @ViewChild('defaultDisplayTemplate', {static: true}) defaultDisplayTemplate: TemplateRef<any>;
@ViewChildren(IdlClassTemplateDirective) idlClassTemplates: QueryList<IdlClassTemplateDirective>;
@Input() domId = 'eg-combobox-' + ComboboxComponent.domIdAuto++;
// when fetching objects by idlClass.
@Input() idlQueryAnd: {[field: string]: any};
+ // Display the selected value as text instead of within
+ // the typeahead
+ @Input() readOnly = false;
+
// Allow the selected entry ID to be passed via the template
// This does NOT not emit onChange events.
@Input() set selectedId(id: any) {
<eg-string #unsetString text="<Unset>" i18n-text></eg-string>
<eg-combobox #comboBox
+ [asyncDataSource]="loadAsync ? getLocationsAsyncHandler : null"
[domId]="domId"
[startId]="startId"
+ [disabled]="disabled"
+ [readOnly]="readOnly"
[displayTemplate]="displayTemplate"
(onChange)="cboxChanged($event)"
[required]="required"
import {Component, OnInit, AfterViewInit, Input, Output, ViewChild,
EventEmitter, forwardRef} from '@angular/core';
import {ControlValueAccessor, FormGroup, FormControl, NG_VALUE_ACCESSOR} from '@angular/forms';
-import {Observable} from 'rxjs';
-import {map} from 'rxjs/operators';
+import {Observable, from, of} from 'rxjs';
+import {tap, map, switchMap} from 'rxjs/operators';
import {IdlObject} from '@eg/core/idl.service';
import {OrgService} from '@eg/core/org.service';
import {AuthService} from '@eg/core/auth.service';
import {PcrudService} from '@eg/core/pcrud.service';
import {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.component';
import {StringComponent} from '@eg/share/string/string.component';
+import {ItemLocationService} from './item-location-select.service';
/**
* Item (Copy) Location Selector.
@Input() domId = 'eg-item-location-select-' +
ItemLocationSelectComponent.domIdAuto++;
+ // If false, selector will be click-able
+ @Input() loadAsync = true;
+
+ @Input() disabled = false;
+
+ // Display the selected value as text instead of within
+ // the typeahead
+ @Input() readOnly = false;
+
@ViewChild('comboBox', {static: false}) comboBox: ComboboxComponent;
@ViewChild('unsetString', {static: false}) unsetString: StringComponent;
- startId: number = null;
+ @Input() startId: number = null;
filterOrgs: number[] = [];
- cache: {[id: number]: IdlObject} = {};
+ filterOrgsApplied = false;
initDone = false; // true after first data load
propagateChange = (id: number) => {};
propagateTouch = () => {};
+ getLocationsAsyncHandler = term => this.getLocationsAsync(term);
+
constructor(
private org: OrgService,
private auth: AuthService,
private perm: PermService,
- private pcrud: PcrudService
+ private pcrud: PcrudService,
+ private loc: ItemLocationService
) {
this.valueChange = new EventEmitter<IdlObject>();
}
ngOnInit() {
- this.setFilterOrgs()
- .then(_ => this.getLocations())
- .then(_ => this.initDone = true);
+ if (this.loadAsync) {
+ this.initDone = true;
+ } else {
+ this.setFilterOrgs()
+ .then(_ => this.getLocations())
+ .then(_ => this.initDone = true);
+ }
}
ngAfterViewInit() {
return this.pcrud.search('acpl', search, {order_by: {acpl: 'name'}}
).pipe(map(loc => {
- this.cache[loc.id()] = loc;
+ this.loc.locationCache[loc.id()] = loc;
entries.push({id: loc.id(), label: loc.name(), userdata: loc});
})).toPromise().then(_ => {
this.comboBox.entries = entries;
});
}
+ getLocationsAsync(term: string): Observable<ComboboxEntry> {
+
+ let obs = of();
+ if (!this.filterOrgsApplied) {
+ // Apply filter orgs the first time they are needed.
+ obs = from(this.setFilterOrgs());
+ }
+
+ return obs.pipe(switchMap(_ => this.getLocationsAsync2(term)));
+ }
+
+ getLocationsAsync2(term: string): Observable<ComboboxEntry> {
+
+ if (this.filterOrgs.length === 0) {
+ return of();
+ }
+
+ const search: any = {
+ deleted: 'f',
+ name: {'ilike': `%${term}%`}
+ };
+
+ if (this.startId) {
+ // Guarantee we have the load-time copy location, which
+ // may not be included in the org-scoped set of locations
+ // we fetch by default.
+ search['-or'] = [
+ {id: this.startId},
+ {owning_lib: this.filterOrgs}
+ ];
+ } else {
+ search.owning_lib = this.filterOrgs;
+ }
+
+ return new Observable<ComboboxEntry>(observer => {
+ if (!this.required) {
+ observer.next({id: null, label: this.unsetString.text});
+ }
+
+ this.pcrud.search('acpl', search, {order_by: {acpl: 'name'}}
+ ).subscribe(
+ loc => {
+ this.loc.locationCache[loc.id()] = loc;
+ observer.next({id: loc.id(), label: loc.name(), userdata: loc});
+ },
+ err => {},
+ () => observer.complete()
+ );
+ });
+ }
+
+
registerOnChange(fn) {
this.propagateChange = fn;
}
cboxChanged(entry: ComboboxEntry) {
const id = entry ? entry.id : null;
this.propagateChange(id);
- this.valueChange.emit(id ? this.cache[id] : null);
+ this.valueChange.emit(id ? this.loc.locationCache[id] : null);
}
writeValue(id: number) {
}
getOneLocation(id: number) {
- if (!id || this.cache[id]) { return Promise.resolve(); }
+ if (!id) { return Promise.resolve(); }
+
+ const promise = this.loc.locationCache[id] ?
+ Promise.resolve(this.loc.locationCache[id]) :
+ this.pcrud.retrieve('acpl', id).toPromise();
+
+ return promise.then(loc => {
+
+ this.loc.locationCache[loc.id()] = loc;
+ const entry: ComboboxEntry = {
+ id: loc.id(), label: loc.name(), userdata: loc};
- return this.pcrud.retrieve('acpl', id).toPromise()
- .then(loc => {
- this.cache[loc.id()] = loc;
- this.comboBox.addAsyncEntry(
- {id: loc.id(), label: loc.name(), userdata: loc});
+ if (this.comboBox.entries) {
+ this.comboBox.entries.push(entry);
+ } else {
+ this.comboBox.entries = [entry];
+ }
});
}
let orgIds = [];
contextOrgIds.forEach(id => orgIds = orgIds.concat(this.org.ancestors(id, true)));
+ this.filterOrgsApplied = true;
+
if (!this.permFilter) {
return Promise.resolve(this.filterOrgs = [...new Set(orgIds)]);
}
+ const orgsFromCache = this.loc.filterOrgsCache[this.permFilter];
+ if (orgsFromCache) {
+ return Promise.resolve(this.filterOrgs = orgsFromCache);
+ }
+
return this.perm.hasWorkPermAt([this.permFilter], true)
.then(values => {
// Include ancestors of perm-approved org units (shared item locations)
}
});
- return this.filterOrgs = [...new Set(trimmedOrgIds)];
+ this.filterOrgs = [...new Set(trimmedOrgIds)];
+ this.loc.filterOrgsCache[this.permFilter] = this.filterOrgs;
+
+ return this.filterOrgs;
});
}
import {CommonWidgetsModule} from '@eg/share/common-widgets.module';
import {ItemLocationSelectComponent} from './item-location-select.component';
import {ReactiveFormsModule} from '@angular/forms';
+import {ItemLocationService} from './item-location-select.service';
@NgModule({
declarations: [
ItemLocationSelectComponent
],
providers: [
+ ItemLocationService
]
})
--- /dev/null
+import {Injectable, EventEmitter} from '@angular/core';
+import {Observable} from 'rxjs';
+import {switchMap, map} from 'rxjs/operators';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+
+@Injectable()
+export class ItemLocationService {
+
+ filterOrgsCache: {[perm: string]: number[]} = {};
+ locationCache: {[id: number]: IdlObject} = {};
+}
return this.org.get(this.selected.id);
}
+ selectedOrgId(): number {
+ return this.selected ? this.selected.id : null;
+ }
+
constructor(
private auth: AuthService,
private store: StoreService,
this.applyTemplate(printReq).then(() => {
// Give templates a chance to render before printing
setTimeout(() => {
- this.dispatchPrint(printReq).then(__ => this.reset());
+ this.dispatchPrint(printReq).then(__ => {
+ this.reset();
+ this.printer.printJobQueued$.emit(printReq);
+ });
});
});
});
onPrintRequest$: EventEmitter<PrintRequest>;
+ // Emitted after a print request has been delivered to Hatch or
+ // window.print() has completed. Note window.print() returning
+ // is not necessarily an indication the job has completed.
+ printJobQueued$: EventEmitter<PrintRequest>;
+
constructor(
private locale: LocaleService,
private auth: AuthService,
private store: StoreService
) {
this.onPrintRequest$ = new EventEmitter<PrintRequest>();
+ this.printJobQueued$ = new EventEmitter<PrintRequest>();
}
print(printReq: PrintRequest) {
--- /dev/null
+
+
+.batch-copy-row:nth-child(even) {
+ background-color: rgba(0,0,0,.03);
+}
--- /dev/null
+
+<eg-confirm-dialog #confirmAlertsDialog
+ i18n-dialogTitle i18n-dialogBody
+ dialogTitle="Confirm Alert" dialogBody="{{alertText ? alertText.code() : ''}}">
+</eg-confirm-dialog>
+
+<eg-acq-cancel-dialog #cancelDialog></eg-acq-cancel-dialog>
+
+<!-- Note the flex values are set so they also match the layout
+ of the list of copies in the copies component. -->
+
+<ng-template #copyAttrsHeader let-hideBarcode="hideBarcode" let-moreCss="moreCss">
+ <div class="div d-flex font-weight-bold {{moreCss}}">
+ <div class="flex-1 p-1" i18n>Owning Branch</div>
+ <div class="flex-1 p-1" i18n>Copy Location</div>
+ <div class="flex-1 p-1" i18n>Collection Code</div>
+ <div class="flex-1 p-1" i18n>Fund</div>
+ <div class="flex-1 p-1" i18n>Circ Modifier</div>
+ <div class="flex-1 p-1" i18n>Callnumber</div>
+ <div class="flex-1 p-1" i18n>
+ <ng-container *ngIf="!hideBarcode">Barcode</ng-container>
+ </div>
+ <div class="flex-1 p-1"></div>
+ <div class="flex-1 p-1"></div>
+ </div>
+</ng-template>
+
+<ng-container
+ *ngTemplateOutlet="copyAttrsHeader;context:{
+ moreCss:'mt-3 bg-light border border-secondary',
+ hideBarcode: true
+ }">
+</ng-container>
+
+<div class="pt-2 bg-light border border-secondary border-top-0 rounded-bottom">
+ <eg-lineitem-copy-attrs (batchApplyRequested)="batchApplyAttrs($event)"
+ [batchMode]="true"> </eg-lineitem-copy-attrs>
+</div>
+
+<hr/>
+
+<ng-container *ngTemplateOutlet="copyAttrsHeader"> </ng-container>
+
+<div class="mt-1 pt-1 border-top">
+ <div class="batch-copy-row" *ngFor="let copy of lineitem.lineitem_details()">
+ <eg-lineitem-copy-attrs
+ (receiveRequested)="receiveCopy($event)"
+ (unReceiveRequested)="unReceiveCopy($event)"
+ (deleteRequested)="deleteCopy($event)"
+ (cancelRequested)="cancelCopy($event)"
+ [lineitem]="lineitem" [copy]="copy"></eg-lineitem-copy-attrs>
+ </div>
+</div>
+
+
+
+
--- /dev/null
+import {Component, OnInit, Input, Output, EventEmitter, ViewChild} from '@angular/core';
+import {tap} from 'rxjs/operators';
+import {Pager} from '@eg/share/util/pager';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {LineitemService} from './lineitem.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {LineitemCopyAttrsComponent} from './copy-attrs.component';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {CancelDialogComponent} from './cancel-dialog.component';
+
+const BATCH_FIELDS = [
+ 'owning_lib',
+ 'location',
+ 'collection_code',
+ 'fund',
+ 'circ_modifier',
+ 'cn_label'
+];
+
+@Component({
+ templateUrl: 'batch-copies.component.html',
+ selector: 'eg-lineitem-batch-copies',
+ styleUrls: ['batch-copies.component.css']
+})
+export class LineitemBatchCopiesComponent implements OnInit {
+
+ @Input() lineitem: IdlObject;
+
+ @ViewChild('confirmAlertsDialog') confirmAlertsDialog: ConfirmDialogComponent;
+ @ViewChild('cancelDialog') cancelDialog: CancelDialogComponent;
+
+ // Current alert that needs confirming
+ alertText: IdlObject;
+
+ constructor(
+ private evt: EventService,
+ private idl: IdlService,
+ private net: NetService,
+ private auth: AuthService,
+ private liService: LineitemService
+ ) {}
+
+ ngOnInit() {}
+
+ // Propagate values from the batch edit bar into the indivudual LID's
+ batchApplyAttrs(copyTemplate: IdlObject) {
+ BATCH_FIELDS.forEach(field => {
+ const val = copyTemplate[field]();
+ if (val === undefined) { return; }
+ this.lineitem.lineitem_details().forEach(copy => {
+ copy[field](val);
+ copy.ischanged(true); // isnew() takes precedence
+ });
+ });
+ }
+
+ deleteCopy(copy: IdlObject) {
+ if (copy.isnew()) {
+ // Brand new copies can be discarded
+ this.lineitem.lineitem_details(
+ this.lineitem.lineitem_details().filter(c => c.id() !== copy.id())
+ );
+ } else {
+ // Requires a Save Changes action.
+ copy.isdeleted(true);
+ }
+ }
+
+ refreshLineitem() {
+ this.liService.getFleshedLineitems([this.lineitem.id()], {toCache: true})
+ .subscribe(liStruct => this.lineitem = liStruct.lineitem);
+ }
+
+ handleActionResponse(resp: any) {
+ const evt = this.evt.parse(resp);
+ if (evt) {
+ alert(evt);
+ } else if (resp) {
+ this.refreshLineitem();
+ }
+ }
+
+ cancelCopy(copy: IdlObject) {
+ this.cancelDialog.open().subscribe(reason => {
+ if (!reason) { return; }
+ this.net.request('open-ils.acq',
+ 'open-ils.acq.lineitem_detail.cancel',
+ this.auth.token(), copy.id(), reason
+ ).subscribe(ok => this.handleActionResponse(ok));
+ });
+ }
+
+ receiveCopy(copy: IdlObject) {
+ this.checkLiAlerts().then(ok => {
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.lineitem_detail.receive',
+ this.auth.token(), copy.id()
+ ).subscribe(ok2 => this.handleActionResponse(ok2));
+ }, err => {}); // avoid console errors
+ }
+
+ unReceiveCopy(copy: IdlObject) {
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.lineitem_detail.receive.rollback',
+ this.auth.token(), copy.id()
+ ).subscribe(ok => this.handleActionResponse(ok));
+ }
+
+ checkLiAlerts(): Promise<boolean> {
+
+ let promise = Promise.resolve(true);
+
+ const notes = this.lineitem.lineitem_notes().filter(note =>
+ note.alert_text() && !this.liService.alertAcks[note.id()]);
+
+ if (notes.length === 0) { return promise; }
+
+ notes.forEach(n => {
+ promise = promise.then(_ => {
+ this.alertText = n.alert_text();
+ return this.confirmAlertsDialog.open().toPromise().then(ok => {
+ if (!ok) { return Promise.reject(); }
+ this.liService.alertAcks[n.id()] = true;
+ return true;
+ });
+ });
+ });
+
+ return promise;
+ }
+}
+
+
--- /dev/null
+
+<h3 class="m-2" i18n>Add A Brief Record</h3>
+
+<div class="d-flex w-50 m-2" *ngFor="let attr of attrs">
+ <div class="flex-1">{{attr.description()}}</div>
+ <div class="flex-3">
+ <input class="form-control" type="text" [(ngModel)]="values[attr.id()]"/>
+ </div>
+</div>
+
+<button class="btn btn-success mt-2" (click)="save()" i18n>Add Record</button>
--- /dev/null
+import {Component, OnInit, Input, Output} from '@angular/core';
+import {ActivatedRoute, Router, ParamMap} from '@angular/router';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {AuthService} from '@eg/core/auth.service';
+
+const MARC_NS = 'http://www.loc.gov/MARC21/slim';
+
+const MARC_XML_BASE = `
+<record xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xmlns="http://www.loc.gov/MARC21/slim"
+ xmlns:marc="http://www.loc.gov/MARC21/slim"
+ xsi:schemaLocation="http://www.loc.gov/MARC21/slim http://www.loc.gov/ standards/marcxml/schema/MARC21slim.xsd">
+ <leader>00000nam a22000007a 4500</leader>
+</record>
+`;
+
+@Component({
+ templateUrl: 'brief-record.component.html',
+ selector: 'eg-lineitem-brief-record'
+})
+export class BriefRecordComponent implements OnInit {
+
+ targetPicklist: number;
+ targetPo: number;
+
+ attrs: IdlObject[] = [];
+ values: {[attr: string]: string} = {};
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private idl: IdlService,
+ private auth: AuthService,
+ private net: NetService,
+ private pcrud: PcrudService
+ ) { }
+
+ ngOnInit() {
+
+ this.route.parent.paramMap.subscribe((params: ParamMap) => {
+ this.targetPicklist = +params.get('picklistId');
+ this.targetPo = +params.get('poId');
+ });
+
+ this.pcrud.retrieveAll('acqlimad')
+ .subscribe(attr => this.attrs.push(attr));
+ }
+
+ compile(): string {
+
+ const doc = new DOMParser().parseFromString(MARC_XML_BASE, 'text/xml');
+
+ this.attrs.forEach(attr => {
+ const value = this.values[attr.id()];
+ if (value === undefined) { return; }
+
+ const expr = attr.xpath();
+
+ // Logic copied from openils/MarcXPathParser.js
+ // Any 3 numbers are a 'tag'.
+ // Any letters are a subfield.
+ // Always use the first.
+ const tags = expr.match(/\d{3}/g);
+ let subfields = expr.match(/['"]([a-z]+)['"]/);
+ if (subfields) { subfields = subfields[1].split(''); }
+
+ const dfNode = doc.createElementNS(MARC_NS, 'marc:datafield');
+ const sfNode = doc.createElementNS(MARC_NS, 'marc:subfield');
+
+ // Append fields to the document
+ dfNode.setAttribute('tag', '' + tags[0]);
+ dfNode.setAttribute('ind1', ' ');
+ dfNode.setAttribute('ind2', ' ');
+ sfNode.setAttribute('code', '' + subfields[0]);
+ const tNode = doc.createTextNode(value);
+
+ sfNode.appendChild(tNode);
+ dfNode.appendChild(sfNode);
+ doc.documentElement.appendChild(dfNode);
+ });
+
+ return new XMLSerializer().serializeToString(doc);
+ }
+
+ save() {
+ const xml = this.compile();
+
+ const li = this.idl.create('jub');
+ li.marc(xml);
+
+ if (this.targetPicklist) {
+ li.picklist(this.targetPicklist);
+ } else if (this.targetPo) {
+ li.purchase_order(this.targetPo);
+ }
+
+ li.selector(this.auth.user().id());
+ li.creator(this.auth.user().id());
+ li.editor(this.auth.user().id());
+
+ this.net.request('open-ils.acq',
+ 'open-ils.acq.lineitem.create', this.auth.token(), li
+ ).toPromise().then(_ => {
+ this.router.navigate(['../'], {
+ relativeTo: this.route,
+ queryParamsHandling: 'merge'
+ });
+ });
+ }
+}
+
--- /dev/null
+<ng-template #dialogContent>
+ <form class="form-validated">
+ <div class="modal-header bg-info">
+ <h3 class="modal-title" i18n>Cancel</h3>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="close()">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <h4 i18n>Select a cancel reason:</h4>
+ <eg-combobox domId="acq-cancel-dialog" name="acq-cancel-dialog"
+ idlClass="acqcr" [(ngModel)]="cancelReason"></eg-combobox>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-success" [disabled]="!cancelReason"
+ (click)="close(cancelReason.id)" i18n>Apply</button>
+ <button type="button" class="btn btn-warning"
+ (click)="close()" i18n>Exit Dialog</button>
+ </div>
+ </form>
+</ng-template>
+
--- /dev/null
+import {Component, Input, ViewChild, TemplateRef, OnInit} from '@angular/core';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+@Component({
+ selector: 'eg-acq-cancel-dialog',
+ templateUrl: './cancel-dialog.component.html'
+})
+
+export class CancelDialogComponent extends DialogComponent {
+ cancelReason: number;
+ constructor(private modal: NgbModal) { super(modal); }
+}
+
+
--- /dev/null
+
+<div class="row mt-3 mb-1">
+ <div class="col-lg-12 form-inline">
+
+ <label class="ml-3" for='copy-count-input' i18n>Item Count: </label>
+ <input class="form-control-sm ml-3 small"
+ id='copy-count-input' [disabled]="liLocked"
+ [(ngModel)]="copyCount" type="text" (keyup.enter)="applyCount()"/>
+
+ <button class="btn btn-sm btn-outline-dark ml-3"
+ [disabled]="liLocked" (click)="applyCount()" i18n>Apply</button>
+
+ <span class="ml-3" i18n> | </span>
+
+ <label class="ml-3" for='distrib-formula-cbox' i18n>Distribution Formulas</label>
+ <span class="ml-3">
+ <eg-combobox idlClass="acqdf" [idlQueryAnd]="formulaFilter"
+ #distribFormCbox domId="distrib-formula-cbox">
+ </eg-combobox>
+ </span>
+ <button class="btn btn-sm btn-outline-dark ml-3"
+ [disabled]="!distribFormCbox.selectedId || liLocked"
+ (click)="applyFormula(distribFormCbox.selectedId)" i18n>Apply</button>
+
+ <button class="btn btn-sm btn-success ml-auto" [disabled]="liLocked"
+ (click)="save()" i18n>Save Changes</button>
+
+ </div>
+</div>
+
+<hr class="m-1 p-1"/>
+
+<div class="col-lg-6 offset-lg-3" *ngIf="saving">
+ <eg-progress-inline [max]="progressMax" [value]="progressValue">
+ </eg-progress-inline>
+</div>
+
+<ng-container *ngIf="lineitem && !saving">
+
+ <div class="card tight-card" *ngIf="lineitem.distribution_formulas().length">
+ <div class="card-header" i18n>Distribution formulas applied to this lineitem</div>
+ <div class="card-body">
+ <ul class="p-0 m-0">
+ <li class="list-group-item p-0 m-0 border-0"
+ *ngFor="let formula of lineitem.distribution_formulas()">
+ <div class="d-flex">
+ <button class="btn btn-outline-danger material-icon-button p-0 m-0"
+ (click)="deleteFormula(formula)" title="Delete Formula" i18n-title>
+ <span class="material-icons">delete</span>
+ </button>
+ <div class="ml-2">{{formula.create_time() | date:'short'}}</div>
+ <div class="ml-2">{{formula.creator().usrname()}}</div>
+ <div class="ml-2 flex-1">{{formula.formula().name()}}</div>
+ </div>
+ </li>
+ </ul>
+ </div>
+ </div>
+
+ <eg-lineitem-batch-copies [lineitem]="lineitem"></eg-lineitem-batch-copies>
+</ng-container>
+
+
--- /dev/null
+import {Component, OnInit, AfterViewInit, Input, Output, EventEmitter,
+ ViewChild} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {tap} from 'rxjs/operators';
+import {Pager} from '@eg/share/util/pager';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {AuthService} from '@eg/core/auth.service';
+import {LineitemService} from './lineitem.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {ItemLocationService} from '@eg/share/item-location-select/item-location-select.service';
+
+const FORMULA_FIELDS = [
+ 'owning_lib',
+ 'location',
+ 'fund',
+ 'circ_modifier',
+ 'collection_code'
+];
+
+interface FormulaApplication {
+ formula: IdlObject;
+ count: number;
+}
+
+@Component({
+ templateUrl: 'copies.component.html'
+})
+export class LineitemCopiesComponent implements OnInit, AfterViewInit {
+ static newCopyId = -1;
+
+ lineitemId: number;
+ lineitem: IdlObject;
+ copyCount = 1;
+ batchOwningLib: IdlObject;
+ batchFund: ComboboxEntry;
+ batchCopyLocId: number;
+ saving = false;
+ progressMax = 0;
+ progressValue = 0;
+ formulaFilter = {owner: []};
+ formulaOffset = 0;
+ formulaValues: {[field: string]: {[val: string]: boolean}} = {};
+
+ // Can any changes be applied?
+ liLocked = false;
+
+ constructor(
+ private route: ActivatedRoute,
+ private idl: IdlService,
+ private org: OrgService,
+ private net: NetService,
+ private pcrud: PcrudService,
+ private auth: AuthService,
+ private loc: ItemLocationService,
+ private liService: LineitemService
+ ) {}
+
+ ngOnInit() {
+
+ this.formulaFilter.owner =
+ this.org.fullPath(this.auth.user().ws_ou(), true);
+
+ this.route.paramMap.subscribe((params: ParamMap) => {
+ const id = +params.get('lineitemId');
+ if (id !== this.lineitemId) {
+ this.lineitemId = id;
+ if (id) { this.load(); }
+ }
+ });
+
+ this.liService.getLiAttrDefs();
+ }
+
+ load(): Promise<any> {
+ this.lineitem = null;
+ this.copyCount = 1;
+ return this.liService.getFleshedLineitems(
+ [this.lineitemId], {toCache: true, fromCache: true})
+ .pipe(tap(liStruct => this.lineitem = liStruct.lineitem)).toPromise()
+ .then(_ => {
+ this.liLocked =
+ this.lineitem.state().match(/on-order|received|cancelled/);
+ })
+ .then(_ => this.applyCount());
+ }
+
+ ngAfterViewInit() {
+ setTimeout(() => {
+ const node = document.getElementById('copy-count-input');
+ if (node) { (node as HTMLInputElement).select(); }
+ });
+ }
+
+ applyCount() {
+ const copies = this.lineitem.lineitem_details();
+ while (copies.length < this.copyCount) {
+ const copy = this.idl.create('acqlid');
+ copy.id(LineitemCopiesComponent.newCopyId--);
+ copy.isnew(true);
+ copy.lineitem(this.lineitem.id());
+ copies.push(copy);
+ }
+
+ if (copies.length > this.copyCount) {
+ this.copyCount = copies.length;
+ }
+ }
+
+ applyFormula(id: number) {
+
+ const copies = this.lineitem.lineitem_details();
+ if (this.formulaOffset >= copies.length) {
+ // We have already applied a formula entry to every item.
+ return;
+ }
+
+ this.formulaValues = {};
+
+ this.pcrud.retrieve('acqdf', id,
+ {flesh: 1, flesh_fields: {acqdf: ['entries']}})
+ .subscribe(formula => {
+
+ formula.entries(
+ formula.entries().sort((e1, e2) =>
+ e1.position() < e2.position() ? -1 : 1));
+
+ let rowIdx = this.formulaOffset - 1;
+
+ while (++rowIdx < copies.length) {
+ this.formulateOneCopy(formula, rowIdx, true);
+ }
+
+ // No new values will be applied
+ if (!Object.keys(this.formulaValues)) { return; }
+
+ this.fetchFormulaValues().then(_ => {
+
+ let applied = 0;
+ let rowIdx2 = this.formulaOffset - 1;
+
+ while (++rowIdx2 < copies.length) {
+ applied += this.formulateOneCopy(formula, rowIdx2);
+ }
+
+ if (applied) {
+ this.formulaOffset += applied;
+ this.saveAppliedFormula(formula);
+ }
+ });
+ });
+ }
+
+ saveAppliedFormula(formula: IdlObject) {
+ const app = this.idl.create('acqdfa');
+ app.lineitem(this.lineitem.id());
+ app.creator(this.auth.user().id());
+ app.formula(formula.id());
+
+ this.pcrud.create(app).toPromise().then(a => {
+ a.creator(this.auth.user());
+ a.formula(formula);
+ this.lineitem.distribution_formulas().push(a);
+ });
+ }
+
+ // Grab values applied by distribution formulas and cache them before
+ // applying them to their target copies, so the comboboxes, etc.
+ // are not required to go fetch them en masse / en duplicato.
+ fetchFormulaValues(): Promise<any> {
+
+ const funds = Object.keys(this.formulaValues.fund);
+ const mods = Object.keys(this.formulaValues.circ_modifier);
+ const locs = Object.keys(this.formulaValues.location);
+
+ let promise = Promise.resolve();
+
+ if (funds.length > 0) {
+ promise = promise.then(_ => {
+ return this.pcrud.search('acqf', {id: funds})
+ .pipe(tap(fund => {
+ this.liService.fundCache[fund.id()] = fund;
+ this.liService.batchOptionWanted.emit(
+ {fund: {id: fund.id(), label: fund.code(), fm: fund}});
+ })).toPromise();
+ });
+ }
+
+ if (mods.length > 0) {
+ promise = promise.then(_ => {
+ return this.pcrud.search('ccm', {code: mods})
+ .pipe(tap(mod => {
+ this.liService.circModCache[mod.code()] = mod;
+ this.liService.batchOptionWanted.emit({circ_modifier:
+ {id: mod.code(), label: mod.code(), fm: mod}});
+ })).toPromise();
+ });
+ }
+
+ if (locs.length > 0) {
+ promise = promise.then(_ => {
+ return this.pcrud.search('acpl', {id: locs})
+ .pipe(tap(loc => {
+ this.loc.locationCache[loc.id()] = loc;
+ this.liService.batchOptionWanted.emit({location:
+ {id: loc.id(), label: loc.name(), fm: loc}});
+ })).toPromise();
+ });
+ }
+
+ return promise;
+ }
+
+ // Apply a formula entry to a single copy.
+ // extracOnly means we are only collecting the new values we wish to
+ // apply from the formula w/o applying them to the copy in question.
+ formulateOneCopy(formula: IdlObject,
+ rowIdx: number, extractOnly?: boolean): number {
+
+ let targetEntry = null;
+ let entryIdx = this.formulaOffset;
+ const copy = this.lineitem.lineitem_details()[rowIdx];
+
+ // Find the correct entry for the current copy.
+ formula.entries().forEach(entry => {
+ if (!targetEntry) {
+ entryIdx += entry.item_count();
+ if (entryIdx > rowIdx) {
+ targetEntry = entry;
+ }
+ }
+ });
+
+ // We ran out of copies.
+ if (!targetEntry) { return 0; }
+
+ FORMULA_FIELDS.forEach(field => {
+ const val = targetEntry[field]();
+ if (val === undefined || val === null) { return; }
+
+ if (extractOnly) {
+ if (!this.formulaValues[field]) {
+ this.formulaValues[field] = {};
+ }
+ this.formulaValues[field][val] = true;
+
+ } else {
+ copy[field](val);
+ }
+ });
+
+ return 1;
+ }
+
+ save() {
+ this.saving = true;
+ this.progressMax = null;
+ this.progressValue = 0;
+
+ this.liService.updateLiDetails(this.lineitem).subscribe(
+ struct => {
+ this.progressMax = struct.total;
+ this.progressValue++;
+ },
+ err => {},
+ () => this.load().then(_ => {
+ this.liService.activateStateChange.emit(this.lineitem.id());
+ this.saving = false;
+ })
+ );
+ }
+
+ deleteFormula(formula: IdlObject) {
+ this.pcrud.remove(formula).subscribe(_ => {
+ this.lineitem.distribution_formulas(
+ this.lineitem.distribution_formulas()
+ .filter(f => f.id() !== formula.id())
+ );
+ });
+ }
+}
+
+
--- /dev/null
+<!-- Flex values are set to align with lineitem copies UI
+ and the batch copy editor component -->
+
+<div class="div d-flex batch-copy-row" *ngIf="copy">
+ <div class="flex-1 p-1">
+ <eg-org-select #owningLibSelect placeholder="Owning Branch..."
+ i18n-placeholder [readOnly]="fieldIsDisabled('owning_lib')"
+ [applyOrgId]="copy.owning_lib()"
+ (onChange)="valueChange('owning_lib', $event)">
+ </eg-org-select>
+ </div>
+ <div class="flex-1 p-1">
+ <eg-item-location-select [readOnly]="fieldIsDisabled('location')"
+ #locationSelector [ngModel]="copy.location()"
+ (valueChange)="valueChange('location', $event)"
+ permFilter="CREATE_PICKLIST">
+ </eg-item-location-select>
+ </div>
+ <div class="flex-1 p-1">
+ <ng-container *ngIf="fieldIsDisabled('collection_code')">
+ <span>{{copy.collection_code()}}</span>
+ </ng-container>
+ <ng-container *ngIf="!fieldIsDisabled('collection_code')">
+ <input type="text" class="form-control"
+ placeholder="Collection Code..." i18n-placeholder
+ (ngModelChange)="valueChange('collection_code', $event)"
+ [ngModel]="copy.collection_code()"/>
+ </ng-container>
+ </div>
+ <div class="flex-1 p-1">
+ <eg-combobox idlClass="acqf" placeholder="Fund..." i18n-placeholder
+ [readOnly]="fieldIsDisabled('fund')"
+ #fundSelector [entries]="fundEntries"
+ [selectedId]="copy.fund()" (onChange)="valueChange('fund', $event)"
+ [idlQueryAnd]="{active: 't'}">
+ </eg-combobox>
+ </div>
+ <div class="flex-1 p-1">
+ <eg-combobox idlClass="ccm" placeholder="Circ Modifier..." i18n-placeholder
+ [readOnly]="fieldIsDisabled('circ_modifier')"
+ #circModSelector [entries]="circModEntries"
+ [selectedId]="copy.circ_modifier()"
+ (onChange)="valueChange('circ_modifier', $event)">
+ </eg-combobox>
+ </div>
+ <div class="flex-1 p-1">
+ <ng-container *ngIf="fieldIsDisabled('cn_label')">
+ <span>{{copy.cn_label()}}</span>
+ </ng-container>
+ <ng-container *ngIf="!fieldIsDisabled('cn_label')">
+ <input type="text" class="form-control"
+ placeholder="Call Number..." i18n-placeholder
+ [ngModel]="copy.cn_label()"
+ (ngModelChange)="valueChange('cn_label', $event)">
+ </ng-container>
+ </div>
+ <div class="flex-1 p-1">
+ <ng-container *ngIf="batchMode">
+ <button class="btn btn-outline-dark"
+ (click)="batchApplyRequested.emit(copy)" i18n>Batch Update</button>
+ </ng-container>
+ <ng-container *ngIf="!batchMode">
+ <ng-container *ngIf="fieldIsDisabled('barcode')">
+ <span>{{copy.barcode()}}</span>
+ </ng-container>
+ <ng-container *ngIf="!fieldIsDisabled('barcode')">
+ <input type="text" class="form-control"
+ [disabled]="fieldIsDisabled('barcode')" [ngModel]="copy.barcode()"
+ placeholder="Barcode..." i18n-placeholder
+ (ngModelChange)="valueChange('barcode', $event)">
+ </ng-container>
+ </ng-container>
+ </div>
+ <ng-container *ngIf="!embedded">
+ <div class="flex-2 p-1 pr-2 pl-2">
+ <ng-container *ngIf="!batchMode">
+ <ng-container *ngIf="disposition() == 'pre-order'">
+ <button *ngIf="!copy.isdeleted()"
+ class="btn btn-outline-danger material-icon-button"
+ (click)="deleteRequested.emit(copy)" title="Delete Item" i18n-title>
+ <span class="material-icons">delete</span>
+ </button>
+ <button *ngIf="copy.isdeleted()"
+ class="btn btn-outline-info material-icon-button"
+ (click)="copy.isdeleted(false)" title="Un-Delete Item" i18n-title>
+ <span class="material-icons">restore_page</span>
+ </button>
+ </ng-container>
+ <ng-container *ngIf="disposition() == 'on-order'">
+ <a href="javascript:;" (click)="receiveRequested.emit(copy)" i18n>Mark Received</a>
+ </ng-container>
+ <ng-container *ngIf="disposition() == 'received'">
+ <a href="javascript:;" (click)="unReceiveRequested.emit(copy)" i18n>Un-Receive</a>
+ </ng-container>
+ <ng-container *ngIf="disposition() == 'on-order'">
+ <a href="javascript:;" class="ml-2" (click)="cancelRequested.emit(copy)" i18n>Cancel</a>
+ </ng-container>
+ <ng-container *ngIf="disposition() == 'delayed'">
+ <a href="javascript:;" (click)="cancelRequested.emit(copy)" i18n>Cancel</a>
+ </ng-container>
+ <ng-container *ngIf="disposition() == 'delayed'">
+ <span class="font-italic ml-2" title="{{copy.cancel_reason().description()}}">
+ {{copy.cancel_reason().label()}}
+ </span>
+ </ng-container>
+ <ng-container *ngIf="disposition() == 'canceled'">
+ <span class="font-italic" title="{{copy.cancel_reason().description()}}">
+ {{copy.cancel_reason().label()}}
+ </span>
+ </ng-container>
+
+ </ng-container>
+ </div>
+ </ng-container>
+</div>
+
--- /dev/null
+import {Component, OnInit, AfterViewInit, ViewChild, Input, Output, EventEmitter} from '@angular/core';
+import {tap} from 'rxjs/operators';
+import {Pager} from '@eg/share/util/pager';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {LineitemService} from './lineitem.service';
+import {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {ItemLocationService} from '@eg/share/item-location-select/item-location-select.service';
+import {ItemLocationSelectComponent} from '@eg/share/item-location-select/item-location-select.component';
+
+@Component({
+ templateUrl: 'copy-attrs.component.html',
+ selector: 'eg-lineitem-copy-attrs'
+})
+export class LineitemCopyAttrsComponent implements OnInit {
+
+ @Input() lineitem: IdlObject;
+ fundEntries: ComboboxEntry[];
+ circModEntries: ComboboxEntry[];
+
+ private _copy: IdlObject;
+ @Input() set copy(c: IdlObject) { // acqlid
+ if (c === undefined) {
+ return;
+ } else if (c === null) {
+ this._copy = null;
+ } else {
+ // Enture cbox entries are populated before the copy is
+ // applied so the cbox has the minimal set of values it
+ // needs at copy render time.
+ this.setInitialOptions(c);
+ this._copy = c;
+ }
+ }
+
+ get copy(): IdlObject {
+ return this._copy;
+ }
+
+ // A row of batch edit inputs
+ @Input() batchMode = false;
+
+ // One of several rows embedded in the main LI list page.
+ // Always read-only.
+ @Input() embedded = false;
+
+ // Emits an 'acqlid' object;
+ @Output() batchApplyRequested: EventEmitter<IdlObject> = new EventEmitter<IdlObject>();
+ @Output() deleteRequested: EventEmitter<IdlObject> = new EventEmitter<IdlObject>();
+ @Output() receiveRequested: EventEmitter<IdlObject> = new EventEmitter<IdlObject>();
+ @Output() unReceiveRequested: EventEmitter<IdlObject> = new EventEmitter<IdlObject>();
+ @Output() cancelRequested: EventEmitter<IdlObject> = new EventEmitter<IdlObject>();
+
+ @ViewChild('locationSelector') locationSelector: ItemLocationSelectComponent;
+ @ViewChild('circModSelector') circModSelector: ComboboxComponent;
+ @ViewChild('fundSelector') fundSelector: ComboboxComponent;
+
+ constructor(
+ private idl: IdlService,
+ private net: NetService,
+ private auth: AuthService,
+ private loc: ItemLocationService,
+ private liService: LineitemService
+ ) {}
+
+ ngOnInit() {
+
+ if (this.batchMode) { // stub batch copy
+ this.copy = this.idl.create('acqlid');
+ this.copy.isnew(true);
+
+ } else {
+
+ // When a batch selector value changes, duplicate the selected
+ // value into our selector entries, so if/when the value is
+ // chosen we (and our pile of siblings) are not required to
+ // re-fetch them from the server.
+ this.liService.batchOptionWanted.subscribe(option => {
+ const field = Object.keys(option)[0];
+ if (field === 'location') {
+ this.locationSelector.comboBox.addAsyncEntry(option[field]);
+ } else if (field === 'circ_modifier') {
+ this.circModSelector.addAsyncEntry(option[field]);
+ } else if (field === 'fund') {
+ this.fundSelector.addAsyncEntry(option[field]);
+ }
+ });
+ }
+ }
+
+ valueChange(field: string, entry: ComboboxEntry) {
+
+ const announce: any = {};
+ this.copy.ischanged(true);
+
+ switch (field) {
+
+ case 'cn_label':
+ case 'barcode':
+ case 'collection_code':
+ this.copy[field](entry);
+ break;
+
+ case 'owning_lib':
+ this.copy[field](entry ? entry.id() : null);
+ break;
+
+ case 'location':
+ this.copy[field](entry ? entry.id() : null);
+ if (this.batchMode) {
+ announce[field] = entry;
+ this.liService.batchOptionWanted.emit(announce);
+ }
+ break;
+
+ case 'circ_modifier':
+ case 'fund':
+ this.copy[field](entry ? entry.id : null);
+ if (this.batchMode) {
+ announce[field] = entry;
+ this.liService.batchOptionWanted.emit(announce);
+ }
+ break;
+ }
+ }
+
+ // Tell our inputs about the values we know we need
+ // Values will be pre-cached in the liService
+ setInitialOptions(copy: IdlObject) {
+
+ if (copy.fund()) {
+ const fund = this.liService.fundCache[copy.fund()];
+ this.fundEntries = [{id: fund.id(), label: fund.code(), fm: fund}];
+ }
+
+ if (copy.circ_modifier()) {
+ const mod = this.liService.circModCache[copy.circ_modifier()];
+ this.circModEntries = [{id: mod.code(), label: mod.name(), fm: mod}];
+ }
+ }
+
+ fieldIsDisabled(field: string) {
+ if (this.batchMode) { return false; }
+
+ if (this.embedded || // inline expandy view
+ this.copy.isdeleted() ||
+ this.disposition() !== 'pre-order') {
+ return true;
+ }
+
+ return false;
+ }
+
+ disposition(): 'canceled' | 'delayed' | 'received' | 'on-order' | 'pre-order' {
+ if (!this.copy || !this.lineitem) {
+ return null;
+ } else if (this.copy.cancel_reason()) {
+ if (this.copy.cancel_reason().keep_debits() === 't') {
+ return 'delayed';
+ } else {
+ return 'canceled';
+ }
+ } else if (this.copy.recv_time()) {
+ return 'received';
+ } else if (this.lineitem.state() === 'on-order') {
+ return 'on-order';
+ } else { return 'pre-order'; }
+ }
+}
+
+
--- /dev/null
+<hr class="p-1"/>
+
+<ul ngbNav #liDetailNav="ngbNav" class="nav-tabs">
+ <li ngbNavItem="attrs">
+ <a ngbNavLink i18n>Attributes</a>
+ <ng-template ngbNavContent>
+ <ng-container *ngIf="lineitem">
+ <div class="mt-3">
+ <div class="row" *ngFor="let attr of lineitem.attributes()">
+ <div class="col-lg-2">{{attrLabel(attr)}}</div>
+ <div class="col-lg-10 border-left">{{attr.attr_value()}}</div>
+ </div>
+ </div>
+ </ng-container>
+ </ng-template>
+ </li>
+ <li ngbNavItem="marc-html">
+ <a ngbNavLink i18n>MARC View</a>
+ <ng-template ngbNavContent>
+ <ng-container *ngIf="lineitem">
+ <div class="mt-3">
+ <eg-marc-html recordType="bib" [recordId]="lineitem.eg_bib_id()"
+ [recordXml]="lineitem.marc()"> </eg-marc-html>
+ </div>
+ </ng-container>
+ </ng-template>
+ </li>
+ <li ngbNavItem="marc-edit">
+ <a ngbNavLink i18n>MARC Edit</a>
+ <ng-template ngbNavContent>
+ <ng-container *ngIf="lineitem">
+ <div class="mt-3">
+ <div *ngIf="lineitem.eg_bib_id()" class="alert alert-warning" i18n>
+ Changes to lineitems that are linked to catalog records will
+ not result in changes to the cataloged record.
+ </div>
+ <eg-marc-editor [recordXml]="lineitem.marc()" [inPlaceMode]="true"
+ [recordType]="lineitem" (recordSaved)="saveMarcChanges($event)">
+ </eg-marc-editor>
+ </div>
+ </ng-container>
+ </ng-template>
+ </li>
+</ul>
+<div [ngbNavOutlet]="liDetailNav"></div>
+
+
+
--- /dev/null
+import {Component, OnInit, AfterViewInit, Input, Output, EventEmitter} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {tap} from 'rxjs/operators';
+import {Pager} from '@eg/share/util/pager';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {LineitemService, BatchLineitemStruct} from './lineitem.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+@Component({
+ templateUrl: 'detail.component.html'
+})
+export class LineitemDetailComponent implements OnInit {
+
+ lineitemId: number;
+ lineitem: IdlObject;
+ tab: string;
+
+ constructor(
+ private route: ActivatedRoute,
+ private net: NetService,
+ private auth: AuthService,
+ private liService: LineitemService
+ ) {}
+
+ ngOnInit() {
+
+ this.route.paramMap.subscribe((params: ParamMap) => {
+ const id = +params.get('lineitemId');
+ if (id !== this.lineitemId) {
+ this.lineitemId = id;
+ if (id) { this.load(); }
+ }
+ });
+
+ this.liService.getLiAttrDefs();
+ }
+
+ load() {
+ this.lineitem = null;
+ // Avoid pulling from cache since li's do not have marc()
+ // fleshed by default.
+ return this.liService.getFleshedLineitems([this.lineitemId], {
+ toCache: true, // OK to cache with marc()
+ fleshMore: {clear_marc: false}
+ }).pipe(tap(liStruct => this.lineitem = liStruct.lineitem)).toPromise();
+ }
+
+ attrLabel(attr: IdlObject): string {
+ if (!this.liService.liAttrDefs) { return; }
+
+ const def = this.liService.liAttrDefs.filter(
+ d => d.id() === attr.definition())[0];
+
+ return def ? def.description() : '';
+ }
+
+ saveMarcChanges(changes) { // MarcSavedEvent
+ const xml = changes.marcXml;
+ this.lineitem.marc(xml);
+ this.liService.updateLineitems([this.lineitem]).toPromise()
+ .then(_ => this.load());
+ }
+}
+
+
--- /dev/null
+
+<!-- TODO: workstation setting -->
+
+<div class="mt-3">
+ <eg-grid idlClass="acqlih" [dataSource]="dataSource" [sortable]="true"
+ persistKey="acq.lineitem.history"
+ hideFields="id,audit_id,marc,audit_time,audit_action,queued_record">
+ </eg-grid>
+</div>
--- /dev/null
+import {Component, OnInit, Input, Output} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {empty} from 'rxjs';
+import {Pager} from '@eg/share/util/pager';
+import {IdlObject} from '@eg/core/idl.service';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {PcrudService} from '@eg/core/pcrud.service';
+
+@Component({
+ templateUrl: 'history.component.html',
+ selector: 'eg-lineitem-history'
+})
+export class LineitemHistoryComponent implements OnInit {
+
+ lineitemId: number;
+ dataSource: GridDataSource = new GridDataSource();
+
+ constructor(
+ private route: ActivatedRoute,
+ private pcrud: PcrudService
+ ) {}
+
+ ngOnInit() {
+
+ this.dataSource.getRows = (pager: Pager, sort: any) =>
+ this.getHistory(pager, sort);
+
+ this.route.paramMap.subscribe((params: ParamMap) => {
+ this.lineitemId = +params.get('lineitemId');
+ });
+ }
+
+ getHistory(pager: Pager, sort: any) {
+ if (!this.lineitemId) { return empty(); }
+
+ const orderBy: any = {acqlih: 'edit_time DESC'};
+ if (sort.length) {
+ orderBy.acqlih = sort[0].name + ' ' + sort[0].dir;
+ }
+
+ return this.pcrud.search('acqlih', {id: this.lineitemId}, {
+ offset: pager.offset,
+ limit: pager.limit,
+ order_by: orderBy,
+ flesh: 1,
+ flesh_fields: {
+ acqlih: ['creator', 'editor', 'provider', 'cancel_reason']
+ }
+ });
+ }
+}
+
--- /dev/null
+
+.jacket-wrapper {
+ width: 70px;
+}
+.jacket {
+ width: 65px;
+}
+
+input[type="text"].form-control-sm { border-width: 1px; }
+
+.toolbar .form-check-label {
+ font-size: 115%;
+}
+
+.batch-copy-row:nth-child(even) {
+ background-color: rgba(0,0,0,.03);
+}
+
+/* Kind of hacky -- only way to get a toolbar button with no
+ * mat icon to line up horizontally with mat icon buttons */
+.toolbar .text-button {
+ padding-top: 11px;
+ padding-bottom: 11px;
+}
+
+
+.li-state-new { background-color: #FFFFEE; }
+.li-state-selector-ready { background-color: #FFEEEE; }
+.li-state-order-ready { background-color: #EEEEEE; }
+.li-state-pending-order { background-color: #EEEEDD; }
+.li-state-on-order { background-color: #EEDDDD; }
+.li-state-received { background-color: #DDDDDD; }
+.li-state-delayed { background-color: #99CCFF; }
--- /dev/null
+
+<!-- BATCH ACTIONS -->
+<eg-acq-cancel-dialog #cancelDialog></eg-acq-cancel-dialog>
+
+<div class="row mt-3">
+ <div class="col-lg-1">
+ <div ngbDropdown>
+ <button class="btn btn-info btn-sm" ngbDropdownToggle i18n>Actions</button>
+ <div ngbDropdownMenu>
+ <a ngbDropdownItem routerLink="../brief-record"
+ queryParamsHandling="merge" i18n>Add Brief Record</a>
+ <div class="dropdown-divider"></div>
+ <h6 class="dropdown-header" i18n>Selection List Actions</h6>
+ <button ngbDropdownItem (click)="deleteLineitems()"
+ [disabled]="!picklistId" i18n>Delete Selected Lineitems</button>
+ <button ngbDropdownItem (click)="createPo()"
+ [disabled]="!picklistId" i18n>Create Purchase Order from Selected Lineitems</button>
+ <button ngbDropdownItem (click)="createPo(true)"
+ [disabled]="!picklistId" i18n>Create Purchase Order from All Lineitems</button>
+ <div class="dropdown-divider"></div>
+ <h6 class="dropdown-header" i18n>Purchase Order Actions</h6>
+ <button ngbDropdownItem (click)="receiveSelected()"
+ [disabled]="!poId" i18n>Mark Selected Lineitems as Received</button>
+ <button ngbDropdownItem (click)="unReceiveSelected()"
+ [disabled]="!poId" i18n>Un-Receive Selected Lineitems</button>
+ <button ngbDropdownItem (click)="cancelSelected()"
+ [disabled]="!poId" i18n>Cancel Selected Lineitems</button>
+ </div>
+ </div>
+ </div>
+ <div class="col-lg-5">
+ <input type="text" class="form-control" [(ngModel)]="batchNote"
+ placeholder="New Line Item Note..." i18n-placeholder/>
+ </div>
+ <div class="col-lg-4 form-inline">
+ <div class="form-check mr-2">
+ <input class="form-check-input" type="checkbox"
+ id="vendor-public" [(ngModel)]="noteIsPublic">
+ <label class="form-check-label" for="vendor-public">
+ Note is vendor-public
+ </label>
+ </div>
+ <button class="btn btn-outline-dark" (click)="applyBatchNote()"
+ [disabled]="!selectedIds().length" i18n>
+ Apply To Selected
+ </button>
+ </div>
+</div>
+
+<div *ngIf="batchFailure" class="row mt-2 p-2">
+ <div class="col-lg-12 p-2 border border-danger label-with-material-icon" i18n>
+ <span class="material-icons text-danger pr-2">report</span>
+ Batch operation failed:
+ {{batchFailure.textcode}} {{batchFailure.desc}}
+
+ <a class="ml-auto" href="javascript:;"
+ (click)="batchFailure = null" title="Close" i18n-title>
+ <span class="material-icons text-danger">close</span>
+ </a>
+ </div>
+</div>
+
+<!-- NAVIGATION / EXPANDY -->
+
+<div class="row mt-3 mb-1 border border-info rounded toolbar">
+ <div class="col-lg-12 d-flex">
+ <div class="d-flex justify-content-center flex-column h-100">
+ <div class="form-check">
+ <input class="form-check-input" id='toggle-page-cbox'
+ [(ngModel)]="batchSelectPage" (change)="toggleSelectAll(false)" type="checkbox"/>
+ <label class="form-check-label" for='toggle-page-cbox' i18n>Items In Page</label>
+ </div>
+ </div>
+
+ <div class="d-flex justify-content-center flex-column h-100 ml-3">
+ <div class="form-check">
+ <input class="form-check-input" id='toggle-all-cbox'
+ [(ngModel)]="batchSelectAll" (change)="toggleSelectAll(true)" type="checkbox"/>
+ <label class="form-check-label" for='toggle-all-cbox' i18n>All Items</label>
+ </div>
+ </div>
+
+ <div class="d-flex ml-3 justify-content-center flex-column h-100">
+ <span class="font-italic" style="font-size:90%" i18n>
+ {{selectedIds().length}} Selected
+ </span>
+ </div>
+
+ <div class="flex-1"></div>
+
+ <div class="btn-toolbar">
+ <button type="button" (click)="toggleExpandAll()"
+ class="btn btn-sm btn-outline-dark mr-1">
+ <span title="Expand All" i18n-title *ngIf="!expandAll"
+ class="material-icons mat-icon-in-button">unfold_more</span>
+ <span title="Collapse All" i18n-title *ngIf="expandAll"
+ class="material-icons mat-icon-in-button">unfold_less</span>
+ </button>
+ <button [disabled]="pager.isFirstPage()" type="button"
+ class="btn btn-sm btn-outline-dark mr-1" (click)="pager.toFirst(); goToPage()">
+ <span title="First Page" i18n-title
+ class="material-icons mat-icon-in-button">first_page</span>
+ </button>
+ <button [disabled]="pager.isFirstPage()" type="button"
+ class="btn btn-sm btn-outline-dark mr-1" (click)="pager.decrement(); goToPage()">
+ <span title="Previous Page" i18n-title
+ class="material-icons mat-icon-in-button">keyboard_arrow_left</span>
+ </button>
+ <button [disabled]="pager.isLastPage()" type="button"
+ class="btn btn-sm btn-outline-dark mr-1" (click)="pager.increment(); goToPage()">
+ <span title="Next Page" i18n-title
+ class="material-icons mat-icon-in-button">keyboard_arrow_right</span>
+ </button>
+ <div ngbDropdown class="mr-1" placement="bottom-right">
+ <button ngbDropdownToggle class="btn btn-outline-dark text-button">
+ <span title="Select Row Count" i18n-title i18n>
+ Rows {{pager.limit}}
+ </span>
+ </button>
+ <div class="dropdown-menu" ngbDropdownMenu>
+ <a class="dropdown-item" (click)="pageSizeChange(count)"
+ *ngFor="let count of [5, 10, 25, 50, 100, 500, 1000, 10000]">
+ <span class="ml-2">{{count}}</span>
+ </a>
+ </div>
+ </div>
+ </div><!-- buttons -->
+ </div>
+</div>
+
+<!-- LINEITEM LIST -->
+
+<ng-container *ngFor="let li of pageOfLineitems">
+ <div class="row mt-2 border-bottom pt-2 pb-2 li-state-{{li.state()}}">
+ <div class="col-lg-12 d-flex">
+ <div class="jacket-wrapper">
+ <ng-container *ngIf="jacketIdent(li)">
+ <a href="/opac/extras/ac/jacket/large/{{jacketIdent(li)}}">
+ <img class="jacket"
+ src='/opac/extras/ac/jacket/small/{{jacketIdent(li)}}'/>
+ </a>
+ </ng-container>
+ <ng-container *ngIf="!jacketIdent(li)"><img class="jacket"/></ng-container>
+ </div>
+
+ <div class="ml-2 flex-1"> <!-- lineitem summary info -->
+ <div class="row">
+ <div class="col-lg-12">
+ <input type="checkbox" [(ngModel)]="selected[li.id()]"/>
+ <a class="ml-2" queryParamsHandling="merge" [id]="li.id()"
+ routerLink="./lineitem/{{li.id()}}/detail">
+ {{displayAttr(li, 'title')}}
+ </a>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-12">
+ <span class="pr-1">{{displayAttr(li, 'author')}}</span>
+ <span class="pr-1">{{displayAttr(li, 'isbn')}}</span>
+ <span class="pr-1">{{displayAttr(li, 'issn')}}</span>
+ <span class="pr-1">{{displayAttr(li, 'edition')}}</span>
+ <span class="pr-1">{{displayAttr(li, 'pubdate')}}</span>
+ <span class="pr-1">{{displayAttr(li, 'publisher')}}</span>
+ <span class="pr-1">{{li.source_label()}}</span>
+ </div>
+ </div>
+ <div class="row" *ngIf="li.purchase_order()">
+ <div class="col-lg-12">
+ <eg-lineitem-order-summary [li]="li"></eg-lineitem-order-summary>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-12">
+ <span title="Lineitem ID" i18n-title i18n># {{li.id()}}</span>
+ <span class="ml-1 mr-1" i18n> | </span>
+ <span title="Existing Item Count" i18n-title i18n
+ [ngClass]="{'text-danger font-weight-bold': existingCopyCounts[li.id()] > 0}">
+ {{existingCopyCounts[li.id()]}}</span>
+ <span class="ml-1 mr-1" i18n> | </span>
+ <a class="label-with-material-icon" title="Items" i18n-title
+ routerLink="./lineitem/{{li.id()}}/items" queryParamsHandling="merge">
+ <span class="material-icons small mr-1">shopping_basket</span>
+ <span i18n>Items ({{li.lineitem_details().length}})</span>
+ </a>
+ <span class="ml-1 mr-1" i18n> | </span>
+ <a class="label-with-material-icon" title="Expand" i18n-title
+ href="javascript:;" (click)="toggleShowExpand(li.id())">
+ <ng-container *ngIf="showExpandFor != li.id()">
+ <span class="material-icons small mr-1">unfold_more</span>
+ <span i18n>Expand</span>
+ </ng-container>
+ <ng-container *ngIf="showExpandFor == li.id()">
+ <span class="material-icons small mr-1">unfold_less</span>
+ <span i18n>Collapse</span>
+ </ng-container>
+ </a>
+ <span class="ml-1 mr-1" i18n> | </span>
+ <a class="label-with-material-icon" title="Notes" i18n-title
+ href="javascript:;" (click)="toggleShowNotes(li.id())">
+ <span class="material-icons small mr-1">event_note</span>
+ <span i18n>Notes ({{li.lineitem_notes().length}})</span>
+ <span *ngIf="liHasAlerts(li)" class="text-danger material-icons"
+ title="Has Alerts" i18n-title>flag</span>
+ </a>
+ <span class="ml-1 mr-1" i18n> | </span>
+ <a class="label-with-material-icon"
+ routerLink="lineitem/{{li.id()}}/worksheet/">
+ <span class="material-icons small mr-1">create</span>
+ <span i18n>Worksheet</span>
+ </a>
+ <span class="ml-1 mr-1" i18n> | </span>
+ <a class="label-with-material-icon"
+ [queryParams]="{f: 'jub:id', val1: li.id()}"
+ routerLink="/staff/acq/search/invoices">
+ <span class="material-icons small mr-1">list</span>
+ <span i18n>Invoice(s)</span>
+ </a>
+ <ng-container *ngIf="li.eg_bib_id()">
+ <span class="ml-1 mr-1" i18n> | </span>
+ <a class="label-with-material-icon mr-2"
+ routerLink="/staff/catalog/record/{{li.eg_bib_id()}}">
+ <span class="material-icons small mr-1">library_books</span>
+ <span i18n>Catalog</span>
+ </a>
+ </ng-container>
+
+ <ng-container *ngIf="!poId && li.purchase_order()">
+ <span class="ml-1 mr-1" i18n> | </span>
+ <a class="label-with-material-icon"
+ title="Purchase Order" i18n-title
+ routerLink="/staff/acq/po/{{li.purchase_order().id()}}">
+ <span class="material-icons small mr-1">center_focus_weak</span>
+ <span i18n>{{li.purchase_order().id()}}</span>
+ </a>
+ </ng-container>
+ <ng-container *ngIf="!picklistId && li.picklist()">
+ <span class="ml-1 mr-1" i18n> | </span>
+ <a class="label-with-material-icon"
+ title="Selection List" i18n-title
+ routerLink="/staff/acq/picklist/{{li.picklist().id()}}">
+ <span class="material-icons small mr-1">widgets</span>
+ <span i18n>{{li.picklist().name()}}</span>
+ </a>
+ </ng-container>
+ <ng-container *ngIf="li.provider()">
+ <span class="ml-1 mr-1" i18n> | </span>
+ <a class="label-with-material-icon"
+ title="Selection List" i18n-title
+ routerLink="/staff/acq/provider/{{li.provider().id()}}/details">
+ <span class="material-icons small mr-1">store</span>
+ <span i18n>{{li.provider().name()}}</span>
+ </a>
+ </ng-container>
+ </div>
+ </div>
+ </div>
+
+ <!-- actions along the right -->
+ <div class="d-flex flex-column justify-content-end">
+ <div class="row">
+ <div class="col-lg-12 d-flex">
+ <div class="flex-1"> </div>
+ <!-- w-auto allows the input group to stick to the right
+ as the status label grows -->
+ <div class="input-group w-auto">
+ <div class="input-group-prepend">
+ <div ngbDropdown>
+ <button class="btn btn-outline-dark btn-sm" ngbDropdownToggle
+ title="Order Identifier Type" i18n-title
+ [ngClass]="{'btn-warning': !selectedIdent(li)}">
+ <ng-container *ngIf="orderIdentTypes[li.id()]=='isbn'" i18n>ISBN</ng-container>
+ <ng-container *ngIf="orderIdentTypes[li.id()]=='upc'" i18n>UPC</ng-container>
+ <ng-container *ngIf="orderIdentTypes[li.id()]=='issn'" i18n>ISSN</ng-container>
+ </button>
+ <div ngbDropdownMenu>
+ <button class="btn-sm" ngbDropdownItem
+ (click)="orderIdentTypes[li.id()]='isbn'" i18n>ISBN</button>
+ <button class="btn-sm" ngbDropdownItem
+ (click)="orderIdentTypes[li.id()]='upc'" i18n>UPC</button>
+ <button class="btn-sm" ngbDropdownItem
+ (click)="orderIdentTypes[li.id()]='issn'" i18n>ISSN</button>
+ </div>
+ </div>
+ </div>
+ <eg-combobox [entries]="identOptions(li)" [smallFormControl]="true"
+ placeholder="Order Identifer..." i18n-placeholder
+ [allowFreeText]="true" [selectedId]="selectedIdent(li)"
+ (onChange)="orderIdentChanged(li, $event)">
+ </eg-combobox>
+ </div>
+ </div>
+ </div>
+ <div class="row mt-2">
+ <div class="col-lg-12 d-flex">
+ <div class="flex-1"></div>
+ <div class="mr-2">
+ <ng-container [ngSwitch]="li.state()">
+ <div i18n
+ class="p-1 text-dark border border-dark bg-light rounded-lg"
+ *ngSwitchCase="'new'">New</div>
+ <div i18n
+ class="p-1 text-dark border border-dark bg-light rounded-lg"
+ *ngSwitchCase="'selector-ready'">Selector-Ready</div>
+ <div i18n
+ class="p-1 text-dark border border-dark bg-light rounded-lg"
+ *ngSwitchCase="'order-ready'">Order-Ready</div>
+ <div i18n
+ class="p-1 text-dark border border-dark bg-light rounded-lg"
+ *ngSwitchCase="'approved'">Approved</div>
+ <div i18n
+ class="p-1 text-dark border border-dark bg-light rounded-lg"
+ *ngSwitchCase="'pending-order'">Pending-Order</div>
+ <div i18n
+ class="p-1 text-primary border border-primary bg-light rounded-lg"
+ *ngSwitchCase="'on-order'">On-Order</div>
+ <div i18n
+ class="p-1 text-success border border-success bg-light rounded-lg"
+ *ngSwitchCase="'received'">Received</div>
+ <div i18n
+ class="p-1 text-danger border border-danger bg-light rounded-lg"
+ *ngSwitchCase="'cancelled'">Canceled</div>
+ </ng-container>
+ </div>
+ <div class="mr-2">
+ <div ngbDropdown>
+ <button class="btn btn-info btn-sm" ngbDropdownToggle i18n>Actions</button>
+ <div ngbDropdownMenu>
+ <button ngbDropdownItem [disabled]="li.state() != 'on-order'"
+ (click)="markReceived([li.id()])" i18n>Mark Received</button>
+ <button ngbDropdownItem [disabled]="li.state() != 'received'"
+ (click)="markUnReceived([li.id()])" i18n>Mark Un-Received</button>
+ <button ngbDropdownItem [disabled]="!liHasRealCopies(li)"
+ (click)="editHoldings(li)" i18n>Holdings Maintenance</button>
+ <a ngbDropdownItem routerLink="lineitem/{{li.id()}}/history"
+ queryParamsHandling="merge" i18n>View History</a>
+ </div>
+ </div>
+ </div>
+ <div>
+ <input type="text" class="form-control-sm medium"
+ [ngClass]="{'border border-danger text-danger': !liPriceIsValid(li)}"
+ placeholder='Price...' i18n-placeholder
+ (change)="liPriceChange(li)" [ngModel]="li.estimated_unit_price()"
+ (ngModelChange)="li.estimated_unit_price($event)"/>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <div class="row" *ngIf="showNotesFor == li.id()">
+ <div class="col-lg-10 offset-lg-1 p-2 mt-2">
+ <eg-lineitem-notes [lineitem]="li" (closeRequested)="showNotesFor = null">
+ </eg-lineitem-notes>
+ </div>
+ </div>
+ <div class="row" *ngIf="showExpandFor == li.id() || expandAll">
+ <div class="col-lg-10 offset-lg-1 p-2 mt-2 shadow">
+
+ <!-- Note the flex values are set so they also match the layout
+ of the list of copies in the copies component. -->
+ <div class="div d-flex font-weight-bold">
+ <div class="flex-1 p-1" i18n>Owning Branch</div>
+ <div class="flex-1 p-1" i18n>Copy Location</div>
+ <div class="flex-1 p-1" i18n>Collection Code</div>
+ <div class="flex-1 p-1" i18n>Fund</div>
+ <div class="flex-1 p-1" i18n>Circ Modifier</div>
+ <div class="flex-1 p-1" i18n>Callnumber</div>
+ <div class="flex-1 p-1" i18n>Barcode</div>
+ </div>
+ <div class="batch-copy-row" *ngFor="let copy of li.lineitem_details()">
+ <eg-lineitem-copy-attrs [embedded]="true" [copy]="copy">
+ </eg-lineitem-copy-attrs>
+ </div>
+ </div>
+ </div>
+</ng-container>
+
+<div class="row" *ngIf="loading">
+ <div class="offset-lg-3 col-lg-6">
+ <eg-progress-inline *ngIf="loading"></eg-progress-inline>
+ </div>
+</div>
+
--- /dev/null
+import {Component, OnInit, Input, Output, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {Observable} from 'rxjs';
+import {tap} from 'rxjs/operators';
+import {Pager} from '@eg/share/util/pager';
+import {EgEvent, EventService} from '@eg/core/event.service';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {LineitemService} from './lineitem.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
+import {CancelDialogComponent} from './cancel-dialog.component';
+
+@Component({
+ templateUrl: 'lineitem-list.component.html',
+ selector: 'eg-lineitem-list',
+ styleUrls: ['lineitem-list.component.css']
+})
+export class LineitemListComponent implements OnInit {
+
+ picklistId: number = null;
+ poId: number = null;
+
+ loading = false;
+ pager: Pager = new Pager();
+ pageOfLineitems: IdlObject[] = [];
+ lineitemIds: number[] = [];
+
+ // Selected lineitems
+ selected: {[id: number]: boolean} = {};
+
+ // Order identifier type per lineitem
+ orderIdentTypes: {[id: number]: 'isbn' | 'issn' | 'upc'} = {};
+
+ // Copy counts per lineitem
+ existingCopyCounts: {[id: number]: number} = {};
+
+ // Squash these down to an easily traversable data set to avoid
+ // a lot of repetitive looping.
+ liMarcAttrs: {[id: number]: {[name: string]: IdlObject[]}} = {};
+
+ batchNote: string;
+ noteIsPublic = false;
+ batchSelectPage = false;
+ batchSelectAll = false;
+ showNotesFor: number;
+ showExpandFor: number; // 'Expand'
+ expandAll = false;
+ action = '';
+ batchFailure: EgEvent;
+ focusLi: number;
+
+ @ViewChild('cancelDialog') cancelDialog: CancelDialogComponent;
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private evt: EventService,
+ private net: NetService,
+ private auth: AuthService,
+ private store: ServerStoreService,
+ private holdings: HoldingsService,
+ private liService: LineitemService
+ ) {}
+
+ ngOnInit() {
+
+ this.route.queryParamMap.subscribe((params: ParamMap) => {
+ this.pager.offset = +params.get('offset');
+ this.pager.limit = +params.get('limit');
+ this.load();
+ });
+
+ this.route.fragment.subscribe((fragment: string) => {
+ const id = Number(fragment);
+ if (id > 0) { this.focusLineitem(id); }
+ });
+
+ this.route.parent.paramMap.subscribe((params: ParamMap) => {
+ this.picklistId = +params.get('picklistId');
+ this.poId = +params.get('poId');
+ this.load();
+ });
+
+ this.store.getItem('acq.lineitem.page_size').then(count => {
+ this.pager.setLimit(count || 20);
+ this.load();
+ });
+ }
+
+ pageSizeChange(count: number) {
+ this.store.setItem('acq.lineitem.page_size', count).then(_ => {
+ this.pager.setLimit(count);
+ this.pager.toFirst();
+ this.goToPage();
+ });
+ }
+
+ // Focus the selected lineitem, which may not yet exist in the
+ // DOM for focusing.
+ focusLineitem(id?: number) {
+ if (id !== undefined) { this.focusLi = id; }
+ if (this.focusLi) {
+ const node = document.getElementById('' + this.focusLi);
+ if (node) { node.scrollIntoView(true); }
+ }
+ }
+
+ load(): Promise<any> {
+ this.pageOfLineitems = [];
+
+ if (!this.loading &&
+ this.pager.limit && (this.poId || this.picklistId)) {
+
+ this.loading = true;
+
+ return this.loadIds()
+ .then(_ => this.loadPage())
+ .then(_ => this.loading = false)
+ .catch(_ => {}); // re-route while page is loading
+ }
+
+ // We have not collected enough data to proceed.
+ return Promise.resolve();
+
+ }
+
+ loadIds(): Promise<any> {
+ this.lineitemIds = [];
+
+ let id = this.poId;
+ let options: any = {flesh_lineitem_ids: true, li_limit: 10000};
+ let method = 'open-ils.acq.purchase_order.retrieve';
+ let handler = (po) => po.lineitems();
+
+ if (this.picklistId) {
+ id = this.picklistId;
+ options = {idlist: true, limit: 1000};
+ method = 'open-ils.acq.lineitem.picklist.retrieve.atomic';
+ handler = (ids) => ids;
+ }
+
+ return this.net.request(
+ 'open-ils.acq', method, this.auth.token(), id, options
+ ).toPromise().then(resp => {
+ const ids = handler(resp);
+
+ this.lineitemIds = ids
+ .map(i => Number(i))
+ .sort((id1, id2) => id1 < id2 ? -1 : 1);
+
+ this.pager.resultCount = ids.length;
+ });
+ }
+
+ goToPage() {
+ this.focusLi = null;
+ this.router.navigate([], {
+ relativeTo: this.route,
+ queryParamsHandling: 'merge',
+ fragment: null,
+ queryParams: {
+ offset: this.pager.offset,
+ limit: this.pager.limit
+ }
+ });
+ }
+
+ loadPage(): Promise<any> {
+ return this.jumpToLiPage()
+ .then(_ => this.loadPageOfLis())
+ .then(_ => this.setBatchSelect())
+ .then(_ => setTimeout(() => this.focusLineitem()));
+ }
+
+ jumpToLiPage(): Promise<boolean> {
+ if (!this.focusLi) { return Promise.resolve(true); }
+
+ const idx = this.lineitemIds.indexOf(this.focusLi);
+ if (idx === -1) { return Promise.resolve(true); }
+
+ const offset = Math.floor(idx / this.pager.limit) * this.pager.limit;
+
+ return this.router.navigate(['./'], {
+ relativeTo: this.route,
+ queryParams: {offset: offset, limit: this.pager.limit},
+ fragment: '' + this.focusLi
+ });
+ }
+
+ loadPageOfLis(): Promise<any> {
+ this.pageOfLineitems = [];
+
+ const ids = this.lineitemIds.slice(
+ this.pager.offset, this.pager.offset + this.pager.limit)
+ .filter(id => id !== undefined);
+
+ if (ids.length === 0) { return Promise.resolve(); }
+
+ if (this.pageOfLineitems.length === ids.length) {
+ // All entries found in the cache
+ return Promise.resolve();
+ }
+
+ this.pageOfLineitems = []; // reset
+
+ return this.liService.getFleshedLineitems(
+ ids, {fromCache: true, toCache: true})
+ .pipe(tap(struct => {
+ this.ingestOneLi(struct.lineitem);
+ this.existingCopyCounts[struct.id] = struct.existing_copies;
+ })).toPromise();
+ }
+
+ ingestOneLi(li: IdlObject, replace?: boolean) {
+ this.liMarcAttrs[li.id()] = {};
+
+ li.attributes().forEach(attr => {
+ const name = attr.attr_name();
+ this.liMarcAttrs[li.id()][name] =
+ this.liService.getAttributes(
+ li, name, 'lineitem_marc_attr_definition');
+ });
+
+ const ident = this.liService.getOrderIdent(li);
+ this.orderIdentTypes[li.id()] = ident ? ident.attr_name() : 'isbn';
+
+ // newest to oldest
+ li.lineitem_notes(li.lineitem_notes().sort(
+ (n1, n2) => n1.create_time() < n2.create_time() ? 1 : -1));
+
+ if (replace) {
+ for (let idx = 0; idx < this.pageOfLineitems.length; idx++) {
+ if (this.pageOfLineitems[idx].id() === li.id()) {
+ this.pageOfLineitems[idx] = li;
+ break;
+ }
+ }
+ } else {
+ this.pageOfLineitems.push(li);
+ }
+ }
+
+ // First matching attr
+ displayAttr(li: IdlObject, name: string): string {
+ return (
+ this.liMarcAttrs[li.id()][name] &&
+ this.liMarcAttrs[li.id()][name][0]
+ ) ? this.liMarcAttrs[li.id()][name][0].attr_value() : '';
+ }
+
+ // All matching attrs
+ attrs(li: IdlObject, name: string, attrType?: string): IdlObject[] {
+ return this.liService.getAttributes(li, name, attrType);
+ }
+
+ jacketIdent(li: IdlObject): string {
+ return this.displayAttr(li, 'isbn') || this.displayAttr(li, 'upc');
+ }
+
+ // Order ident options are pulled from the MARC, but the ident
+ // value proper is stored as a local attr def.
+ identOptions(li: IdlObject): ComboboxEntry[] {
+ const otype = this.orderIdentTypes[li.id()];
+
+ if (this.liMarcAttrs[li.id()][otype]) {
+ return this.liMarcAttrs[li.id()][otype].map(
+ attr => ({id: attr.id(), label: attr.attr_value()}));
+ }
+
+ return [];
+ }
+
+ // Returns the MARC attr with the same type and value as the applied
+ // order identifier (which is a local attr)
+ selectedIdent(li: IdlObject): number {
+ const ident = this.liService.getOrderIdent(li);
+ if (!ident) { return null; }
+
+ const attr = this.identOptions(li).filter(
+ (entry: ComboboxEntry) => entry.label === ident.attr_value())[0];
+ return attr ? attr.id : null;
+ }
+
+ currentIdent(li: IdlObject): IdlObject {
+ return this.liService.getOrderIdent(li);
+ }
+
+ orderIdentChanged(li: IdlObject, entry: ComboboxEntry) {
+ if (entry === null) { return; }
+
+ this.liService.changeOrderIdent(
+ li, entry.id, this.orderIdentTypes[li.id()], entry.label
+ ).subscribe(freshLi => this.ingestOneLi(freshLi, true));
+ }
+
+ addBriefRecord() {
+ }
+
+ selectedIds(): number[] {
+ return Object.keys(this.selected)
+ .filter(id => this.selected[id] === true)
+ .map(id => Number(id));
+ }
+
+
+ // After a page of LI's are loaded, see if the batch-select checkbox
+ // needs to be on or off.
+ setBatchSelect() {
+ let on = true;
+ const ids = this.selectedIds();
+ this.pageOfLineitems.forEach(li => {
+ if (!ids.includes(li.id())) { on = false; }
+ });
+
+ this.batchSelectPage = on;
+
+ on = true;
+
+ this.lineitemIds.forEach(id => {
+ if (!this.selected[id]) { on = false; }
+ });
+
+ this.batchSelectAll = on;
+ }
+
+ toggleSelectAll(allItems: boolean) {
+
+ if (allItems) {
+ this.lineitemIds.forEach(
+ id => this.selected[id] = this.batchSelectAll);
+
+ this.batchSelectPage = this.batchSelectAll;
+
+ } else {
+
+ this.pageOfLineitems.forEach(
+ li => this.selected[li.id()] = this.batchSelectPage);
+
+ if (!this.batchSelectPage) {
+ // When deselecting items in the page, we're no longer
+ // selecting all items.
+ this.batchSelectAll = false;
+ }
+ }
+ }
+
+ applyBatchNote() {
+ const ids = this.selectedIds();
+ if (ids.length === 0 || !this.batchNote) { return; }
+
+ this.liService.applyBatchNote(ids, this.batchNote, this.noteIsPublic)
+ .then(resp => this.load());
+ }
+
+ liPriceIsValid(li: IdlObject): boolean {
+ const price = li.estimated_unit_price();
+ if (price === null || price === undefined || price === '') {
+ return true;
+ }
+ return !Number.isNaN(Number(price)) && Number(price) >= 0;
+ }
+
+ liPriceChange(li: IdlObject) {
+ const price = li.estimated_unit_price();
+ if (this.liPriceIsValid(li)) {
+ li.estimated_unit_price(Number(price).toFixed(2));
+
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.lineitem.update',
+ this.auth.token(), li
+ ).subscribe(resp =>
+ this.liService.activateStateChange.emit(li.id()));
+ }
+ }
+
+ toggleShowNotes(liId: number) {
+ this.showExpandFor = null;
+ this.showNotesFor = this.showNotesFor === liId ? null : liId;
+ }
+
+ toggleShowExpand(liId: number) {
+ this.showNotesFor = null;
+ this.showExpandFor = this.showExpandFor === liId ? null : liId;
+ }
+
+ toggleExpandAll() {
+ this.showNotesFor = null;
+ this.showExpandFor = null;
+ this.expandAll = !this.expandAll;
+ }
+
+ liHasAlerts(li: IdlObject): boolean {
+ return li.lineitem_notes().filter(n => n.alert_text()).length > 0;
+ }
+
+ deleteLineitems() {
+ const ids = Object.keys(this.selected).filter(id => this.selected[id]);
+
+ const method = this.poId ?
+ 'open-ils.acq.purchase_order.lineitem.delete' :
+ 'open-ils.acq.picklist.lineitem.delete';
+
+ let promise = Promise.resolve();
+
+ this.loading = true;
+
+ ids.forEach(id => {
+ promise = promise
+ .then(_ => this.net.request(
+ 'open-ils.acq', method, this.auth.token(), id).toPromise()
+ );
+ });
+
+ promise.then(_ => this.load());
+ }
+
+ liHasRealCopies(li: IdlObject): boolean {
+ for (let idx = 0; idx < li.lineitem_details().length; idx++) {
+ if (li.lineitem_details()[idx].eg_copy_id()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ editHoldings(li: IdlObject) {
+
+ const copies = li.lineitem_details()
+ .filter(lid => lid.eg_copy_id()).map(lid => lid.eg_copy_id());
+
+ if (copies.length === 0) { return; }
+
+ this.holdings.spawnAddHoldingsUi(
+ li.eg_bib_id(),
+ copies.map(c => c.call_number()),
+ null,
+ copies.map(c => c.id())
+ );
+ }
+
+ receiveSelected() {
+ this.markReceived(this.selectedIds());
+ }
+
+ unReceiveSelected() {
+ this.markUnReceived(this.selectedIds());
+ }
+
+ cancelSelected() {
+ const liIds = this.selectedIds();
+ if (liIds.length === 0) { return; }
+
+ this.cancelDialog.open().subscribe(reason => {
+ if (!reason) { return; }
+
+ this.net.request('open-ils.acq',
+ 'open-ils.acq.lineitem.cancel.batch',
+ this.auth.token(), liIds, reason
+ ).toPromise().then(resp => this.postBatchAction(resp, liIds));
+ });
+ }
+
+ markReceived(liIds: number[]) {
+ if (liIds.length === 0) { return; }
+
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.lineitem.receive.batch',
+ this.auth.token(), liIds
+ ).toPromise().then(resp => this.postBatchAction(resp, liIds));
+ }
+
+ markUnReceived(liIds: number[]) {
+ if (liIds.length === 0) { return; }
+
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.lineitem.receive.rollback.batch',
+ this.auth.token(), liIds
+ ).toPromise().then(resp => this.postBatchAction(resp, liIds));
+ }
+
+ postBatchAction(response: any, liIds: number[]) {
+ const evt = this.evt.parse(response);
+
+ if (evt) {
+ console.warn('Batch operation failed', evt);
+ this.batchFailure = evt;
+ return;
+ }
+
+ this.batchFailure = null;
+
+ // Remove the modified LI's from the cache so we are
+ // forced to re-fetch them.
+ liIds.forEach(id => delete this.liService.liCache[id]);
+
+ this.loadPageOfLis();
+ }
+
+ createPo(fromAll?: boolean) {
+ this.router.navigate(['/staff/acq/po/create'], {
+ queryParams: {li: fromAll ? this.lineitemIds : this.selectedIds()}
+ });
+ }
+}
+
--- /dev/null
+
+<router-outlet></router-outlet>
+
--- /dev/null
+import {Component, OnInit} from '@angular/core';
+
+@Component({
+ templateUrl: 'lineitem.component.html'
+})
+export class LineitemComponent implements OnInit {
+ ngOnInit() {}
+}
+
--- /dev/null
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {HttpClientModule} from '@angular/common/http';
+import {ItemLocationSelectModule
+ } from '@eg/share/item-location-select/item-location-select.module';
+import {LineitemWorksheetComponent} from './worksheet.component';
+import {LineitemService} from './lineitem.service';
+import {LineitemComponent} from './lineitem.component';
+import {LineitemNotesComponent} from './notes.component';
+import {LineitemDetailComponent} from './detail.component';
+import {LineitemOrderSummaryComponent} from './order-summary.component';
+import {LineitemListComponent} from './lineitem-list.component';
+import {LineitemCopiesComponent} from './copies.component';
+import {LineitemBatchCopiesComponent} from './batch-copies.component';
+import {LineitemCopyAttrsComponent} from './copy-attrs.component';
+import {LineitemHistoryComponent} from './history.component';
+import {BriefRecordComponent} from './brief-record.component';
+import {CancelDialogComponent} from './cancel-dialog.component';
+import {MarcEditModule} from '@eg/staff/share/marc-edit/marc-edit.module';
+
+@NgModule({
+ declarations: [
+ LineitemComponent,
+ LineitemListComponent,
+ LineitemNotesComponent,
+ LineitemDetailComponent,
+ LineitemCopiesComponent,
+ LineitemOrderSummaryComponent,
+ LineitemBatchCopiesComponent,
+ LineitemCopyAttrsComponent,
+ LineitemHistoryComponent,
+ CancelDialogComponent,
+ BriefRecordComponent,
+ LineitemWorksheetComponent
+ ],
+ exports: [
+ LineitemListComponent,
+ CancelDialogComponent
+ ],
+ imports: [
+ StaffCommonModule,
+ ItemLocationSelectModule,
+ MarcEditModule
+ ],
+ providers: [
+ LineitemService
+ ]
+})
+
+export class LineitemModule {
+}
--- /dev/null
+import {Injectable, EventEmitter} from '@angular/core';
+import {Observable, from, concat, empty} from 'rxjs';
+import {switchMap, map, tap, merge} from 'rxjs/operators';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {ItemLocationService} from '@eg/share/item-location-select/item-location-select.service';
+
+export interface BatchLineitemStruct {
+ id: number;
+ lineitem: IdlObject;
+ existing_copies: number;
+ all_locations: IdlObject[];
+ all_funds: IdlObject[];
+ all_circ_modifiers: IdlObject[];
+}
+
+export interface BatchLineitemUpdateStruct {
+ lineitem: IdlObject;
+ lid: number;
+ max: number;
+ progress: number;
+ complete: number; // Perl bool
+ total: number;
+ [key: string]: any; // Perl Acq::BatchManager
+}
+
+interface FleshedLineitemParams {
+
+ // Flesh data beyond the default.
+ fleshMore?: any;
+
+ // OK to pull the requested LI from the cache.
+ fromCache?: boolean;
+
+ // OK to add this LI to the cache.
+ // Generally a good thing, but if you are fetching an LI with
+ // fewer fleshed fields than the default, this could break code.
+ toCache?: boolean;
+}
+
+@Injectable()
+export class LineitemService {
+
+ liAttrDefs: IdlObject[];
+
+ // Emitted when our copy batch editor wants to apply a value
+ // to a set of inputs. This allows the the copy input comboboxes, etc.
+ // to add the entry before it's forced to grab the value from the
+ // server, often in large parallel batches.
+ batchOptionWanted: EventEmitter<{[field: string]: ComboboxEntry}>
+ = new EventEmitter<{[field: string]: ComboboxEntry}> ();
+
+ // Emits a LI ID if the LI was edited in a way that could impact
+ // its activatability of its linked PO.
+ activateStateChange: EventEmitter<number> = new EventEmitter<number>();
+
+ // Cache for circ modifiers and funds; locations are cached in the
+ // item location select service.
+ circModCache: {[code: string]: IdlObject} = {};
+ fundCache: {[id: number]: IdlObject} = {};
+ liCache: {[id: number]: BatchLineitemStruct} = {};
+
+ // Alerts the user has already confirmed are OK.
+ alertAcks: {[id: number]: boolean} = {};
+
+ constructor(
+ private idl: IdlService,
+ private net: NetService,
+ private auth: AuthService,
+ private pcrud: PcrudService,
+ private loc: ItemLocationService
+ ) {}
+
+ getFleshedLineitems(ids: number[],
+ params: FleshedLineitemParams = {}): Observable<BatchLineitemStruct> {
+
+ if (params.fromCache) {
+ const fromCache = this.getLineitemsFromCache(ids);
+ if (fromCache) { return from(fromCache); }
+ }
+
+ const flesh: any = Object.assign({
+ flesh_attrs: true,
+ flesh_provider: true,
+ flesh_order_summary: true,
+ flesh_cancel_reason: true,
+ flesh_li_details: true,
+ flesh_notes: true,
+ flesh_fund: true,
+ flesh_circ_modifier: true,
+ flesh_location: true,
+ flesh_fund_debit: true,
+ flesh_po: true,
+ flesh_pl: true,
+ flesh_formulas: true,
+ flesh_copies: true,
+ clear_marc: false
+ }, params.fleshMore || {});
+
+ return this.net.request(
+ 'open-ils.acq', 'open-ils.acq.lineitem.retrieve.batch',
+ this.auth.token(), ids, flesh
+ ).pipe(tap(liStruct =>
+ this.ingestLineitem(liStruct, params.toCache)));
+ }
+
+ getLineitemsFromCache(ids: number[]): BatchLineitemStruct[] {
+
+ const fromCache: BatchLineitemStruct[] = [];
+
+ ids.forEach(id => {
+ if (this.liCache[id]) { fromCache.push(this.liCache[id]); }
+ });
+
+ // Only return LI's from cache if all of the requested LI's
+ // are cached, otherwise they would be returned in the wrong
+ // order. Typically it will be all or none so I'm not
+ // fussing with interleaving cached and uncached lineitems
+ // to fix the sorting.
+ if (fromCache.length === ids.length) { return fromCache; }
+
+ return null;
+ }
+
+ ingestLineitem(
+ liStruct: BatchLineitemStruct, toCache: boolean): BatchLineitemStruct {
+
+ const li = liStruct.lineitem;
+
+ // These values come through as NULL
+ const summary = li.order_summary();
+ if (!summary.estimated_amount()) { summary.estimated_amount(0); }
+ if (!summary.encumbrance_amount()) { summary.encumbrance_amount(0); }
+ if (!summary.paid_amount()) { summary.paid_amount(0); }
+
+ // Sort the formula applications
+ li.distribution_formulas(
+ li.distribution_formulas().sort((f1, f2) =>
+ f1.create_time() < f2.create_time() ? -1 : 1)
+ );
+
+ // consistent sorting
+ li.lineitem_details(
+ li.lineitem_details().sort((d1, d2) =>
+ d1.id() < d2.id() ? -1 : 1)
+ );
+
+ // De-flesh some values we don't want living directly on
+ // the copy. Cache the values so our comboboxes, etc.
+ // can use them without have to re-fetch them .
+ li.lineitem_details().forEach(copy => {
+ let val;
+ if ((val = copy.circ_modifier())) { // assignment
+ this.circModCache[val.code()] = copy.circ_modifier();
+ copy.circ_modifier(val.code());
+ }
+ if ((val = copy.fund())) {
+ this.fundCache[val.id()] = copy.fund();
+ copy.fund(val.id());
+ }
+ if ((val = copy.location())) {
+ this.loc.locationCache[val.id()] = copy.location();
+ copy.location(val.id());
+ }
+ });
+
+ if (toCache) { this.liCache[li.id()] = liStruct; }
+ return liStruct;
+ }
+
+ // Returns all matching attributes
+ // 'li' should be fleshed with attributes()
+ getAttributes(li: IdlObject, name: string, attrType?: string): IdlObject[] {
+ const values: IdlObject[] = [];
+ li.attributes().forEach(attr => {
+ if (attr.attr_name() === name) {
+ if (!attrType || attrType === attr.attr_type()) {
+ values.push(attr);
+ }
+ }
+ });
+
+ return values;
+ }
+
+ getAttributeValues(li: IdlObject, name: string, attrType?: string): string[] {
+ return this.getAttributes(li, name, attrType).map(attr => attr.attr_value());
+ }
+
+ // Returns the first matching attribute
+ // 'li' should be fleshed with attributes()
+ getFirstAttribute(li: IdlObject, name: string, attrType?: string): IdlObject {
+ return this.getAttributes(li, name, attrType)[0];
+ }
+
+ getFirstAttributeValue(li: IdlObject, name: string, attrType?: string): string {
+ const attr = this.getFirstAttribute(li, name, attrType);
+ return attr ? attr.attr_value() : '';
+ }
+
+ getOrderIdent(li: IdlObject): IdlObject {
+ for (let idx = 0; idx < li.attributes().length; idx++) {
+ const attr = li.attributes()[idx];
+ if (attr.order_ident() === 't' &&
+ attr.attr_type() === 'lineitem_local_attr_definition') {
+ return attr;
+ }
+ }
+ return null;
+ }
+
+ // Returns an updated copy of the lineitem
+ changeOrderIdent(li: IdlObject,
+ id: number, attrType: string, attrValue: string): Observable<IdlObject> {
+
+ const args: any = {lineitem_id: li.id()};
+
+ if (id) {
+ // Order ident set to an existing attribute.
+ args.source_attr_id = id;
+ } else {
+ // Order ident set to a new free text value
+ args.attr_name = attrType;
+ args.attr_value = attrValue;
+ }
+
+ return this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.lineitem.order_identifier.set',
+ this.auth.token(), args
+ ).pipe(switchMap(_ => this.getFleshedLineitems([li.id()]))
+ ).pipe(map(struct => struct.lineitem));
+ }
+
+ applyBatchNote(liIds: number[],
+ noteValue: string, vendorPublic: boolean): Promise<any> {
+
+ if (!noteValue || liIds.length === 0) { return Promise.resolve(); }
+
+ const notes = [];
+ liIds.forEach(id => {
+ const note = this.idl.create('acqlin');
+ note.isnew(true);
+ note.lineitem(id);
+ note.value(noteValue);
+ note.vendor_public(vendorPublic ? 't' : 'f');
+ notes.push(note);
+ });
+
+ return this.net.request('open-ils.acq',
+ 'open-ils.acq.lineitem_note.cud.batch',
+ this.auth.token(), notes
+ ).pipe(tap(resp => {
+ if (resp && resp.note) {
+ const li = this.liCache[resp.note.lineitem()].lineitem;
+ li.lineitem_notes().unshift(resp.note);
+ }
+ })).toPromise();
+ }
+
+ getLiAttrDefs(): Promise<IdlObject[]> {
+ if (this.liAttrDefs) {
+ return Promise.resolve(this.liAttrDefs);
+ }
+
+ return this.pcrud.retrieveAll('acqliad', {}, {atomic: true})
+ .toPromise().then(defs => this.liAttrDefs = defs);
+ }
+
+ updateLiDetails(li: IdlObject): Observable<BatchLineitemUpdateStruct> {
+ const lids = li.lineitem_details().filter(copy =>
+ (copy.isnew() || copy.ischanged() || copy.isdeleted()));
+
+ return this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.lineitem_detail.cud.batch', this.auth.token(), lids);
+ }
+
+ updateLineitems(lis: IdlObject[]): Observable<BatchLineitemUpdateStruct> {
+
+ // Fire updates one LI at a time. Note the API allows passing
+ // multiple LI's, but does not stream responses. This approach
+ // allows the caller to get a stream of responses instead of a
+ // final "all done".
+ let obs: Observable<any> = empty();
+ lis.forEach(li => {
+ obs = concat(obs, this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.lineitem.update',
+ this.auth.token(), li
+ ));
+ });
+
+ return obs;
+ }
+}
+
--- /dev/null
+
+<div class="shadow border-top w-100">
+
+ <div class="p-1 m-1 form-inline">
+ <input type="text" class="form-control form-control-sm" id="note-text-input"
+ [(ngModel)]="noteText" placeholder="Note Text" i18n-placeholder/>
+ <div class="form-check ml-2">
+ <input class="form-check-input" type="checkbox"
+ [(ngModel)]="vendorPublic" id="vendor-public-cbox">
+ <label class="form-check-label" for="vendor-public-cbox" i18n>
+ Vendor Public
+ </label>
+ </div>
+ <button class="btn btn-sm btn-success ml-2" [disabled]="!noteText"
+ (click)="newNote()" i18n>New Note</button>
+ <span class="ml-2">
+ <eg-combobox idlClass="acqliat" [(ngModel)]="alertEntry"
+ [asyncSupportsEmptyTermClick]="true">
+ </eg-combobox>
+ </span>
+ <button class="btn btn-sm btn-info ml-2" [disabled]="!alertEntry"
+ (click)="newNote(true)" i18n>New Alert</button>
+ <a class="ml-auto" href="javascript:;" (click)="close()" title="Close" i18n-title>
+ <span class="material-icons text-danger">close</span>
+ </a>
+ </div>
+
+ <div *ngFor="let note of lineitem.lineitem_notes()">
+ <div class="d-flex m-1 p-2 border">
+ <div class="flex-1 p-1">
+ <ng-container *ngIf="note.vendor_public() == 't'">
+ <div class="text-primary" i18n>VENDOR PUBLIC</div>
+ </ng-container>
+ <ng-container *ngIf="note.alert_text()">
+ <span class="text-danger" i18n>
+ [{{orgSn(note.alert_text().owning_lib())}}] {{note.alert_text().code()}}
+ </span>
+ </ng-container>
+ </div>
+ <div class="flex-5 ml-2 p-1">{{note.value()}}</div>
+ <div class="ml-2 p-1">{{note.create_time() | date:'short'}}</div>
+ <div class="ml-2 p-1">
+ <a href="javascript:;" class="text-danger"
+ (click)="deleteNote(note)" i18n>Delete</a>
+ </div>
+ </div>
+ </div>
+</div>
+
+
+
--- /dev/null
+import {Component, OnInit, AfterViewInit, Input, Output, EventEmitter} from '@angular/core';
+import {Observable} from 'rxjs';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+@Component({
+ templateUrl: 'notes.component.html',
+ selector: 'eg-lineitem-notes'
+})
+export class LineitemNotesComponent implements OnInit, AfterViewInit {
+
+ @Input() lineitem: IdlObject;
+ noteText: string;
+ vendorPublic = false;
+ alertEntry: ComboboxEntry;
+
+ @Output() closeRequested: EventEmitter<void> = new EventEmitter<void>();
+
+ constructor(
+ private idl: IdlService,
+ private org: OrgService,
+ private auth: AuthService,
+ private net: NetService
+ ) {}
+
+ ngOnInit() {
+ }
+
+ ngAfterViewInit() {
+ const node = document.getElementById('note-text-input');
+ if (node) { node.focus(); }
+ }
+
+ orgSn(id: number): string {
+ return this.org.get(id).shortname();
+ }
+
+ close() {
+ this.closeRequested.emit();
+ }
+
+ newNote(isAlert?: boolean) {
+ const note = this.idl.create('acqlin');
+ note.isnew(true);
+ note.lineitem(this.lineitem.id());
+ note.value(this.noteText || '');
+ if (isAlert) {
+ note.alert_text(this.alertEntry.id);
+ } else {
+ note.vendor_public(this.vendorPublic ? 't' : 'f');
+ }
+
+ this.modifyNotes(note).subscribe(resp => {
+ if (resp.note) {
+ this.lineitem.lineitem_notes().unshift(resp.note);
+ }
+ });
+ }
+
+ deleteNote(note: IdlObject) {
+ note.isdeleted(true);
+ this.modifyNotes(note).toPromise().then(_ => {
+ this.lineitem.lineitem_notes(
+ this.lineitem.lineitem_notes().filter(n => n.id() !== note.id())
+ );
+ });
+ }
+
+ modifyNotes(notes: IdlObject | IdlObject[]): Observable<any> {
+ notes = [].concat(notes);
+
+ return this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.lineitem_note.cud.batch',
+ this.auth.token(), notes);
+ }
+}
+
--- /dev/null
+<ng-container *ngIf="li">
+ <span i18n>{{li.order_summary().item_count()}} Items</span>
+ <span class="ml-1 mr-1" i18n> | </span>
+ <span i18n>{{li.order_summary().recv_count()}} Received</span>
+ <span class="ml-1 mr-1" i18n> | </span>
+ <span i18n>{{li.order_summary().invoice_count()}} Invoiced</span>
+ <span class="ml-1 mr-1" i18n> | </span>
+ <span i18n>{{li.order_summary().cancel_count()}} Canceled</span>
+ <span class="ml-1 mr-1" i18n> | </span>
+ <span i18n>{{li.order_summary().delay_count()}} Delayed</span>
+ <span class="ml-1 mr-1" i18n> | </span>
+ <span i18n>{{li.order_summary().estimated_amount() | currency}} Estimated</span>
+ <span class="ml-1 mr-1" i18n> | </span>
+ <span i18n>{{li.order_summary().encumbrance_amount() | currency}} Encumbered</span>
+ <span class="ml-1 mr-1" i18n> | </span>
+ <span [ngClass]="{'text-danger': paidOff()}" i18n>
+ {{li.order_summary().paid_amount() | currency}} Paid
+ </span>
+</ng-container>
+
--- /dev/null
+import {Component, OnInit, Input, Output} from '@angular/core';
+import {IdlObject} from '@eg/core/idl.service';
+
+@Component({
+ templateUrl: 'order-summary.component.html',
+ selector: 'eg-lineitem-order-summary'
+})
+export class LineitemOrderSummaryComponent {
+ @Input() li: IdlObject;
+
+ // True if at least one item has been invoiced and all items are either
+ // invoiced or canceled.
+ paidOff(): boolean {
+ const sum = this.li.order_summary();
+ return (
+ sum.invoice_count() > 0 && (
+ sum.item_count() === (sum.invoice_count() + sum.cancel_count())
+ )
+ );
+ }
+}
+
--- /dev/null
+<div class="row">
+ <div class="p-2 m-2 ml-4">
+ <button class="btn btn-outline-dark" (click)="printWorksheet()" i18n>
+ Print Worksheet
+ </button>
+ </div>
+</div>
+
+<div id="worksheet-outlet" class="m-3 p-3 w-90 border border-dark"></div>
+
--- /dev/null
+import {Component, OnInit, AfterViewInit} from '@angular/core';
+import {map, take} from 'rxjs/operators';
+import {ActivatedRoute, ParamMap} from '@angular/router';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+import {LineitemService} from './lineitem.service';
+import {PrintService} from '@eg/share/print/print.service';
+
+@Component({
+ templateUrl: 'worksheet.component.html'
+})
+export class LineitemWorksheetComponent implements OnInit, AfterViewInit {
+
+ outlet: Element;
+ lineitemId: number;
+ lineitem: IdlObject;
+ holdCount: number;
+ printing: boolean;
+ closing: boolean;
+
+ constructor(
+ private route: ActivatedRoute,
+ private org: OrgService,
+ private net: NetService,
+ private auth: AuthService,
+ private pcrud: PcrudService,
+ private printer: PrintService,
+ private liService: LineitemService
+ ) { }
+
+ ngOnInit() {
+
+ this.route.paramMap.subscribe((params: ParamMap) => {
+ const id = +params.get('lineitemId');
+ if (id !== this.lineitemId) {
+ this.lineitemId = id;
+ if (id) { this.load(); }
+ }
+ });
+ }
+
+ ngAfterViewInit() {
+ this.outlet = document.getElementById('worksheet-outlet');
+ }
+
+ load() {
+ if (!this.lineitemId) { return; }
+
+ this.net.request(
+ 'open-ils.acq', 'open-ils.acq.lineitem.retrieve',
+ this.auth.token(), this.lineitemId, {
+ flesh_attrs: true,
+ flesh_notes: true,
+ flesh_cancel_reason: true,
+ flesh_li_details: true,
+ flesh_fund: true,
+ flesh_li_details_copy: true,
+ flesh_li_details_location: true,
+ flesh_li_details_receiver: true,
+ distribution_formulas: true
+ }
+ ).toPromise()
+ .then(li => this.lineitem = li)
+ .then(_ => this.getRemainingData())
+ .then(_ => this.populatePreview());
+ }
+
+ getRemainingData(): Promise<any> {
+
+ // Flesh owning lib
+ this.lineitem.lineitem_details().forEach(lid => {
+ lid.owning_lib(this.org.get(lid.owning_lib()));
+ });
+
+ return this.net.request(
+ 'open-ils.circ',
+ 'open-ils.circ.bre.holds.count', this.lineitem.eg_bib_id()
+ ).toPromise().then(count => this.holdCount = count);
+
+ }
+
+ populatePreview(): Promise<any> {
+
+ return this.printer.compileRemoteTemplate({
+ templateName: 'lineitem_worksheet',
+ printContext: 'default',
+ contextData: {
+ lineitem: this.lineitem,
+ hold_count: this.holdCount
+ }
+
+ }).then(response => {
+ this.outlet.innerHTML = response.content;
+ });
+ }
+
+ printWorksheet(closeTab?: boolean) {
+
+ if (closeTab || this.closing) {
+ const sub: any = this.printer.printJobQueued$.subscribe(
+ req => {
+ if (req.templateName === 'lineitem_worksheet') {
+ setTimeout(() => {
+ window.close();
+ sub.unsubscribe();
+ }, 2000); // allow for a time cushion past queueing.
+ }
+ }
+ );
+ }
+
+ this.printer.print({
+ templateName: 'lineitem_worksheet',
+ contextData: {
+ lineitem: this.lineitem,
+ hold_count: this.holdCount
+ },
+ printContext: 'default'
+ });
+ }
+}
+
--- /dev/null
+<eg-staff-banner bannerText="Selection List" i18n-bannerText></eg-staff-banner>
+
+<eg-acq-picklist-summary [picklistId]="picklistId"></eg-acq-picklist-summary>
+
+<ng-container *ngIf="!isBasePage()">
+ <div class="mt-2">
+ <a routerLink="/staff/acq/picklist/{{picklistId}}" queryParamsHandling="merge">
+ <button class="btn btn-sm btn-info" i18n>Return</button>
+ </a>
+ </div>
+</ng-container>
+
+<router-outlet></router-outlet>
+
--- /dev/null
+import {Component, OnInit} from '@angular/core';
+import {ActivatedRoute, ParamMap} from '@angular/router';
+
+/**
+ * Parent component for all Selection List sub-displays.
+ */
+
+
+@Component({
+ templateUrl: 'picklist.component.html'
+})
+export class PicklistComponent implements OnInit {
+
+ picklistId: number;
+
+ constructor(private route: ActivatedRoute) {}
+
+ ngOnInit() {
+ this.route.paramMap.subscribe((params: ParamMap) => {
+ this.picklistId = +params.get('picklistId');
+ });
+ }
+
+ isBasePage(): boolean {
+ return !this.route.firstChild ||
+ this.route.firstChild.snapshot.url.length === 0;
+ }
+}
--- /dev/null
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {CatalogCommonModule} from '@eg/share/catalog/catalog-common.module';
+import {LineitemModule} from '@eg/staff/acq/lineitem/lineitem.module';
+import {HoldingsModule} from '@eg/staff/share/holdings/holdings.module';
+import {PicklistRoutingModule} from './routing.module';
+import {PicklistComponent} from './picklist.component';
+import {PicklistSummaryComponent} from './summary.component';
+
+@NgModule({
+ declarations: [
+ PicklistComponent,
+ PicklistSummaryComponent
+ ],
+ imports: [
+ StaffCommonModule,
+ CatalogCommonModule,
+ LineitemModule,
+ HoldingsModule,
+ PicklistRoutingModule
+ ],
+ providers: []
+})
+
+export class PicklistModule {}
--- /dev/null
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {PicklistComponent} from './picklist.component';
+import {PicklistSummaryComponent} from './summary.component';
+import {LineitemListComponent} from '../lineitem/lineitem-list.component';
+import {LineitemDetailComponent} from '../lineitem/detail.component';
+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';
+
+const routes: Routes = [{
+ path: ':picklistId',
+ component: PicklistComponent,
+ children : [{
+ path: '',
+ component: LineitemListComponent
+ }, {
+ path: 'brief-record',
+ component: BriefRecordComponent
+ }, {
+ path: 'lineitem/:lineitemId/detail',
+ component: LineitemDetailComponent
+ }, {
+ path: 'lineitem/:lineitemId/history',
+ component: LineitemHistoryComponent
+ }, {
+ path: 'lineitem/:lineitemId/items',
+ component: LineitemCopiesComponent
+ }, {
+ path: 'lineitem/:lineitemId/worksheet',
+ component: LineitemWorksheetComponent
+ }]
+}];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule],
+ providers: []
+})
+
+export class PicklistRoutingModule {}
--- /dev/null
+
+<div *ngIf="picklist" class="row pt-1 pb-1 border border-secondary rounded">
+ <div class="col-lg-4 form-inline">
+ <span class="mr-2" i18n>Selection List:</span>
+ <ng-container *ngIf="editPlName">
+ <input id='pl-name-input' type="text" class="form-control"
+ [(ngModel)]="newPlName" (blur)="toggleNameEdit()"/>
+ </ng-container>
+ <ng-container *ngIf="!editPlName">
+ <a (click)="toggleNameEdit()" href='javascript:;'
+ class='font-weight-bold' i18n>{{picklist.name()}} (#{{picklist.id()}})</a>
+ </ng-container>
+ </div>
+ <div class="col-lg-2" i18n>
+ Create Date: {{picklist.create_time() | date:'shortDate'}}
+ </div>
+ <div class="col-lg-2" i18n>
+ Last Updated: {{picklist.edit_time() | date:'shortDate'}}
+ </div>
+ <div class="col-lg-2" i18n>
+ Selector: {{picklist.owner().usrname()}}
+ </div>
+ <div class="col-lg-2" i18n>
+ Entry Count: {{picklist.entry_count()}}
+ </div>
+</div>
--- /dev/null
+import {Component, Input, OnInit, AfterViewInit, ViewChild} from '@angular/core';
+import {of, Observable} from 'rxjs';
+import {tap, take, map} from 'rxjs/operators';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {FormatService} from '@eg/core/format.service';
+import {AuthService} from '@eg/core/auth.service';
+import {OrgService} from '@eg/core/org.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {StoreService} from '@eg/core/store.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {ComboboxEntry, ComboboxComponent} from '@eg/share/combobox/combobox.component';
+import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
+import {EventService} from '@eg/core/event.service';
+import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {BroadcastService} from '@eg/share/util/broadcast.service';
+
+
+@Component({
+ templateUrl: 'summary.component.html',
+ selector: 'eg-acq-picklist-summary'
+})
+export class PicklistSummaryComponent implements OnInit, AfterViewInit {
+
+ private _picklistId: number;
+ @Input() set picklistId(id: number) {
+ if (id !== this._picklistId) {
+ this._picklistId = id;
+ if (this.initDone) {
+ this.load();
+ }
+ }
+ }
+
+ get picklistId(): number {
+ return this._picklistId;
+ }
+
+ picklist: IdlObject;
+ newPlName: string;
+ editPlName = false;
+ initDone = false;
+
+ constructor(
+ private idl: IdlService,
+ private net: NetService,
+ private format: FormatService,
+ private evt: EventService,
+ private org: OrgService,
+ private pcrud: PcrudService,
+ private auth: AuthService,
+ private store: StoreService,
+ private serverStore: ServerStoreService,
+ private broadcaster: BroadcastService,
+ private holdingSvc: HoldingsService
+ ) {}
+
+ ngOnInit() {
+ this.load().then(_ => this.initDone = true);
+ }
+
+ ngAfterViewInit() {
+ }
+
+ load(): Promise<any> {
+ this.picklist = null;
+ if (!this.picklistId) { return Promise.resolve(); }
+
+ return this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.picklist.retrieve.authoritative',
+ this.auth.token(), this.picklistId,
+ {flesh_lineitem_count: true, flesh_owner: true}
+ ).toPromise().then(list => {
+
+ const evt = this.evt.parse(list);
+ if (evt) {
+ console.error('API returned ', evt);
+ return Promise.reject();
+ }
+
+ this.picklist = list;
+ });
+ }
+
+ toggleNameEdit() {
+ this.editPlName = !this.editPlName;
+
+ if (this.editPlName) {
+ this.newPlName = this.picklist.name();
+ setTimeout(() => {
+ const node =
+ document.getElementById('pl-name-input') as HTMLInputElement;
+ if (node) { node.select(); }
+ });
+
+ } else if (this.newPlName && this.newPlName !== this.picklist.name()) {
+
+ const prevName = this.picklist.name();
+ this.picklist.name(this.newPlName);
+ this.newPlName = null;
+
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.picklist.update',
+ this.auth.token(), this.picklist
+ ).subscribe(resp => {
+ const evt = this.evt.parse(resp);
+ if (evt) {
+ alert(evt);
+ this.picklist.name(prevName);
+ }
+ });
+ }
+ }
+}
--- /dev/null
+
+<h4 i18n>Direct Charges, Taxes, Fees, etc.
+ <button class="btn btn-info btn-sm" (click)="newCharge()">New Charge</button>
+</h4>
+
+<ng-container *ngIf="showBody">
+ <div class="row d-flex">
+ <div class="flex-2 p-2 font-weight-bold">Charge Type</div>
+ <div class="flex-2 p-2 font-weight-bold">Fund</div>
+ <div class="flex-2 p-2 font-weight-bold">Title/Description</div>
+ <div class="flex-2 p-2 font-weight-bold">Author</div>
+ <div class="flex-2 p-2 font-weight-bold">Note</div>
+ <div class="flex-2 p-2 font-weight-bold">Estimated Cost</div>
+ <div class="flex-2 p-2"> </div>
+ </div>
+ <div class="row mt-2 pt-2 d-flex border-top form-validated"
+ *ngFor="let charge of po().po_items()">
+ <div class="flex-2 p-2">
+ <eg-combobox idlClass="aiit" [selectedId]="charge.inv_item_type()"
+ (onChange)="charge.inv_item_type($event ? $event.id : null)"
+ i18n-placeholder placeholder="Charge Type..."
+ [required]="true" [readOnly]="!charge.isnew()"></eg-combobox>
+ </div>
+ <div class="flex-2 p-2">
+ <!-- the IDL does not require a fund, but the Perl code assumes
+ one is present -->
+ <eg-combobox idlClass="acqf" [selectedId]="charge.fund()"
+ (onChange)="charge.fund($event ? $event.id : null)"
+ i18n-placeholder placeholder="Fund..."
+ [required]="true" [readOnly]="!charge.isnew()"></eg-combobox>
+ </div>
+ <div class="flex-2 p-2">
+ <span *ngIf="!charge.isnew()">{{charge.title()}}</span>
+ <input *ngIf="charge.isnew()" type="text" class="form-control"
+ i18n-placeholder placeholder="Title..."
+ [ngModel]="charge.title()" (ngModelChange)="charge.title($event)"/>
+ </div>
+ <div class="flex-2 p-2">
+ <span *ngIf="!charge.isnew()">{{charge.author()}}</span>
+ <input *ngIf="charge.isnew()" type="text" class="form-control"
+ i18n-placeholder placeholder="Author..."
+ [ngModel]="charge.author()" (ngModelChange)="charge.author($event)"/>
+ </div>
+ <div class="flex-2 p-2">
+ <span *ngIf="!charge.isnew()">{{charge.note()}}</span>
+ <input *ngIf="charge.isnew()" type="text" class="form-control"
+ i18n-placeholder placeholder="Note..."
+ [ngModel]="charge.note()" (ngModelChange)="charge.note($event)"/>
+ </div>
+ <div class="flex-2 p-2">
+ <span *ngIf="!charge.isnew()">{{charge.estimated_cost() | currency}}</span>
+ <input *ngIf="charge.isnew()" type="number" min="0" class="form-control"
+ i18n-placeholder placeholder="Esimated Cost..."
+ [ngModel]="charge.estimated_cost()" (ngModelChange)="charge.estimated_cost($event)"/>
+ </div>
+ <div class="flex-1 p-1">
+ <button *ngIf="charge.isnew()" class="btn btn-success btn-sm"
+ (click)="saveCharge(charge)" i18n>Save</button>
+ </div>
+ <div class="flex-1 p-1">
+ <button class="btn btn-danger btn-sm"
+ (click)="removeCharge(charge)" i18n>Remove</button>
+ </div>
+ </div>
+</ng-container>
--- /dev/null
+import {Component, OnInit, Input} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {PoService} from './po.service';
+
+@Component({
+ templateUrl: 'charges.component.html',
+ selector: 'eg-acq-po-charges'
+})
+export class PoChargesComponent implements OnInit {
+
+ showBody = false;
+ autoId = -1;
+
+ constructor(
+ private idl: IdlService,
+ private pcrud: PcrudService,
+ public poService: PoService
+ ) {}
+
+ ngOnInit() {
+ this.poService.poRetrieved.subscribe(() => {
+ if (this.po().po_items().length > 0) {
+ this.showBody = true;
+ }
+ });
+ }
+
+ po(): IdlObject {
+ return this.poService.currentPo;
+ }
+
+ newCharge() {
+ this.showBody = true;
+ const chg = this.idl.create('acqpoi');
+ chg.isnew(true);
+ chg.purchase_order(this.po().id());
+ chg.id(this.autoId--);
+ this.po().po_items().push(chg);
+ }
+
+ saveCharge(charge: IdlObject) {
+ if (!charge.inv_item_type()) { return; }
+
+ charge.id(undefined);
+ this.pcrud.create(charge).toPromise()
+ .then(item => {
+ charge.id(item.id());
+ charge.isnew(false);
+ })
+ .then(_ => this.poService.refreshOrderSummary());
+ }
+
+ removeCharge(charge: IdlObject) {
+ this.po().po_items( // remove local copy
+ this.po().po_items().filter(item => item.id() !== charge.id())
+ );
+
+ if (!charge.isnew()) {
+ this.pcrud.remove(charge).toPromise()
+ .then(_ => this.poService.refreshOrderSummary());
+ }
+ }
+}
+
--- /dev/null
+<eg-staff-banner bannerText="Create Purchase Order" i18n-bannerText>
+</eg-staff-banner>
+
+<div class="col-lg-4 offset-lg-4">
+ <div *ngIf="lineitems.length">
+ <span i18n>Creating for {{lineitems.length}} lineitems.</span>
+ <hr class="p-1" />
+ </div>
+ <div class="form-group">
+ <label for="order-agency-input" i18n>Ordering Agency</label>
+ <eg-org-select (onChange)="orgChange($event)" domId="order-agency-input">
+ </eg-org-select>
+ </div>
+ <div class="form-group">
+ <label for="name-input" i18n>Name (optional)</label>
+ <input id="name-input" class="form-control" type="text" [(ngModel)]="poName"/>
+ </div>
+ <div class="form-group">
+ <label for="name-input" i18n>Provider</label>
+ <eg-combobox domId="provider-input" [(ngModel)]="provider" idlClass="acqpro">
+ </eg-combobox>
+ </div>
+ <div class="form-group form-check">
+ <input type="checkbox" class="form-check-input"
+ [(ngModel)]="prepaymentRequired" id="prepayment-required">
+ <label class="form-check-label" for="prepayment-required" i18n>Prepayment Required</label>
+ </div>
+ <div class="form-group form-check">
+ <input type="checkbox" class="form-check-input"
+ [(ngModel)]="createAssets" id="create-assets">
+ <label class="form-check-label" for="create-assets" i18n>
+ Import Bibs and Create Copies
+ </label>
+ </div>
+ <hr class="p-1" />
+ <button [disabled]="!canCreate()" (click)="create()"
+ type="submit" class="btn btn-primary" i18n>Create</button>
+</div>
--- /dev/null
+import {Component, Input, OnInit, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {of, Observable} from 'rxjs';
+import {tap, take, map} from 'rxjs/operators';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {OrgService} from '@eg/core/org.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {ComboboxEntry, ComboboxComponent} from '@eg/share/combobox/combobox.component';
+import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
+import {EventService, EgEvent} from '@eg/core/event.service';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {PoService} from './po.service';
+import {LineitemService} from '../lineitem/lineitem.service';
+import {CancelDialogComponent} from '../lineitem/cancel-dialog.component';
+
+
+@Component({
+ templateUrl: 'create.component.html',
+ selector: 'eg-acq-po-create'
+})
+export class PoCreateComponent implements OnInit {
+
+ initDone = false;
+ lineitems: number[] = [];
+ poName: string;
+ orderAgency: number;
+ provider: ComboboxEntry;
+ prepaymentRequired = false;
+ createAssets = false;
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private evt: EventService,
+ private idl: IdlService,
+ private net: NetService,
+ private org: OrgService,
+ private pcrud: PcrudService,
+ private auth: AuthService,
+ private store: ServerStoreService,
+ private liService: LineitemService,
+ private poService: PoService
+ ) {}
+
+ ngOnInit() {
+ this.route.queryParamMap.subscribe((params: ParamMap) => {
+ this.lineitems = params.getAll('li').map(id => Number(id));
+ });
+
+ this.load().then(_ => this.initDone = true);
+ }
+
+ load(): Promise<any> {
+ return Promise.resolve();
+ }
+
+ orgChange(org: IdlObject) {
+ this.orderAgency = org ? org.id() : null;
+ }
+
+ canCreate(): boolean {
+ return (Boolean(this.orderAgency) && Boolean(this.provider));
+ }
+
+ create() {
+
+ const po = this.idl.create('acqpo');
+ po.ordering_agency(this.orderAgency);
+ po.provider(this.provider.id);
+ po.name(this.poName || null);
+ po.prepayment_required(this.prepaymentRequired ? 't' : 'f');
+
+ const args: any = {};
+ if (this.lineitems.length > 0) {
+ args.lineitems = this.lineitems;
+ }
+
+ if (this.createAssets) {
+ // This version simply creates all records sans Vandelay merging, etc.
+ // TODO: go to asset creator.
+ args.vandelay = {
+ import_no_match: true,
+ queue_name: `ACQ ${new Date().toISOString()}`
+ };
+ }
+
+ this.net.request('open-ils.acq',
+ 'open-ils.acq.purchase_order.create',
+ this.auth.token(), po, args
+ ).toPromise().then(resp => {
+ if (resp && resp.purchase_order) {
+ this.router.navigate(
+ ['/staff/acq/po/' + resp.purchase_order.id()]);
+ }
+ });
+ }
+}
+
+
--- /dev/null
+
+<!-- TODO: workstation setting -->
+
+<div class="mt-3">
+ <eg-grid idlClass="acqedim" [dataSource]="dataSource" [sortable]="true"
+ persistKey="acq.po.edi_messages"
+ hideFields="id">
+ </eg-grid>
+</div>
--- /dev/null
+import {Component, OnInit, Input, Output} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {empty} from 'rxjs';
+import {Pager} from '@eg/share/util/pager';
+import {IdlObject} from '@eg/core/idl.service';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {PcrudService} from '@eg/core/pcrud.service';
+
+@Component({templateUrl: 'edi.component.html'})
+export class PoEdiMessagesComponent implements OnInit {
+
+ poId: number;
+ dataSource: GridDataSource = new GridDataSource();
+
+ constructor(
+ private route: ActivatedRoute,
+ private pcrud: PcrudService
+ ) {}
+
+ ngOnInit() {
+ this.dataSource.getRows = (pager: Pager, sort: any) =>
+ this.getEdiMessages(pager, sort);
+
+ this.route.parent.paramMap.subscribe((params: ParamMap) => {
+ this.poId = +params.get('poId');
+ });
+ }
+
+ getEdiMessages(pager: Pager, sort: any) {
+ if (!this.poId) { return empty(); }
+
+ const orderBy: any = {acqedim: 'create_time DESC'};
+ if (sort.length) {
+ orderBy.acqedim = sort[0].name + ' ' + sort[0].dir;
+ }
+
+ return this.pcrud.search('acqedim', {purchase_order: this.poId}, {
+ offset: pager.offset,
+ limit: pager.limit,
+ order_by: orderBy,
+ flesh: 1,
+ flesh_fields: {acqedim: ['account', 'purchase_order']}
+ });
+ }
+}
+
--- /dev/null
+
+<!-- TODO: workstation setting -->
+
+<div class="mt-3">
+ <eg-grid idlClass="acqpoh" [dataSource]="dataSource" [sortable]="true"
+ persistKey="acq.po.history"
+ hideFields="id,audit_id,audit_time,audit_action">
+ <eg-grid-column name="create_time" [datePlusTime]="true"></eg-grid-column>
+ <eg-grid-column name="edit_time" [datePlusTime]="true"></eg-grid-column>
+ <eg-grid-column name="order_date" [datePlusTime]="true"></eg-grid-column>
+ </eg-grid>
+</div>
--- /dev/null
+import {Component, OnInit, Input, Output} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {empty} from 'rxjs';
+import {Pager} from '@eg/share/util/pager';
+import {IdlObject} from '@eg/core/idl.service';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {PcrudService} from '@eg/core/pcrud.service';
+
+@Component({templateUrl: 'history.component.html'})
+export class PoHistoryComponent implements OnInit {
+
+ poId: number;
+ dataSource: GridDataSource = new GridDataSource();
+
+ constructor(
+ private route: ActivatedRoute,
+ private pcrud: PcrudService
+ ) {}
+
+ ngOnInit() {
+ this.dataSource.getRows = (pager: Pager, sort: any) =>
+ this.getHistory(pager, sort);
+
+ this.route.parent.paramMap.subscribe((params: ParamMap) => {
+ this.poId = +params.get('poId');
+ });
+ }
+
+ getHistory(pager: Pager, sort: any) {
+ if (!this.poId) { return empty(); }
+
+ const orderBy: any = {acqpoh: 'edit_time DESC'};
+ if (sort.length) {
+ orderBy.acqpoh = sort[0].name + ' ' + sort[0].dir;
+ }
+
+ return this.pcrud.search('acqpoh', {id: this.poId}, {
+ offset: pager.offset,
+ limit: pager.limit,
+ order_by: orderBy,
+ flesh: 1,
+ flesh_fields: {
+ acqpoh: ['owner', 'creator', 'editor', 'provider', 'cancel_reason']
+ }
+ });
+ }
+}
+
--- /dev/null
+
+<div class="shadow border-top w-100">
+
+ <div class="p-1 m-1 form-inline">
+ <input type="text" class="form-control form-control-sm" id="note-text-input"
+ [(ngModel)]="noteText" placeholder="Note Text" i18n-placeholder/>
+ <div class="form-check ml-2">
+ <input class="form-check-input" type="checkbox"
+ [(ngModel)]="vendorPublic" id="vendor-public-cbox">
+ <label class="form-check-label" for="vendor-public-cbox" i18n>
+ Vendor Public
+ </label>
+ </div>
+ <button class="btn btn-sm btn-success ml-2" [disabled]="!noteText"
+ (click)="newNote()" i18n>New Note</button>
+ <a class="ml-auto" href="javascript:;" (click)="close()" title="Close" i18n-title>
+ <span class="material-icons text-danger">close</span>
+ </a>
+ </div>
+
+ <div *ngFor="let note of po.notes()">
+ <div class="d-flex m-1 p-2 border">
+ <div class="flex-1 p-1">
+ <ng-container *ngIf="note.vendor_public() == 't'">
+ <div class="text-primary" i18n>VENDOR PUBLIC</div>
+ </ng-container>
+ </div>
+ <div class="flex-5 ml-2 p-1">{{note.value()}}</div>
+ <div class="ml-2 p-1">{{note.create_time() | date:'short'}}</div>
+ <div class="ml-2 p-1">
+ <a href="javascript:;" class="text-danger"
+ (click)="deleteNote(note)" i18n>Delete</a>
+ </div>
+ </div>
+ </div>
+</div>
+
+
+
--- /dev/null
+import {Component, OnInit, AfterViewInit, Input, Output, EventEmitter} from '@angular/core';
+import {Observable} from 'rxjs';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+@Component({
+ templateUrl: 'notes.component.html',
+ selector: 'eg-po-notes'
+})
+export class PoNotesComponent implements OnInit, AfterViewInit {
+
+ @Input() po: IdlObject;
+ noteText: string;
+ vendorPublic = false;
+
+ @Output() closeRequested: EventEmitter<void> = new EventEmitter<void>();
+
+ constructor(
+ private idl: IdlService,
+ private org: OrgService,
+ private auth: AuthService,
+ private net: NetService
+ ) {}
+
+ ngOnInit() {
+ }
+
+ ngAfterViewInit() {
+ const node = document.getElementById('note-text-input');
+ if (node) { node.focus(); }
+ }
+
+ orgSn(id: number): string {
+ return this.org.get(id).shortname();
+ }
+
+ close() {
+ this.closeRequested.emit();
+ }
+
+ newNote() {
+ const note = this.idl.create('acqpon');
+ note.isnew(true);
+ note.purchase_order(this.po.id());
+ note.value(this.noteText || '');
+ note.vendor_public(this.vendorPublic ? 't' : 'f');
+
+ this.modifyNotes(note).subscribe(resp => {
+ if (resp.note) {
+ this.po.notes().unshift(resp.note);
+ }
+ });
+ }
+
+ deleteNote(note: IdlObject) {
+ note.isdeleted(true);
+ this.modifyNotes(note).toPromise().then(_ => {
+ this.po.notes(
+ this.po.notes().filter(n => n.id() !== note.id())
+ );
+ });
+ }
+
+ modifyNotes(notes: IdlObject | IdlObject[]): Observable<any> {
+ notes = [].concat(notes);
+
+ return this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.po_note.cud.batch',
+ this.auth.token(), notes);
+ }
+}
+
--- /dev/null
+<ng-container *ngIf="poService.currentPo">
+ <eg-staff-banner i18n-bannerText
+ bannerText="Purchase Order #{{poId}} ({{poService.currentPo.state()}})">
+ </eg-staff-banner>
+</ng-container>
+
+<eg-acq-po-summary [poId]="poId"></eg-acq-po-summary>
+
+<ng-container *ngIf="!isBasePage()">
+ <div class="mt-2">
+ <a routerLink="/staff/acq/po/{{poId}}" queryParamsHandling="merge">
+ <button class="btn btn-sm btn-info" i18n>Return</button>
+ </a>
+ </div>
+</ng-container>
+
+<router-outlet></router-outlet>
+
+<hr class="mt-3 pt-3"/>
+
+<!-- gets its po from poService -->
+<ng-container *ngIf="isBasePage()">
+ <eg-acq-po-charges></eg-acq-po-charges>
+</ng-container>
--- /dev/null
+import {Component, OnInit} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {IdlObject} from '@eg/core/idl.service';
+import {PoService} from './po.service';
+
+@Component({
+ templateUrl: 'po.component.html'
+})
+export class PoComponent implements OnInit {
+
+ poId: number;
+
+ constructor(
+ private route: ActivatedRoute,
+ public poService: PoService
+ ) {}
+
+ ngOnInit() {
+ this.route.paramMap.subscribe((params: ParamMap) => {
+ this.poId = +params.get('poId');
+ });
+ }
+
+ isBasePage(): boolean {
+ return !this.route.firstChild ||
+ this.route.firstChild.snapshot.url.length === 0;
+ }
+}
+
--- /dev/null
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {HttpClientModule} from '@angular/common/http';
+import {CatalogCommonModule} from '@eg/share/catalog/catalog-common.module';
+import {LineitemModule} from '@eg/staff/acq/lineitem/lineitem.module';
+import {HoldingsModule} from '@eg/staff/share/holdings/holdings.module';
+import {PoRoutingModule} from './routing.module';
+import {PoService} from './po.service';
+import {PoComponent} from './po.component';
+import {PrintComponent} from './print.component';
+import {PoSummaryComponent} from './summary.component';
+import {PoHistoryComponent} from './history.component';
+import {PoEdiMessagesComponent} from './edi.component';
+import {PoNotesComponent} from './notes.component';
+import {PoCreateComponent} from './create.component';
+import {PoChargesComponent} from './charges.component';
+
+
+@NgModule({
+ declarations: [
+ PoComponent,
+ PoSummaryComponent,
+ PoHistoryComponent,
+ PoEdiMessagesComponent,
+ PoNotesComponent,
+ PoCreateComponent,
+ PoChargesComponent,
+ PrintComponent
+ ],
+ imports: [
+ StaffCommonModule,
+ CatalogCommonModule,
+ LineitemModule,
+ HoldingsModule,
+ PoRoutingModule
+ ],
+ providers: [
+ PoService
+ ]
+})
+
+export class PoModule {
+}
--- /dev/null
+import {Injectable, EventEmitter} from '@angular/core';
+import {Observable, from} from 'rxjs';
+import {switchMap, map, tap, merge} from 'rxjs/operators';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+
+@Injectable()
+export class PoService {
+
+ currentPo: IdlObject;
+
+ poRetrieved: EventEmitter<IdlObject> = new EventEmitter<IdlObject>();
+
+ constructor(
+ private evt: EventService,
+ private net: NetService,
+ private auth: AuthService
+ ) {}
+
+ getFleshedPo(id: number, fleshMore?: any, noCache?: boolean): Promise<IdlObject> {
+
+ if (!noCache) {
+ if (this.currentPo && id === this.currentPo.id()) {
+ // Set poService.currentPo = null to bypass the cache
+ return Promise.resolve(this.currentPo);
+ }
+ }
+
+ const flesh = Object.assign({
+ flesh_provider: true,
+ flesh_notes: true,
+ flesh_po_items: true,
+ flesh_price_summary: true,
+ flesh_lineitem_count: true
+ }, fleshMore || {});
+
+ return this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.purchase_order.retrieve',
+ this.auth.token(), id, flesh
+ ).toPromise().then(po => {
+
+ const evt = this.evt.parse(po);
+ if (evt) { return Promise.reject(evt + ''); }
+
+ if (!noCache) { this.currentPo = po; }
+
+ this.poRetrieved.emit(po);
+ return po;
+ });
+ }
+
+ // Fetch the PO again (with less fleshing) and update the
+ // order summary totals our main fully-fleshed PO.
+ refreshOrderSummary(): Promise<any> {
+
+ return this.net.request('open-ils.acq',
+ 'open-ils.acq.purchase_order.retrieve.authoritative',
+ this.auth.token(), this.currentPo.id(),
+ {flesh_price_summary: true}
+
+ ).toPromise().then(po => {
+
+ this.currentPo.amount_encumbered(po.amount_encumbered());
+ this.currentPo.amount_spent(po.amount_spent());
+ this.currentPo.amount_estimated(po.amount_estimated());
+ });
+ }
+}
+
+
--- /dev/null
+<div class="row">
+ <div class="p-2 m-2 ml-4">
+ <button class="btn btn-outline-dark" (click)="printPo()" i18n>
+ Print Purchase Order
+ </button>
+ </div>
+</div>
+
+<div id="print-outlet" class="m-3 p-3 w-90 border border-dark"></div>
+
--- /dev/null
+import {Component, OnInit, AfterViewInit} from '@angular/core';
+import {Observable} from 'rxjs';
+import {map, take} from 'rxjs/operators';
+import {ActivatedRoute, ParamMap} from '@angular/router';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {IdlService} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {PrintService} from '@eg/share/print/print.service';
+import {BroadcastService} from '@eg/share/util/broadcast.service';
+import {PoService} from './po.service';
+
+@Component({
+ templateUrl: 'print.component.html'
+})
+export class PrintComponent implements OnInit, AfterViewInit {
+
+ poId: number;
+ outlet: Element;
+ po: IdlObject;
+ printing: boolean;
+ closing: boolean;
+ initDone = false;
+
+ constructor(
+ private route: ActivatedRoute,
+ private idl: IdlService,
+ private org: OrgService,
+ private net: NetService,
+ private auth: AuthService,
+ private pcrud: PcrudService,
+ private poService: PoService,
+ private broadcaster: BroadcastService,
+ private printer: PrintService) {
+ }
+
+ ngOnInit() {
+ this.route.parent.paramMap.subscribe((params: ParamMap) => {
+ const poId = +params.get('poId');
+ if (poId !== this.poId) {
+ this.poId = poId;
+ if (poId && this.initDone) { this.load(); }
+ }
+ });
+
+ this.load();
+ }
+
+ ngAfterViewInit() {
+ this.outlet = document.getElementById('print-outlet');
+ }
+
+ load() {
+ if (!this.poId) { return; }
+
+ this.po = null;
+ this.poService.getFleshedPo(this.poId, {
+ flesh_provider_addresses: true,
+ flesh_lineitems: true,
+ flesh_lineitem_attrs: true,
+ flesh_lineitem_notes: true,
+ flesh_lineitem_details: true,
+ clear_marc: true,
+ flesh_notes: true
+ }, true)
+ .then(po => this.po = po)
+ .then(_ => this.populatePreview())
+ .then(_ => this.initDone = true);
+ }
+
+ populatePreview(): Promise<any> {
+
+ return this.printer.compileRemoteTemplate({
+ templateName: 'purchase_order',
+ printContext: 'default',
+ contextData: {po: this.po}
+
+ }).then(response => {
+ this.outlet.innerHTML = response.content;
+ });
+ }
+
+ addLiPrintNotes(): Promise<any> {
+
+ const notes = [];
+ this.po.lineitems().forEach(li => {
+ const note = this.idl.create('acqlin');
+ note.isnew(true);
+ note.lineitem(li.id());
+ note.value('printed: ' + this.auth.user().usrname());
+ notes.push(note);
+ });
+
+ return this.net.request('open-ils.acq',
+ 'open-ils.acq.lineitem_note.cud.batch', this.auth.token(), notes)
+ .toPromise().then(_ => {
+ this.broadcaster.broadcast(
+ 'eg.acq.lineitem.notes.update', {
+ lineitems: notes.map(n => Number(n.lineitem()))
+ });
+ });
+ }
+
+ printPo(closeTab?: boolean) {
+ this.addLiPrintNotes().then(_ => this.printPo2(closeTab));
+ }
+
+ printPo2(closeTab?: boolean) {
+ if (closeTab || this.closing) {
+ const sub: any = this.printer.printJobQueued$.subscribe(req => {
+ if (req.templateName === 'purchase_order') {
+ setTimeout(() => {
+ window.close();
+ sub.unsubscribe();
+ }, 2000); // allow for a time cushion past queueing.
+ }
+ });
+ }
+
+ this.printer.print({
+ templateName: 'purchase_order',
+ printContext: 'default',
+ contextData: {po: this.po}
+ });
+ }
+}
+
--- /dev/null
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {PoComponent} from './po.component';
+import {PrintComponent} from './print.component';
+import {PoSummaryComponent} from './summary.component';
+import {LineitemListComponent} from '../lineitem/lineitem-list.component';
+import {LineitemDetailComponent} from '../lineitem/detail.component';
+import {LineitemCopiesComponent} from '../lineitem/copies.component';
+import {BriefRecordComponent} from '../lineitem/brief-record.component';
+import {LineitemHistoryComponent} from '../lineitem/history.component';
+import {LineitemWorksheetComponent} from '../lineitem/worksheet.component';
+import {PoHistoryComponent} from './history.component';
+import {PoEdiMessagesComponent} from './edi.component';
+import {PoCreateComponent} from './create.component';
+
+const routes: Routes = [{
+ path: 'create',
+ component: PoCreateComponent
+}, {
+ path: ':poId',
+ component: PoComponent,
+ children : [{
+ path: '',
+ component: LineitemListComponent
+ }, {
+ path: 'history',
+ component: PoHistoryComponent
+ }, {
+ path: 'edi',
+ component: PoEdiMessagesComponent
+ }, {
+ path: 'brief-record',
+ component: BriefRecordComponent
+ }, {
+ path: 'lineitem/:lineitemId/detail',
+ component: LineitemDetailComponent
+ }, {
+ path: 'lineitem/:lineitemId/history',
+ component: LineitemHistoryComponent
+ }, {
+ path: 'lineitem/:lineitemId/items',
+ component: LineitemCopiesComponent
+ }, {
+ path: 'lineitem/:lineitemId/worksheet',
+ component: LineitemWorksheetComponent
+ }, {
+ path: 'printer',
+ component: PrintComponent
+ }, {
+ path: 'printer/print',
+ component: PrintComponent
+ }, {
+ path: 'printer/print/close',
+ component: PrintComponent
+ }]
+}];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule],
+ providers: []
+})
+
+export class PoRoutingModule {}
--- /dev/null
+
+<eg-acq-cancel-dialog #cancelDialog></eg-acq-cancel-dialog>
+<eg-progress-dialog #progressDialog></eg-progress-dialog>
+
+<div *ngIf="po()" class="p-1 border border-secondary rounded">
+
+ <div class="row">
+ <div class="col-lg-9">
+
+ <div class="row">
+ <div class="col-lg-3 d-flex">
+ <div class="flex-2" i18n>PO ID:</div>
+ <div class="flex-3">{{poId}}</div>
+ </div>
+ <div class="col-lg-9 d-flex">
+ <div class="flex-1" i18n>PO Name:</div>
+ <div class="flex-6">
+ <ng-container *ngIf="editPoName">
+ <input id='pl-name-input' type="text" class="form-control"
+ [(ngModel)]="newPoName" (keyup.enter)="toggleNameEdit(true)"
+ (blur)="toggleNameEdit()"/>
+ </ng-container>
+ <ng-container *ngIf="!editPoName">
+ <a (click)="toggleNameEdit()" href='javascript:;'
+ class='font-weight-bold'>{{po().name()}}</a>
+ </ng-container>
+ </div>
+ </div>
+ </div>
+
+ <div class="row">
+ <div class="col-lg-3 d-flex">
+ <div class="flex-2" i18n>Lineitems:</div>
+ <div class="flex-3">{{po().lineitem_count()}}</div>
+ </div>
+ <div class="col-lg-9 d-flex">
+ <div class="flex-1" i18n>Provider:</div>
+ <div class="flex-6">
+ <a routerLink="/staff/acq/provider/{{po().provider().id()}}/details">
+ {{po().provider().name()}}
+ </a>
+ </div>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-lg-3 d-flex">
+ <div class="flex-2" i18n>Activated:</div>
+ <div class="flex-3">
+ <span *ngIf="po().order_date()">{{po().order_date() | date:'short'}}</span>
+ <span *ngIf="!po().order_date()" i18n>N/A</span>
+ </div>
+ </div>
+ <div class="col-lg-9 d-flex">
+ <div class="flex-1" i18n>Status:</div>
+ <div class="flex-6">
+ <div class="w-50" *ngIf="canActivate === null">
+ <eg-progress-inline></eg-progress-inline>
+ </div>
+
+ <span *ngIf="po().state() == 'on-order'" i18n>On Order</span>
+ <ng-container *ngIf="canActivate">
+ <span *ngIf="!activationEvent" i18n>Pending / Activatable</span>
+ <span *ngIf="activationEvent" i18n>
+ Activation Error: {{activationEvent.textcode}} {{activationEvent.desc}}
+ </span>
+ </ng-container>
+
+ <!-- canceled -->
+ <ng-container *ngIf="po().cancel_reason()">
+ <span class="text-danger" i18n>
+ {{po().cancel_reason().label()}} => {{po().cancel_reason().description()}}
+ </span>
+ </ng-container>
+
+ <!-- activation blocks -->
+ <div class="text-danger" *ngFor="let evt of activationBlocks">
+ <ng-container
+ *ngIf="evt.textcode == 'ACQ_FUND_EXCEEDS_STOP_PERCENT'; else fundWarn">
+ <span i18n>
+ Fund exceeds stop percent:
+ {{evt.payload.fund.code()}} ({{evt.payload.fund.year()}}).
+ </span>
+ </ng-container>
+ <ng-template #fundWarn>
+ <ng-container
+ *ngIf="evt.textcode == 'ACQ_FUND_EXCEEDS_WARN_PERCENT'; else noPrice">
+ <span i18n>
+ Fund exceeds warning percent:
+ {{evt.payload.fund.code()}} ({{evt.payload.fund.year()}}).
+ </span>
+ </ng-container>
+ </ng-template>
+ <ng-template #noPrice>
+ <ng-container
+ *ngIf="evt.textcode == 'ACQ_LINEITEM_NO_PRICE'; else noCopies">
+ <span i18n>One or more lineitems have no price.</span>
+ </ng-container>
+ </ng-template>
+ <ng-template #noCopies>
+ <ng-container
+ *ngIf="evt.textcode == 'ACQ_LINEITEM_NO_COPIES'; else otherBlock">
+ <span i18n>One or more lineitems have no items attached.</span>
+ </ng-container>
+ </ng-template>
+ <ng-template #otherBlock>
+ <span i18n>{{evt.textcode}} : {{evt.desc}}</span>
+ </ng-template>
+ </div>
+ </div>
+ </div>
+ </div>
+ <hr class="p-0 m-0 mt-1"/>
+ <div class="row mt-1">
+ <div class="col-lg-12">
+ <a class="" href="javascript:;" (click)="showNotes=!showNotes"
+ i18n>Notes ({{po().notes().length}})</a>
+ <span class="pl-2 pr-2" i18n> | </span>
+ <a [queryParams]="{f: 'acqpo:id', val1: poId}"
+ routerLink="/staff/acq/search/invoices" i18n>Invoices ({{invoiceCount}})</a>
+ <span class="pl-2 pr-2" i18n> | </span>
+ <a href="/eg/acq/invoice/view?create=1&attach_po={{poId}}"
+ i18n>Create Invoice</a>
+ <span class="pl-2 pr-2" i18n> | </span>
+ <a routerLink="./edi" i18n>EDI Messages ({{ediMessageCount}})</a>
+ <span class="pl-2 pr-2" i18n> | </span>
+ <a routerLink="./history" i18n>History</a>
+ <span class="pl-2 pr-2" i18n> | </span>
+ <a routerLink="./printer" i18n>Print</a>
+ <ng-container *ngIf="po().state() == 'on-order' || po().state() == 'pending'">
+ <span class="pl-2 pr-2" i18n> | </span>
+ <a (click)="cancelPo()" href="javascript:;" i18n>Cancel Order</a>
+ </ng-container>
+ <ng-container *ngIf="canActivate === true">
+ <span class="pl-2 pr-2" i18n> | </span>
+ <a (click)="activatePo()" href="javascript:;" i18n>Activate Order</a>
+ </ng-container>
+ </div>
+ </div>
+ </div>
+ <div class="col-lg-3">
+ <div class="row">
+ <div class="col-lg-8" i18n>Estimated Amount:</div>
+ <div class="col-lg-4">{{po().amount_estimated() | currency}}</div>
+ </div>
+ <div class="row">
+ <div class="col-lg-8" i18n>Encumbered Amount:</div>
+ <div class="col-lg-4">{{po().amount_encumbered() | currency}}</div>
+ </div>
+ <div class="row">
+ <div class="col-lg-8" i18n>Spent Amount:</div>
+ <div class="col-lg-4">{{po().amount_spent() | currency}}</div>
+ </div>
+ <div class="row">
+ <div class="col-lg-8" i18n>Prepayment Required?</div>
+ <div class="col-lg-4">
+ <eg-bool [value]="po().provider().prepayment_required()"></eg-bool>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="row" *ngIf="showNotes">
+ <div class="col-lg-10 offset-lg-1 p-2 mt-2 shadow">
+ <eg-po-notes [po]="po()" (closeRequested)="showNotes = false">
+ </eg-po-notes>
+ </div>
+ </div>
+</div>
+
--- /dev/null
+import {Component, Input, OnInit, ViewChild} from '@angular/core';
+import {Router} from '@angular/router';
+import {of, Observable} from 'rxjs';
+import {tap, take, map} from 'rxjs/operators';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {OrgService} from '@eg/core/org.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {ComboboxEntry, ComboboxComponent} from '@eg/share/combobox/combobox.component';
+import {ProgressDialogComponent} from '@eg/share/dialog/progress.component';
+import {EventService, EgEvent} from '@eg/core/event.service';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {PoService} from './po.service';
+import {LineitemService} from '../lineitem/lineitem.service';
+import {CancelDialogComponent} from '../lineitem/cancel-dialog.component';
+
+
+@Component({
+ templateUrl: 'summary.component.html',
+ selector: 'eg-acq-po-summary'
+})
+export class PoSummaryComponent implements OnInit {
+
+ private _poId: number;
+ @Input() set poId(id: number) {
+ if (id === this._poId) { return; }
+ this._poId = id;
+ if (this.initDone) { this.load(); }
+ }
+ get poId(): number { return this._poId; }
+
+ newPoName: string;
+ editPoName = false;
+ initDone = false;
+ ediMessageCount = 0;
+ invoiceCount = 0;
+ showNotes = false;
+ canActivate: boolean = null;
+
+ activationBlocks: EgEvent[] = [];
+ activationEvent: EgEvent;
+ nameEditEnterToggled = false;
+
+ @ViewChild('cancelDialog') cancelDialog: CancelDialogComponent;
+ @ViewChild('progressDialog') progressDialog: ProgressDialogComponent;
+
+ constructor(
+ private router: Router,
+ private evt: EventService,
+ private idl: IdlService,
+ private net: NetService,
+ private org: OrgService,
+ private pcrud: PcrudService,
+ private auth: AuthService,
+ private store: ServerStoreService,
+ private liService: LineitemService,
+ private poService: PoService
+ ) {}
+
+ ngOnInit() {
+ this.load().then(_ => this.initDone = true);
+
+ // Re-check for activation blocks if the LI service tells us
+ // something significant happened.
+ this.liService.activateStateChange
+ .subscribe(_ => this.setCanActivate());
+ }
+
+ po(): IdlObject {
+ return this.poService.currentPo;
+ }
+
+ load(): Promise<any> {
+ if (!this.poId) { return Promise.resolve(); }
+
+ return this.poService.getFleshedPo(this.poId)
+ .then(po => {
+
+ // EDI message count
+ return this.pcrud.search('acqedim',
+ {purchase_order: this.poId}, {}, {idlist: true, atomic: true}
+ ).toPromise().then(ids => this.ediMessageCount = ids.length);
+
+ }).then(_ => {
+
+ // Invoice count
+ return this.net.request('open-ils.acq',
+ 'open-ils.acq.invoice.unified_search.atomic',
+ this.auth.token(), {acqpo: [{id: this.poId}]},
+ null, null, {id_list: true}
+ ).toPromise().then(ids => this.invoiceCount = ids.length);
+
+ }).then(_ => this.setCanActivate());
+ }
+
+ // Can run via Enter or blur. If it just ran via Enter, avoid
+ // running it again on the blur, which will happen directly after
+ // the Enter.
+ toggleNameEdit(fromEnter?: boolean) {
+ if (fromEnter) {
+ this.nameEditEnterToggled = true;
+ } else {
+ if (this.nameEditEnterToggled) {
+ this.nameEditEnterToggled = false;
+ return;
+ }
+ }
+
+ this.editPoName = !this.editPoName;
+
+ if (this.editPoName) {
+ this.newPoName = this.po().name();
+ setTimeout(() => {
+ const node =
+ document.getElementById('pl-name-input') as HTMLInputElement;
+ if (node) { node.select(); }
+ });
+
+ } else if (this.newPoName && this.newPoName !== this.po().name()) {
+
+ const prevName = this.po().name();
+ this.po().name(this.newPoName);
+ this.newPoName = null;
+
+ this.pcrud.update(this.po()).subscribe(resp => {
+ const evt = this.evt.parse(resp);
+ if (evt) {
+ alert(evt);
+ this.po().name(prevName);
+ }
+ });
+ }
+ }
+
+ cancelPo() {
+ this.cancelDialog.open().subscribe(reason => {
+ if (!reason) { return; }
+
+ this.progressDialog.reset();
+ this.progressDialog.open();
+ this.net.request('open-ils.acq',
+ 'open-ils.acq.purchase_order.cancel',
+ this.auth.token(), this.poId, reason
+ ).subscribe(ok => {
+ this.progressDialog.close();
+ location.href = location.href;
+ });
+ });
+ }
+
+ setCanActivate() {
+ this.canActivate = null;
+ this.activationBlocks = [];
+
+ if (!(this.po().state().match(/new|pending/))) {
+ this.canActivate = false;
+ return;
+ }
+
+ this.net.request('open-ils.acq',
+ 'open-ils.acq.purchase_order.activate.dry_run',
+ this.auth.token(), this.poId
+
+ ).pipe(tap(resp => {
+
+ const evt = this.evt.parse(resp);
+ if (evt) { this.activationBlocks.push(evt); }
+
+ })).toPromise().then(_ => {
+
+ if (this.activationBlocks.length === 0) {
+ this.canActivate = true;
+ return;
+ }
+
+ this.canActivate = false;
+
+ // TODO More logic likely needed here to handle zero-copy
+ // activation / ACQ_LINEITEM_NO_COPIES
+ });
+ }
+
+ activatePo() {
+ // TODO This code bypasses the Vandelay UI and force-loads the records.
+
+ this.activationEvent = null;
+ this.progressDialog.open();
+ this.progressDialog.update({max: this.po().lineitem_count() * 3});
+
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.purchase_order.activate',
+ this.auth.token(), this.poId, {
+ // Import all records, no merging, etc.
+ import_no_match: true,
+ queue_name: `ACQ ${new Date().toISOString()}`
+ }
+ ).subscribe(resp => {
+ const evt = this.evt.parse(resp);
+
+ if (evt) {
+ this.progressDialog.close();
+ this.activationEvent = evt;
+ return;
+ }
+
+ if (Number(resp) === 1) {
+ this.progressDialog.close();
+ // Refresh everything.
+ location.href = location.href;
+
+ } else {
+ this.progressDialog.update(
+ {value: resp.bibs + resp.li + resp.vqbr});
+ }
+ });
+ }
+}
+
+
import {NgModule} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
-const routes: Routes = [
- { path: 'search',
- loadChildren: () =>
- import('./search/acq-search.module').then(m => m.AcqSearchModule)
- },
- { path: 'provider',
- loadChildren: () =>
- import('./provider/acq-provider.module').then(m => m.AcqProviderModule)
- }
-];
+const routes: Routes = [{
+ path: 'search',
+ loadChildren: () =>
+ import('./search/acq-search.module').then(m => m.AcqSearchModule)
+}, {
+ path: 'provider',
+ loadChildren: () =>
+ import('./provider/acq-provider.module').then(m => m.AcqProviderModule)
+}, {
+ path: 'po',
+ loadChildren: () => import('./po/po.module').then(m => m.PoModule)
+}, {
+ path: 'picklist',
+ loadChildren: () =>
+ import('./picklist/picklist.module').then(m => m.PicklistModule)
+}];
@NgModule({
imports: [RouterModule.forChild(routes)],
defaultSearchSetting="eg.acq.search.default.lineitems"></eg-acq-search-form>
<ng-template #idTmpl let-lineitem="row">
- <a *ngIf="lineitem.purchase_order()" href="/eg/staff/acq/legacy/po/view/{{lineitem.purchase_order().id()}}?focus_li={{lineitem.id()}}"
- target="_blank">
+ <a *ngIf="lineitem.purchase_order()"
+ routerLink="/staff/acq/po/{{lineitem.purchase_order().id()}}"
+ fragment="{{lineitem.id()}}" target="_blank">
{{lineitem.id()}}
</a>
- <a *ngIf="lineitem.picklist() && !lineitem.purchase_order()" href="/eg/staff/acq/legacy/picklist/view/{{lineitem.picklist().id()}}?focus_li={{lineitem.id()}}"
- target="_blank">
+ <a *ngIf="lineitem.picklist() && !lineitem.purchase_order()"
+ routerLink="/staff/acq/picklist/{{lineitem.picklist().id()}}"
+ fragment="{{lineitem.id()}}" target="_blank">
{{lineitem.id()}}
</a>
</ng-template>
<ng-template #poTmpl let-lineitem="row">
- <a *ngIf="lineitem.purchase_order()" href="/eg/staff/acq/legacy/po/view/{{lineitem.purchase_order().id()}}?focus_li={{lineitem.id()}}"
- target="_blank">
+ <a *ngIf="lineitem.purchase_order()"
+ routerLink="/staff/acq/po/{{lineitem.purchase_order().id()}}"
+ fragment="{{lineitem.id()}}" target="_blank">
{{lineitem.purchase_order().name()}}
</a>
</ng-template>
<ng-template #plTmpl let-lineitem="row">
- <a *ngIf="lineitem.picklist()" href="/eg/staff/acq/legacy/picklist/view/{{lineitem.picklist().id()}}?focus_li={{lineitem.id()}}"
- target="_blank">
+ <a *ngIf="lineitem.picklist()"
+ routerLink="/staff/acq/picklist/{{lineitem.picklist().id()}}"
+ fragment="{{lineitem.id()}}" target="_blank">
{{lineitem.picklist().name()}}
</a>
</ng-template>
<li *ngIf="lineitem.eg_bib_id()">
<a routerLink="/staff/catalog/record/{{lineitem.eg_bib_id()}}"
target="_blank" i18n>Catalog</a></li>
- <li><a href="/eg/staff/acq/legacy/lineitem/worksheet/{{lineitem.id()}}"
+ <li><a routerLink="/staff/acq/lineitem/{{lineitem.id()}}/worksheet"
target="_blank" i18n>Worksheet</a></li>
<li *ngIf="lineitem.purchase_order()">
- <a href="/eg/staff/acq/legacy/po/view/{{lineitem.purchase_order().id()}}"
+ <a routerLink="/staff/acq/po/{{lineitem.purchase_order().id()}}"
target="_blank" i18n>Purchase Order</a></li>
<li><a href="/eg/staff/acq/requests/lineitem/{{lineitem.id()}}"
target="_blank" i18n>Requests</a></li>
<a routerLink="/staff/cat/vandelay/queue/bib/{{lineitem.queued_record().queue()}}"
target="_blank" i18n>Queue</a></li>
<li *ngIf="lineitem.picklist()">
- <a href="/eg/staff/acq/legacy/picklist/view/{{lineitem.picklist().id()}}"
+ <a routerLink="/staff/acq/picklist/{{lineitem.picklist().id()}}"
target="_blank" i18n>Selection List</a></li>
</ul>
</ng-template>
</eg-string>
<ng-template #nameTmpl let-selectionlist="row">
- <a href="/eg/staff/acq/legacy/picklist/view/{{selectionlist.id()}}"
- target="_blank">
+ <a routerLink="/staff/acq/picklist/{{selectionlist.id()}}" target="_blank">
{{selectionlist.name()}}
</a>
</ng-template>
<span class="material-icons" aria-hidden="true">shopping_cart</span>
<span i18n>Purchase Orders</span>
</a>
- <a class="dropdown-item" href="/eg/staff/acq/legacy/po/create">
+ <a class="dropdown-item" routerLink="/staff/acq/po/create">
<span class="material-icons" aria-hidden="true">add_shopping_cart</span>
<span i18n>Create Purchase Order</span>
</a>
import {NgbPopover} from '@ng-bootstrap/ng-bootstrap';
import {TagTable} from './tagtable.service';
+const MARC_RECORD_TYPES: 'biblio' | 'authority' | 'serial' | 'lineitem' = null;
+
+export type MARC_RECORD_TYPE = typeof MARC_RECORD_TYPES;
+
/* Per-instance MARC editor context. */
const STUB_DATA_00X = ' ';
recordChange: EventEmitter<MarcRecord>;
fieldFocusRequest: EventEmitter<FieldFocusRequest>;
textUndoRedoRequest: EventEmitter<TextUndoRedoAction>;
- recordType: 'biblio' | 'authority' = 'biblio';
+ recordType: MARC_RECORD_TYPE;
lastFocused: FieldFocusRequest = null;
import {PcrudService} from '@eg/core/pcrud.service';
import {DialogComponent} from '@eg/share/dialog/dialog.component';
import {NgbModal, NgbModalRef, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
-import {MarcEditContext} from './editor-context';
+import {MarcEditContext, MARC_RECORD_TYPE} from './editor-context';
/**
@Input() context: MarcEditContext;
@Input() recordXml: string;
- @Input() recordType: 'biblio' | 'authority' = 'biblio';
+ @Input() recordType: MARC_RECORD_TYPE = 'biblio';
constructor(
private modal: NgbModal,
import {ComboboxEntry, ComboboxComponent
} from '@eg/share/combobox/combobox.component';
import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
-import {MarcEditContext} from './editor-context';
+import {MarcEditContext, MARC_RECORD_TYPE} from './editor-context';
import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
+
export interface MarcSavedEvent {
marcXml: string;
bibSource?: number;
// True if the save request is in flight
dataSaving: boolean;
- @Input() recordType: 'biblio' | 'authority' = 'biblio';
+ @Input() recordType: MARC_RECORD_TYPE = 'biblio';
_pendingRecordId: number;
@Input() set recordId(id: number) {
import {NetService} from '@eg/core/net.service';
import {PcrudService} from '@eg/core/pcrud.service';
import {ContextMenuEntry} from '@eg/share/context-menu/context-menu.service';
+import {MARC_RECORD_TYPE} from './editor-context';
const DEFAULT_MARC_FORMAT = 'marc21';
interface TagTableSelector {
marcFormat?: string;
- marcRecordType: 'biblio' | 'authority' | 'serial';
+ marcRecordType: MARC_RECORD_TYPE;
// MARC record fixed field "Type" value.
ffType: string;
.flex-3 {flex: 3}
.flex-4 {flex: 4}
.flex-5 {flex: 5}
+.flex-6 {flex: 6}
/** BS deprecated the well, but it's replacement is not quite the same.
* Define our own version and expand it to a full "table".
font-size: 18px;
}
+.material-icons.small {
+ font-size: 18px;
+}
+
.input-group .mat-icon-in-button {
font-size: .88rem !important; /* useful for buttons that cuddle up with inputs */
}
.negative-money-amount {
color: red;
}
+
+input.medium {
+ width: 6em;
+}
+
+input.small {
+ width: 4em;
+}
+
--- /dev/null
+package OpenILS::Application::Acq::Common;
+use strict; use warnings;
+use OpenILS::Application::AppUtils;
+my $U = 'OpenILS::Application::AppUtils';
+
+# retrieves a lineitem, fleshes its PO and PL, checks perms
+# returns ($li, $evt, $org)
+sub fetch_and_check_li {
+ my ($class, $e, $li_id, $perm_mode) = @_;
+ $perm_mode ||= 'read';
+
+ my $li = $e->retrieve_acq_lineitem([
+ $li_id,
+ { flesh => 1,
+ flesh_fields => {jub => ['purchase_order', 'picklist']}
+ }
+ ]) or return (undef, $e->die_event);
+
+ my $org;
+ if(my $po = $li->purchase_order) {
+ $org = $po->ordering_agency;
+ my $perms = ($perm_mode eq 'read') ? 'VIEW_PURCHASE_ORDER' : 'CREATE_PURCHASE_ORDER';
+ return ($li, $e->die_event) unless $e->allowed($perms, $org);
+
+ } elsif(my $pl = $li->picklist) {
+ $org = $pl->org_unit;
+ my $perms = ($perm_mode eq 'read') ? 'VIEW_PICKLIST' : 'CREATE_PICKLIST';
+ return ($li, $e->die_event) unless $e->allowed($perms, $org);
+ }
+
+ return ($li, undef, $org);
+}
+
+sub li_existing_copies {
+ my ($class, $e, $li_id) = @_;
+
+ my ($li, $evt, $org) = $class->fetch_and_check_li($e, $li_id);
+ return 0 if $evt;
+
+ # No fuzzy matching here (e.g. on ISBN). Only exact matches are supported.
+ return 0 unless $li->eg_bib_id;
+
+ my $counts = $e->json_query({
+ select => {acp => [{
+ column => 'id',
+ transform => 'count',
+ aggregate => 1
+ }]},
+ from => {
+ acp => {
+ acqlid => {
+ fkey => 'id',
+ field => 'eg_copy_id',
+ type => 'left'
+ },
+ acn => {join => {bre => {}}}
+ }
+ },
+ where => {
+ '+bre' => {id => $li->eg_bib_id},
+ # don't count copies linked to the lineitem in question
+ '+acqlid' => {
+ '-or' => [
+ {lineitem => undef},
+ {lineitem => {'<>' => $li_id}}
+ ]
+ },
+ '+acn' => {
+ owning_lib => $U->get_org_descendants($org)
+ },
+ # NOTE: should the excluded copy statuses be an AOUS?
+ '+acp' => {status => {'not in' => [3, 4, 13, 17]}}
+ }
+ });
+
+ return $counts->[0]->{id};
+}
+
+
+
use OpenILS::Application::Cat::BibCommon;
use OpenILS::Application::Cat::AssetCommon;
use OpenILS::Application::Acq::Lineitem::BatchUpdate;
+use OpenILS::Application::Acq::Common;
my $U = 'OpenILS::Application::AppUtils';
+my $AC = 'OpenILS::Application::Acq::Common';
__PACKAGE__->register_method(
push(@{$fields->{jub} }, 'editor') if $$options{flesh_editor};
push(@{$fields->{jub} }, 'selector') if $$options{flesh_selector};
+ if ($$options{flesh_formulas}) {
+ push(@{$fields->{jub}}, 'distribution_formulas');
+ push(@{$fields->{acqdfa}}, 'formula');
+ push(@{$fields->{acqdfa}}, 'creator');
+ }
+
if($$options{flesh_li_details}) {
push(@{$fields->{jub} }, 'lineitem_details');
push(@{$fields->{acqlid}}, 'fund' ) if $$options{flesh_fund};
push(@{$fields->{acqlid}}, 'fund_debit' ) if $$options{flesh_fund_debit};
push(@{$fields->{acqlid}}, 'cancel_reason') if $$options{flesh_cancel_reason};
+ push(@{$fields->{acqlid}}, 'circ_modifier') if $$options{flesh_circ_modifier};
+ push(@{$fields->{acqlid}}, 'location') if $$options{flesh_location};
+ push(@{$fields->{acqlid}}, 'eg_copy_id') if $$options{flesh_copies};
}
if($$options{clear_marc}) { # avoid fetching marc blob
return $li;
}
+__PACKAGE__->register_method(
+ method => 'retrieve_lineitem_batch',
+ api_name => 'open-ils.acq.lineitem.retrieve.batch',
+ stream => 1,
+ max_bundle_count => 1,
+ signature => {
+ desc => q/
+ Retrieves a set of lineitems.
+ See open-ils.acq.lineitem.retrieve/,
+ params => [
+ {desc => 'Authentication token', type => 'string'},
+ {desc => 'Array of lineitem IDs to retrieve', type => 'array'},
+ {options => q/See open-ils.acq.lineitem.retrieve/}
+ ],
+ return => {desc => 'Stream of lineitems, Event on error'}
+ }
+);
+
+
+sub retrieve_lineitem_batch {
+ my($self, $client, $auth, $li_ids, $options) = @_;
+ my $e = new_editor(authtoken => $auth, xact => 1);
+ return $e->die_event unless $e->checkauth;
+
+ for my $li_id (@$li_ids) {
+ $client->respond({
+ id => $li_id,
+ lineitem => retrieve_lineitem_impl($e, $li_id, $options),
+ existing_copies => $AC->li_existing_copies($e, $li_id)
+ });
+ }
+
+ $e->rollback;
+
+ return undef;
+}
+
__PACKAGE__->register_method(
use MARC::File::XML (BinaryEncoding => 'UTF-8');
use Digest::MD5 qw(md5_hex);
use Data::Dumper;
+use OpenILS::Application::Acq::Common;
+my $AC = 'OpenILS::Application::Acq::Common';
$Data::Dumper::Indent = 0;
my $U = 'OpenILS::Application::AppUtils';
my($mgr, $li_id) = @_;
my $li = $mgr->editor->retrieve_acq_lineitem($li_id) or return 0;
+ return 0 unless ($li->state eq 'received' || $li->state eq 'on-order');
+
my $lid_ids = $mgr->editor->search_acq_lineitem_detail(
{lineitem => $li_id, recv_time => {'!=' => undef}}, {idlist => 1});
'RECEIVE_PURCHASE_ORDER', $li->purchase_order->ordering_agency
);
- receive_lineitem($mgr, $li_id) or return $e->die_event;
+ # Editor may have no die_event to return
+ receive_lineitem($mgr, $li_id) or return
+ $e->die_event || OpenILS::Event->new('ACQ_LI_RECEIVE_FAILED');
+
$mgr->respond;
}
return $e->die_event unless
$e->allowed('RECEIVE_PURCHASE_ORDER', $po->ordering_agency);
- $li = rollback_receive_lineitem($mgr, $li_id) or return $e->die_event;
+ unless ($li = rollback_receive_lineitem($mgr, $li_id)) {
+ return (
+ $e->die_event || # may not be an event here
+ OpenILS::Event->new('ACQ_LI_ROLLBACK_RECEIVE_FAILED')
+ );
+ }
my $result = {"li" => {$li->id => {"state" => $li->state}}};
if ($po->state eq "received") { # should happen first time, not after
__PACKAGE__->register_method(
method => 'activate_purchase_order',
- api_name => 'open-ils.acq.purchase_order.activate.dry_run'
+ api_name => 'open-ils.acq.purchase_order.activate.dry_run',
+ max_bundle_count => 1
);
__PACKAGE__->register_method(
method => 'activate_purchase_order',
api_name => 'open-ils.acq.purchase_order.activate',
+ max_bundle_count => 1,
signature => {
desc => q/Activates a purchase order. This updates the status of the PO / .
q/and Lineitems to 'on-order'. Activated PO's are ready for EDI delivery if appropriate./,
# retrieves a lineitem, fleshes its PO and PL, checks perms
# returns ($li, $evt, $org)
sub fetch_and_check_li {
- my $e = shift;
- my $li_id = shift;
- my $perm_mode = shift || 'read';
-
- my $li = $e->retrieve_acq_lineitem([
- $li_id,
- { flesh => 1,
- flesh_fields => {jub => ['purchase_order', 'picklist']}
- }
- ]) or return (undef, $e->die_event);
-
- my $org;
- if(my $po = $li->purchase_order) {
- $org = $po->ordering_agency;
- my $perms = ($perm_mode eq 'read') ? 'VIEW_PURCHASE_ORDER' : 'CREATE_PURCHASE_ORDER';
- return ($li, $e->die_event) unless $e->allowed($perms, $org);
-
- } elsif(my $pl = $li->picklist) {
- $org = $pl->org_unit;
- my $perms = ($perm_mode eq 'read') ? 'VIEW_PICKLIST' : 'CREATE_PICKLIST';
- return ($li, $e->die_event) unless $e->allowed($perms, $org);
- }
-
- return ($li, undef, $org);
+ my ($e, $li_id, $perm_mode) = @_;
+ return $AC->fetch_and_check_li($e, $li_id, $perm_mode);
}
my ($self, $client, $auth, $li_id) = @_;
my $e = new_editor("authtoken" => $auth);
return $e->die_event unless $e->checkauth;
-
- my ($li, $evt, $org) = fetch_and_check_li($e, $li_id);
- return 0 if $evt;
-
- # No fuzzy matching here (e.g. on ISBN). Only exact matches are supported.
- return 0 unless $li->eg_bib_id;
-
- my $counts = $e->json_query({
- select => {acp => [{
- column => 'id',
- transform => 'count',
- aggregate => 1
- }]},
- from => {
- acp => {
- acqlid => {
- fkey => 'id',
- field => 'eg_copy_id',
- type => 'left'
- },
- acn => {join => {bre => {}}}
- }
- },
- where => {
- '+bre' => {id => $li->eg_bib_id},
- # don't count copies linked to the lineitem in question
- '+acqlid' => {
- '-or' => [
- {lineitem => undef},
- {lineitem => {'<>' => $li_id}}
- ]
- },
- '+acn' => {
- owning_lib => $U->get_org_descendants($org)
- },
- # NOTE: should the excluded copy statuses be an AOUS?
- '+acp' => {status => {'not in' => [3, 4, 13, 17]}}
- }
- });
-
- return $counts->[0]->{id};
+ return $AC->li_existing_copies($e, $li_id);
}