--- /dev/null
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {UploadComponent} from './picklist/upload.component';
+
+@NgModule({
+ declarations: [
+ UploadComponent
+ ],
+ exports: [
+ UploadComponent
+ ],
+ imports: [
+ StaffCommonModule
+ ],
+ providers: []
+})
+
+export class AcqCommonModule {
+}
--- /dev/null
+<ng-template #dialogContent>
+ <form class="form-validated">
+ <div class="modal-header bg-info">
+ <h3 class="modal-title" i18n>Add Items to Selected Line Items</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>Line Item(s) selected:
+ <span *ngFor="let id of ids; last as isLast">
+ {{id}}<span *ngIf="!isLast">,</span>
+ </span>
+ </h4>
+ <eg-lineitem-copies (lineitemWithCopies)="lineitemWithCopies = $event" mode="multiAdd"></eg-lineitem-copies>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-success"
+ (click)="close(lineitemWithCopies)" 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-add-copies-dialog',
+ templateUrl: './add-copies-dialog.component.html'
+})
+
+export class AddCopiesDialogComponent extends DialogComponent {
+ @Input() ids: number[];
+ lineitemWithCopies: IdlObject;
+ constructor(private modal: NgbModal) { super(modal); }
+}
+
+
--- /dev/null
+<ng-template #dialogContent>
+ <form class="form-validated">
+ <div class="modal-header bg-info">
+ <h3 class="modal-title" i18n>Add Line Items to Purchase order</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 *ngIf="liIds && liIds.length">Line Item(s) selected:
+ <span *ngFor="let id of liIds; last as isLast">
+ {{id}}<span *ngIf="!isLast">,</span>
+ </span>
+ </h4>
+ <h4 i18n>Please select a PO and click "Add to Purchase Order" to add the line items,
+ or "Exit Dialog" to exit without adding the line items to a PO.</h4>
+ <eg-combobox domId="acq-add-to-po-dialog" name="acq-add-to-po-dialog"
+ [asyncSupportsEmptyTermClick]="true"
+ idlClass="acqpo" [idlQueryAnd]="{state: ['new', 'pending']}"
+ idlIncludeLibraryInLabel="ordering_agency"
+ [(ngModel)]="po"></eg-combobox>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-success" [disabled]="!po"
+ (click)="close(po.id)" i18n>Add to Purchase Order</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-add-to-po-dialog',
+ templateUrl: './add-to-po-dialog.component.html'
+})
+
+export class AddToPoDialogComponent extends DialogComponent {
+ @Input() ids: number[];
+ po: ComboboxEntry;
+ constructor(private modal: NgbModal) { super(modal); }
+}
+
+
-<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>
+<eg-lineitem-alert-dialog #confirmAlertsDialog></eg-lineitem-alert-dialog>
+<eg-acq-cancel-dialog recordType="lid" #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>Shelving 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" *ngIf="!batchAdd" i18n>Callnumber</div>
<div class="flex-1 p-1" i18n>
- <ng-container *ngIf="!hideBarcode">Barcode</ng-container>
+ <ng-container *ngIf="!hideBarcode && !batchAdd">Barcode</ng-container>
</div>
+ <div class="flex-1 p-1" *ngIf="!hasEditableCopies()" i18n>Receiver</div>
<div class="flex-1 p-1"></div>
<div class="flex-1 p-1"></div>
</div>
<div class="pt-2 bg-light border border-secondary border-top-0 rounded-bottom">
<eg-lineitem-copy-attrs (batchApplyRequested)="batchApplyAttrs($event)"
+ [batchAdd]="batchAdd"
[batchMode]="true"> </eg-lineitem-copy-attrs>
</div>
</ng-container>
<div class="batch-copy-row"
*ngFor="let copy of copies(); let idx = index">
<eg-lineitem-copy-attrs
+ [batchAdd]="batchAdd"
(receiveRequested)="receiveCopy($event)"
(unReceiveRequested)="unReceiveCopy($event)"
(deleteRequested)="deleteCopy($event)"
(cancelRequested)="cancelCopy($event)"
+ [showReceiver]="!hasEditableCopies()"
+ (becameDirty)="becameDirty.emit(true)"
[rowIndex]="idx + 1" [lineitem]="lineitem" [copy]="copy">
</eg-lineitem-copy-attrs>
</div>
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';
+import {LineitemAlertDialogComponent} from './lineitem-alert-dialog.component';
const BATCH_FIELDS = [
'owning_lib',
export class LineitemBatchCopiesComponent implements OnInit {
@Input() lineitem: IdlObject;
+ @Input() batchAdd = false;
- @ViewChild('confirmAlertsDialog') confirmAlertsDialog: ConfirmDialogComponent;
+ @Output() becameDirty = new EventEmitter<Boolean>();
+
+ @ViewChild('confirmAlertsDialog') confirmAlertsDialog: LineitemAlertDialogComponent;
@ViewChild('cancelDialog') cancelDialog: CancelDialogComponent;
// Current alert that needs confirming
alertText: IdlObject;
+ liId: number;
+ liTitle: string;
+ alertComment: string;
constructor(
private evt: EventService,
private liService: LineitemService
) {}
- ngOnInit() {}
+ ngOnInit() {
+ if (!this.lineitem) {
+ this.lineitem = this.idl.create('jub');
+ const copy = this.idl.create('acqlid');
+ copy.isnew(true);
+ this.lineitem.lineitem_details([copy]);
+ }
+ }
// Propagate values from the batch edit bar into the indivudual LID's
batchApplyAttrs(copyTemplate: IdlObject) {
this.lineitem.lineitem_details().forEach(copy => {
copy[field](val);
copy.ischanged(true); // isnew() takes precedence
+ this.becameDirty.emit(true);
});
});
}
} else {
// Requires a Save Changes action.
copy.isdeleted(true);
+ this.becameDirty.emit(true);
}
}
}
receiveCopy(copy: IdlObject) {
- this.checkLiAlerts().then(ok => {
+ this.liService.checkLiAlerts([this.lineitem], this.confirmAlertsDialog).then(ok => {
this.net.request(
'open-ils.acq',
'open-ils.acq.lineitem_detail.receive',
).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;
- }
-
hasEditableCopies(): boolean {
if (this.lineitem) {
const copies = this.lineitem.lineitem_details();
--- /dev/null
+<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>Shelving 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"></div>
+ </div>
+</ng-template>
+
+<ng-template #dialogContent>
+ <form class="form-validated">
+ <div class="modal-header bg-info">
+ <h3 class="modal-title" i18n>Batch Update Items on Selected Line Items</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>Line Item(s) selected:
+ <span *ngFor="let id of ids; last as isLast">
+ {{id}}<span *ngIf="!isLast">,</span>
+ </span>
+ </h4>
+ <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'
+ [(ngModel)]="copyCount" [ngModelOptions]="{standalone: true}" type="text"/>
+
+ <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"
+ [asyncSupportsEmptyTermClick]="true" [startsWith]="true"
+ [idlQuerySort]="{acqdf: 'name'}"
+ #distribFormCbox domId="distrib-formula-cbox"
+ [(ngModel)]="selectedFormula" [ngModelOptions]="{standalone: true}">
+ </eg-combobox>
+ </span>
+ </div>
+ </div>
+
+ <hr class="m-1 p-1"/>
+ <ng-container>
+ <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 #copyAttributes [gatherParamsOnly]="true"
+ (templateCopy)="templateCopy = $event" ></eg-lineitem-copy-attrs>
+ </div>
+ </ng-container>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-success"
+ (click)="close(compileBatchChange())" [disabled]="!canApply()" i18n>Batch Update</button>
+ <button type="button" class="btn btn-warning"
+ (click)="close()" i18n>Cancel</button>
+ </div>
+ </form>
+</ng-template>
+
--- /dev/null
+import {Component, Input} from '@angular/core';
+import {Observable} from 'rxjs';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {IdlObject} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.service';
+import {AuthService} from '@eg/core/auth.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {LineitemCopyAttrsComponent} from './copy-attrs.component';
+
+@Component({
+ selector: 'eg-acq-batch-update-copies-dialog',
+ templateUrl: './batch-update-copies-dialog.component.html'
+})
+
+export class BatchUpdateCopiesDialogComponent extends DialogComponent {
+
+ @Input() ids: number[];
+
+ copyCount = '';
+ selectedFormula: ComboboxEntry;
+ formulaFilter = {owner: []};
+ templateCopy: IdlObject;
+
+ constructor(
+ private modal: NgbModal,
+ private org: OrgService,
+ private auth: AuthService
+ ) {
+ super(modal);
+ }
+
+ open(args?: NgbModalOptions): Observable<any> {
+ if (!args) {
+ args = {};
+ }
+
+ this.copyCount = '';
+ this.selectedFormula = null;
+ this.formulaFilter.owner =
+ this.org.fullPath(this.auth.user().ws_ou(), true);
+
+ return super.open(args);
+ }
+
+ canApply(): boolean {
+ if (!this.templateCopy) { return false; }
+
+ const _copyCount = parseInt(this.copyCount, 10);
+ if ((_copyCount && _copyCount > 0) ||
+ this.selectedFormula?.id ||
+ this.templateCopy.owning_lib() ||
+ this.templateCopy.location() ||
+ this.templateCopy.collection_code() ||
+ this.templateCopy.fund() ||
+ this.templateCopy.circ_modifier()) {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ compileBatchChange(): any {
+ const changes = {
+ _dist_formula: this.selectedFormula?.id
+ };
+ const _copyCount = parseInt(this.copyCount, 10);
+ if (_copyCount && _copyCount > 0) {
+ changes['item_count'] = _copyCount;
+ }
+ if (this.templateCopy.owning_lib()) {
+ changes['owning_lib'] = this.templateCopy.owning_lib();
+ }
+ if (this.templateCopy.location()) {
+ changes['location'] = this.templateCopy.location();
+ }
+ if (this.templateCopy.collection_code()) {
+ changes['collection_code'] = this.templateCopy.collection_code();
+ }
+ if (this.templateCopy.fund()) {
+ changes['fund'] = this.templateCopy.fund();
+ }
+ if (this.templateCopy.circ_modifier()) {
+ changes['circ_modifier'] = this.templateCopy.owning_lib();
+ }
+ return changes;
+ }
+
+}
+
+
--- /dev/null
+.bib-finder-results-row:nth-child(even) {
+ background-color: rgba(0,0,0,.03);
+}
--- /dev/null
+<ng-template #dialogContent>
+ <form class="form-validated">
+ <div class="modal-header bg-info">
+ <h3 class="modal-title" i18n>Link Line Item to Catalog</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>Line Item: {{liId}}</h4>
+ <div class="input-group">
+ <label for="searchQuery" class="mr-1" i18n>Search catalog for</label>
+ <input type="text" [(ngModel)]="queryString" [ngModelOptions]="{standalone: true}"
+ class="form-control" id="searchQuery">
+ <button type="submit" (click)="submitSearch()" class="btn btn-primary"
+ [disabled]="doingSearch || queryString.length < 1" i18n>Submit</button>
+ </div>
+ <div class="row">
+ <div class="col-12">
+ <eg-progress-inline *ngIf="doingSearch"></eg-progress-inline>
+ </div>
+ </div>
+ <div class="row mt-2">
+ <div class="col-6">
+ <h5 i18n>Search results</h5>
+ <div class="mt-1 pt-1 border-top">
+ <div *ngFor="let rec of results" class="bib-finder-results-row row mt-1">
+ <div class="col-3">
+ <button class="btn btn-success mr-1" (click)="close(rec.id)" i18n>Link</button>
+ <button class="btn btn-outline-dark mr-1" (click)="bibToDisplay = rec.id" i18n>View MARC</button>
+ </div>
+ <div class="col-9">
+ <span class="pr-1" i18n>Record {{rec.id}}:</span>
+ <span class="pr-1">{{rec.display.title}}</span>
+ <span class="pr-1">{{rec.display.author}}</span>
+ <span class="pr-1">{{rec.display.isbn}}</span>
+ <span class="pr-1">{{rec.display.issn}}</span>
+ <span class="pr-1">{{rec.display.pubdate}}</span>
+ <span class="pr-1">{{rec.display.publisher}}</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="col-6">
+ <h5 i18n>MARC Display</h5>
+ <eg-marc-html recordType="bib" [recordId]="bibToDisplay" *ngIf="bibToDisplay"></eg-marc-html>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-warning"
+ (click)="close()" i18n>Cancel</button>
+ </div>
+ </form>
+</ng-template>
+
--- /dev/null
+import {Component, Input, ViewChild} from '@angular/core';
+import {Observable} from 'rxjs';
+import {map} from 'rxjs/operators';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NetService} from '@eg/core/net.service';
+import {EgEvent, EventService} from '@eg/core/event.service';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {LineitemService} from './lineitem.service';
+import {BibRecordService, BibRecordSummary} from '@eg/share/catalog/bib-record.service';
+
+@Component({
+ selector: 'eg-acq-bib-finder-dialog',
+ styleUrls: ['./bib-finder-dialog.component.css'],
+ templateUrl: './bib-finder-dialog.component.html'
+})
+
+export class BibFinderDialogComponent extends DialogComponent {
+ @Input() liId: number;
+
+ queryString: string;
+ lineitem: IdlObject;
+ results: BibRecordSummary[] = [];
+ doingSearch = false;
+ bibToDisplay: number;
+
+ constructor(
+ private modal: NgbModal,
+ private net: NetService,
+ private evt: EventService,
+ private bib: BibRecordService,
+ private liService: LineitemService
+ ) {
+ super(modal);
+ }
+
+ open(args?: NgbModalOptions): Observable<any> {
+ if (!args) {
+ args = {};
+ }
+
+ this.queryString = '';
+ this.results.length = 0;
+ this.doingSearch = false;
+ this.bibToDisplay = null;
+ this.liService.getFleshedLineitems([this.liId], {fromCache: true}).subscribe(liStruct => {
+ this.lineitem = liStruct.lineitem;
+ this.queryString = this._buildDefaultQuery(this.lineitem);
+ });
+ return super.open(args);
+ }
+
+ _buildDefaultQuery(li: IdlObject): string {
+ let query = '';
+ ['title', 'author'].forEach(field => {
+ const attr = this.liService.getFirstAttributeValue(li, field);
+ if (attr.length) {
+ query += field + ':' + attr + ' ';
+ }
+ });
+ ['isbn', 'issn', 'upc'].forEach(field => {
+ const attr = this.liService.getFirstAttributeValue(li, field);
+ if (attr.length) {
+ query += 'identifier|' + field + ':' + attr + ' ';
+ }
+ });
+ return query;
+ }
+
+ submitSearch() {
+ this.results.length = 0;
+ this.bibToDisplay = null;
+ this.doingSearch = true;
+ this.net.request(
+ 'open-ils.search',
+ 'open-ils.search.biblio.multiclass.query.staff',
+ {limit: 15}, this.queryString, 1
+ ).subscribe(response => {
+ const evt = this.evt.parse(response);
+ if (evt) {
+ this.doingSearch = false;
+ return;
+ }
+ const ids = response.ids.map(x => x[0]);
+ if (ids.length < 1) {
+ this.doingSearch = false;
+ return;
+ }
+ const bibSummaries: {[id: number]: BibRecordSummary} = {};
+ this.bib.getBibSummaries(ids).subscribe(
+ summary => bibSummaries[summary.id] = summary,
+ err => {},
+ () => {
+ this.doingSearch = false;
+ ids.forEach(id => {
+ if (bibSummaries[id]) {
+ this.results.push(bibSummaries[id]);
+ }
+ });
+ }
+ );
+ });
+ }
+}
+
+
// Append fields to the document
dfNode.setAttribute('tag', '' + tags[0]);
- dfNode.setAttribute('ind1', ' ');
+ if (attr.code() === 'upc') {
+ dfNode.setAttribute('ind1', '1');
+ } else {
+ dfNode.setAttribute('ind1', ' ');
+ }
dfNode.setAttribute('ind2', ' ');
sfNode.setAttribute('code', '' + subfields[0]);
const tNode = doc.createTextNode(value);
<ng-template #dialogContent>
<form class="form-validated">
<div class="modal-header bg-info">
- <h3 class="modal-title" i18n>Cancel</h3>
+ <h3 class="modal-title" *ngIf="recordType === 'po'" i18n>Confirm Order Cancellation</h3>
+ <h3 class="modal-title" *ngIf="recordType === 'li'" i18n>Confirm Line Item Cancellation</h3>
+ <h3 class="modal-title" *ngIf="recordType === 'lid'" i18n>Confirm Item Cancellation</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>
+ <h4 *ngIf="recordType === 'po'" i18n>Please select a cancel reason and click "Apply" to cancel the order,
+ or "Exit Dialog" to exit without cancelling the order.</h4>
+ <h4 *ngIf="recordType === 'li'" i18n>Please select a cancel reason and click "Apply" to cancel the line item,
+ or "Exit Dialog" to exit without cancelling the line item.</h4>
+ <h4 *ngIf="recordType === 'lid'" i18n>Please select a cancel reason and click "Apply" to cancel the item,
+ or "Exit Dialog" to exit without cancelling the item.</h4>
<eg-combobox domId="acq-cancel-dialog" name="acq-cancel-dialog"
+ [asyncSupportsEmptyTermClick]="true"
idlClass="acqcr" [(ngModel)]="cancelReason"></eg-combobox>
</div>
<div class="modal-footer">
})
export class CancelDialogComponent extends DialogComponent {
+ @Input() recordType = 'po';
cancelReason: number;
constructor(private modal: NgbModal) { super(modal); }
}
--- /dev/null
+<ng-template #dialogContent>
+ <form class="form-validated">
+ <div class="modal-header bg-info">
+ <h3 class="modal-title" i18n>Apply Claim Policy</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>Line Item(s) selected:
+ <span *ngFor="let id of ids; last as isLast">
+ {{id}}<span *ngIf="!isLast">,</span>
+ </span>
+ </h4>
+ <h4 i18n>Select a claim policy:</h4>
+ <eg-combobox domId="acq-claim-policy-dialog" name="acq-claim-policy-dialog"
+ [asyncSupportsEmptyTermClick]="true"
+ idlClass="acqclp" [(ngModel)]="claimPolicy"></eg-combobox>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-success" [disabled]="!claimPolicy"
+ (click)="close(claimPolicy.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-claim-policy-dialog',
+ templateUrl: './claim-policy-dialog.component.html'
+})
+
+export class ClaimPolicyDialogComponent extends DialogComponent {
+ @Input() ids: number[];
+ claimPolicy: number;
+ constructor(private modal: NgbModal) { super(modal); }
+}
+<h3 *ngIf="mode !== 'multiAdd'" class="mt-3" i18n>Items for Line Item {{lineitem?.id()}} ({{getTitle(lineitem)}})</h3>
+
+<eg-confirm-dialog #leaveConfirm
+ i18n-dialogTitle i18n-dialogBody
+ dialogTitle="Unsaved Changes Warning"
+ dialogBody="There are unsaved changes. Are you sure you want to leave?">
+</eg-confirm-dialog>
<div class="row mt-3 mb-1">
<div class="col-lg-12 form-inline">
<span class="ml-3">
<eg-combobox idlClass="acqdf" [idlQueryAnd]="formulaFilter"
[asyncSupportsEmptyTermClick]="true" [startsWith]="true"
+ [idlQuerySort]="{acqdf: 'name'}"
#distribFormCbox domId="distrib-formula-cbox">
</eg-combobox>
</span>
[disabled]="!distribFormCbox.selectedId || liLocked"
(click)="applyFormula(distribFormCbox.selectedId)" i18n>Apply</button>
- <button class="btn btn-sm btn-success ml-auto" [disabled]="liLocked"
+ <button class="btn btn-sm btn-success ml-auto" [disabled]="liLocked" *ngIf="mode !== 'multiAdd'"
(click)="save()" i18n>Save Changes</button>
</div>
<ng-container *ngIf="lineitem && !saving">
- <div class="card tight-card" *ngIf="lineitem.distribution_formulas().length">
+ <div class="card tight-card" *ngIf="lineitem.distribution_formulas().length && mode !== 'multiAdd'">
<div class="card-header" i18n>Distribution formulas applied to this lineitem</div>
<div class="card-body">
<ul class="p-0 m-0">
</div>
</div>
- <eg-lineitem-batch-copies [lineitem]="lineitem"></eg-lineitem-batch-copies>
+ <eg-lineitem-batch-copies
+ [lineitem]="lineitem" [batchAdd]="mode === 'multiAdd'"
+ (becameDirty)="dirty = true"
+ ></eg-lineitem-batch-copies>
</ng-container>
import {Component, OnInit, AfterViewInit, Input, Output, EventEmitter,
ViewChild} from '@angular/core';
import {Router, ActivatedRoute, ParamMap} from '@angular/router';
-import {tap} from 'rxjs/operators';
+import {Observable, of} from 'rxjs';
+import {tap, map} 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 {LineitemService, FleshCacheParams} from './lineitem.service';
import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
import {ItemLocationService} from '@eg/share/item-location-select/item-location-select.service';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
const FORMULA_FIELDS = [
'owning_lib',
}
@Component({
+ selector: 'eg-lineitem-copies',
templateUrl: 'copies.component.html'
})
export class LineitemCopiesComponent implements OnInit, AfterViewInit {
+
static newCopyId = -1;
+ // modes are 'normal' and 'multiAdd'
+ // normal = manage copies for a single line item whose
+ // ID is taken from the route
+ // multiAdd = embedded in a modal and applying the results
+ // to selected LIs
+ @Input() mode = 'normal';
+
+ // emited only in multiAdd mode
+ @Output() lineitemWithCopies = new EventEmitter<IdlObject>();
+
lineitemId: number;
lineitem: IdlObject;
copyCount = 1;
batchOwningLib: IdlObject;
batchFund: ComboboxEntry;
batchCopyLocId: number;
+ dirty = false;
saving = false;
progressMax = 0;
progressValue = 0;
// Can any changes be applied?
liLocked = false;
+ @ViewChild('leaveConfirm', { static: true }) leaveConfirm: ConfirmDialogComponent;
+
constructor(
private route: ActivatedRoute,
private idl: IdlService,
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(); }
- }
- });
+ if (this.mode === 'multiAdd') {
+ this.load();
+ } else {
+ // normal mode, we're checking the route to initalize
+ // ourselves
+ 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();
}
params = {toCache: true, fromCache: true};
}
- return this.liService.getFleshedLineitems([this.lineitemId], params)
- .pipe(tap(liStruct => this.lineitem = liStruct.lineitem)).toPromise()
- .then(_ => {
- this.liLocked =
- this.lineitem.state().match(/on-order|received|cancelled/);
- })
- .then(_ => this.applyCount());
+ if (this.mode === 'multiAdd') {
+ this.lineitem = this.idl.create('jub');
+ this.lineitem.lineitem_details([]);
+ this.lineitem.distribution_formulas([]);
+ this.liLocked = false; // trusting our invoker in multiAdd mode
+ this.applyCount();
+ this.lineitemWithCopies.emit(this.lineitem);
+ return Promise.resolve(true);
+ } else {
+ return this.liService.getFleshedLineitems([this.lineitemId], params)
+ .pipe(tap(liStruct => this.lineitem = liStruct.lineitem)).toPromise()
+ .then(_ => {
+ this.liLocked =
+ this.lineitem.state().match(/on-order|received|cancelled/);
+ })
+ .then(_ => this.applyCount());
+ }
}
ngAfterViewInit() {
while (copies.length < this.copyCount) {
const copy = this.idl.create('acqlid');
copy.id(LineitemCopiesComponent.newCopyId--);
+ copy.owning_lib(this.auth.user().ws_ou());
copy.isnew(true);
copy.lineitem(this.lineitem.id());
copies.push(copy);
+ this.dirty = true;
}
if (copies.length > this.copyCount) {
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);
- });
+ if (this.mode === 'multiAdd') {
+ app.isnew(true);
+ this.lineitem.distribution_formulas().push(app);
+ this.dirty = true;
+ } else {
+ 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
} else {
copy[field](val);
+ this.dirty = true;
}
});
() => this.load({toCache: true}).then(_ => {
this.liService.activateStateChange.emit(this.lineitem.id());
this.saving = false;
+ this.dirty = false;
})
);
}
);
});
}
+
+ getTitle(li: IdlObject): string {
+ if (!li) { return ''; }
+ return this.liService.getFirstAttributeValue(li, 'title');
+ }
+
+ canDeactivate(): Observable<boolean> {
+ if (this.dirty) {
+ return this.leaveConfirm.open().pipe(map(confirmed => {
+ if (confirmed) {
+ // fire-and-forget fetching the line item to restore it
+ // to its previous state
+ this.liService.getFleshedLineitems([ this.lineitemId ], {toCache: true}).toPromise();
+ }
+ return confirmed;
+ }));
+ } else {
+ return of(true);
+ }
+ }
}
--- /dev/null
+.fund-balance-state-stop {
+ color: #c00;
+ font-weight: bold;
+}
+.fund-balance-state-warning {
+ color: #c93;
+}
<eg-org-select #owningLibSelect placeholder="Owning Branch..."
i18n-placeholder [readOnly]="fieldIsDisabled('owning_lib')"
[applyOrgId]="copy.owning_lib()"
+ [limitPerms]="['CREATE_PICKLIST','CREATE_PURCHASE_ORDER']"
(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()" [startsWith]="true"
+ [contextOrgId]="copy.owning_lib()" [loadAsync]="false"
(valueChange)="valueChange('location', $event)"
permFilter="CREATE_PICKLIST" [showUnsetString]="false">
</eg-item-location-select>
<div class="flex-1 p-1">
<eg-combobox idlClass="acqf" placeholder="Fund..." i18n-placeholder
[readOnly]="fieldIsDisabled('fund')"
+ [asyncSupportsEmptyTermClick]="true"
#fundSelector [entries]="fundEntries"
+ [displayTemplate]="fundTmpl"
[selectedId]="copy.fund()" (onChange)="valueChange('fund', $event)"
[idlQuerySort]="{acqf: 'year DESC, code'}"
[idlQueryAnd]="{active: 't'}">
<eg-combobox idlClass="ccm" placeholder="Circ Modifier..." i18n-placeholder
[readOnly]="fieldIsDisabled('circ_modifier')"
#circModSelector [entries]="circModEntries"
+ [asyncSupportsEmptyTermClick]="true"
[selectedId]="copy.circ_modifier()"
(onChange)="valueChange('circ_modifier', $event)">
</eg-combobox>
</div>
- <div class="flex-1 p-1">
+ <div class="flex-1 p-1" *ngIf="!batchAdd && !gatherParamsOnly">
<ng-container *ngIf="fieldIsDisabled('cn_label')">
<span>{{copy.cn_label()}}</span>
</ng-container>
<button class="btn btn-outline-dark"
(click)="batchApplyRequested.emit(copy)" i18n>Batch Update</button>
</ng-container>
- <ng-container *ngIf="!batchMode">
+ <ng-container *ngIf="!batchMode && !batchAdd && !gatherParamsOnly">
<ng-container *ngIf="fieldIsDisabled('barcode')">
<span>{{copy.barcode()}}</span>
</ng-container>
</ng-container>
</ng-container>
</div>
- <ng-container *ngIf="!embedded">
+ <div class="flex-1 p-1" *ngIf="showReceiver">
+ {{copy.receiver()?.usrname()}}
+ </div>
+ <ng-container *ngIf="!embedded && !gatherParamsOnly">
<div class="flex-2 p-1 pr-2 pl-2">
<ng-container *ngIf="!batchMode">
<ng-container *ngIf="disposition() == 'pre-order'">
<span class="material-icons">delete</span>
</button>
</ng-container>
- <ng-container *ngIf="disposition() == 'on-order'">
+ <ng-container *ngIf="disposition() == 'on-order' || disposition() == 'delayed'">
<a href="javascript:;" (click)="receiveRequested.emit(copy)" i18n>Mark Received</a>
</ng-container>
<ng-container *ngIf="disposition() == 'received'">
<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>
+ <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()}}">
</ng-container>
</div>
+<ng-template #fundTmpl let-r="result" i18n>
+ <span [ngClass]="{'fund-balance-state-stop': checkFundBalance(r.fm.id()) === 'stop',
+ 'fund-balance-state-warning': checkFundBalance(r.fm.id()) === 'warning'}">{{r.label}}</span>
+</ng-template>
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 {LineitemService, COPY_ORDER_DISPOSITION} from './lineitem.service';
import {ComboboxComponent, ComboboxEntry} from '@eg/share/combobox/combobox.component';
import {ItemLocationService} from '@eg/share/item-location-select/item-location-select.service';
@Component({
templateUrl: 'copy-attrs.component.html',
+ styleUrls: ['copy-attrs.component.css'],
selector: 'eg-lineitem-copy-attrs'
})
export class LineitemCopyAttrsComponent implements OnInit {
@Input() lineitem: IdlObject;
@Input() rowIndex: number;
+ @Input() batchAdd = false;
+ @Input() gatherParamsOnly = false;
+
+ @Output() becameDirty = new EventEmitter<Boolean>();
+ @Output() templateCopy = new EventEmitter<IdlObject>();
fundEntries: ComboboxEntry[];
+ _fundBalanceCache: string[] = [];
+ _inflight: Promise<string>[] = [];
circModEntries: ComboboxEntry[];
private _copy: IdlObject;
// Always read-only.
@Input() embedded = false;
+ @Input() showReceiver = false;
+
// Emits an 'acqlid' object;
@Output() batchApplyRequested: EventEmitter<IdlObject> = new EventEmitter<IdlObject>();
@Output() deleteRequested: EventEmitter<IdlObject> = new EventEmitter<IdlObject>();
private idl: IdlService,
private net: NetService,
private auth: AuthService,
+ private org: OrgService,
private loc: ItemLocationService,
private liService: LineitemService
) {}
ngOnInit() {
- if (this.batchMode) { // stub batch copy
+ if (this.gatherParamsOnly) {
+ this.batchMode = false;
+ this.batchAdd = false;
+ }
+
+ if (this.batchMode || this.gatherParamsOnly) { // stub batch copy
this.copy = this.idl.create('acqlid');
this.copy.isnew(true);
-
+ this.templateCopy.emit(this.copy);
} else {
// When a batch selector value changes, duplicate the selected
const announce: any = {};
this.copy.ischanged(true);
+ if (!this.batchMode) {
+ if (field !== 'owning_lib') {
+ this.becameDirty.emit(true);
+ } else {
+ // FIXME eg-org-select current send needless change
+ // events, so we need to check
+ if (entry && this.copy[field]() !== entry.id()) {
+ this.becameDirty.emit(true);
+ }
+ }
+ }
switch (field) {
}
}
+ // copied from combobox to get the label right for funds
+ getOrgShortname(ou: any) {
+ if (typeof ou === 'object') {
+ return ou.shortname();
+ } else {
+ return this.org.get(ou).shortname();
+ }
+ }
+
// Tell our inputs about the values we know we need
// Values will be pre-cached in the liService
+ //
+ // TODO: figure out a better way to do this so that we
+ // don't need to duplicate the code to format
+ // the display labels for funds correctly
setInitialOptions(copy: IdlObject) {
if (copy.fund()) {
const fund = this.liService.fundCache[copy.fund()];
- this.fundEntries = [{id: fund.id(), label: fund.code(), fm: fund}];
+ this.fundEntries = [{
+ id: fund.id(),
+ label: fund.code() + ' (' + fund.year() + ')' +
+ ' (' + this.getOrgShortname(fund.org()) + ')',
+ fm: fund
+ }];
}
if (copy.circ_modifier()) {
}
}
+ checkFundBalance(fundId: number): string {
+ if (this.liService.fundCache[fundId] && this.liService.fundCache[fundId]._balance) {
+ return this.liService.fundCache[fundId]._balance;
+ }
+ if (this._fundBalanceCache[fundId]) {
+ return this._fundBalanceCache[fundId];
+ }
+ if (this._inflight[fundId]) {
+ return 'ok';
+ }
+ this._inflight[fundId] = this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.fund.check_balance_percentages',
+ this.auth.token(), fundId
+ ).toPromise().then(r => {
+ if (r[0]) {
+ this._fundBalanceCache[fundId] = 'stop';
+ } else if (r[1]) {
+ this._fundBalanceCache[fundId] = 'warning';
+ } else {
+ this._fundBalanceCache[fundId] = 'ok';
+ }
+ if (this.liService.fundCache[fundId]) {
+ this.liService.fundCache[fundId]['_balance'] = this._fundBalanceCache[fundId];
+ }
+ delete this._inflight[fundId];
+ return this._fundBalanceCache[fundId];
+ });
+ }
+
fieldIsDisabled(field: string) {
if (this.batchMode) { return false; }
+ if (this.gatherParamsOnly) { return false; }
if (this.embedded || // inline expandy view
this.copy.isdeleted() ||
--- /dev/null
+
+<h3 *ngIf="!activatePo" class="m-2" i18n>Load Bibs and Items</h3>
+<h3 *ngIf="activatePo" class="m-2" i18n>Load Bibs and Items, then Activate Order</h3>
+
+<div class="w-100 m-2">
+ <eg-acq-upload mode="getImportParams" [customAction]="createAssets"></eg-acq-upload>
+</div>
+
+<eg-progress-inline *ngIf="creatingAssets"></eg-progress-inline>
+
+<div class="w-100 m-2" *ngIf="creationRequested">
+ <h4 i18n>Bib and Item Creation Status</h4>
+ <div class="row">
+ <div class="col-2" i18n>Line Items Processed</div>
+ <div class="col-1">{{creationStatus.liProcessed}}</div>
+ </div>
+ <div class="row">
+ <div class="col-2" i18n>Vandelay Records Processed</div>
+ <div class="col-1">{{creationStatus.vqbrProcessed}}</div>
+ </div>
+ <div class="row">
+ <div class="col-2" i18n>Bib Records Merged/Imported</div>
+ <div class="col-1">{{creationStatus.bibsProcessed}}</div>
+ </div>
+ <div class="row">
+ <div class="col-2" i18n>Acquisitions Items Processed</div>
+ <div class="col-1">{{creationStatus.lidProcessed}}</div>
+ </div>
+ <div class="row">
+ <div class="col-2" i18n>Debits Encumbered</div>
+ <div class="col-1">{{creationStatus.debitsProcessed}}</div>
+ </div>
+ <div class="row">
+ <div class="col-2" i18n>Real Items Processed</div>
+ <div class="col-1">{{creationStatus.copiesProcessed}}</div>
+ </div>
+
+ <h4 i18n class="mt-2" *ngIf="creationErrors.length">Errors encountered</h4>
+ <div class="row" *ngFor="let evt of creationErrors">
+ <div class="col-2 alert alert-warning">{{evt.textcode}}</div>
+ <div class="col-5 alert alert-warning">{{evt.desc}}</div>
+ </div>
+</div>
--- /dev/null
+import {Component, OnInit, Input, Output} from '@angular/core';
+import {ActivatedRoute, Router, ParamMap, NavigationStart} from '@angular/router';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {EventService, EgEvent} from '@eg/core/event.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {AuthService} from '@eg/core/auth.service';
+import {LineitemService} from './lineitem.service';
+import {UploadComponent} from '../picklist/upload.component';
+
+
+interface AssetCreationResponse {
+ liProcessed: number;
+ vqbrProcessed: number;
+ bibsProcessed: number;
+ lidProcessed: number;
+ debitsProcessed: number;
+ copiesProcessed: number;
+}
+
+@Component({
+ templateUrl: 'create-assets.component.html'
+})
+export class CreateAssetsComponent implements OnInit {
+
+ targetPo: number;
+ creationRequested = false;
+ creatingAssets = false;
+ activatePo = false;
+
+ creationStatus: AssetCreationResponse = {
+ liProcessed: 0,
+ vqbrProcessed: 0,
+ bibsProcessed: 0,
+ lidProcessed: 0,
+ debitsProcessed: 0,
+ copiesProcessed: 0
+ };
+ creationErrors: EgEvent[] = [];
+
+ constructor(
+ private router: Router,
+ private route: ActivatedRoute,
+ private idl: IdlService,
+ private auth: AuthService,
+ private net: NetService,
+ private evt: EventService,
+ private pcrud: PcrudService,
+ private liService: LineitemService
+ ) { }
+
+ ngOnInit() {
+ this.activatePo = history.state.activatePo ? true : false;
+ this.route.parent.paramMap.subscribe((params: ParamMap) => {
+ this.targetPo = +params.get('poId');
+ });
+ }
+
+ // using arrow notion here because we want 'this' to
+ // refer to CreateAssetsComponent, not the component
+ // that createAssets is passed to
+ createAssets = (args: Object) => {
+ this.creatingAssets = true;
+ this.creationRequested = true;
+ this.creationErrors = [];
+
+ const assetArgs = {
+ vandelay: args['vandelay']
+ };
+
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.purchase_order.assets.create',
+ this.auth.token(),
+ this.targetPo,
+ assetArgs
+ ).subscribe(
+ resp => {
+ const evt = this.evt.parse(resp);
+ if (evt) {
+ this.creationErrors.push(evt);
+ } else {
+ this.creationStatus['liProcessed'] = resp.li;
+ this.creationStatus['vqbrProcessed'] = resp.vqbr;
+ this.creationStatus['bibsProcessed'] = resp.bibs;
+ this.creationStatus['lidProcessed'] = resp.lid;
+ this.creationStatus['debitsProcessed'] = resp.debits_accrued;
+ this.creationStatus['copiesProcessed'] = resp.copies;
+ }
+ },
+ err => {},
+ () => {
+ if (!this.creationErrors.length) {
+ this.creatingAssets = false;
+ if (this.activatePo) {
+ this.router.navigate(
+ ['/staff/acq/po/' + this.targetPo],
+ { state: { finishPoActivation: true } }
+ );
+ }
+ }
+ }
+ );
+ }
+}
+
--- /dev/null
+<ng-template #dialogContent>
+ <form class="form-validated">
+ <div class="modal-header bg-info">
+ <h3 class="modal-title" i18n>Confirm Deletion of Line Items</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>Line Item(s) selected:
+ <span *ngFor="let id of ids; last as isLast">
+ {{id}}<span *ngIf="!isLast">,</span>
+ </span>
+ </h4>
+ <h4 i18n>Are you sure you want to delete the selected line items?</h4>
+ <h4 i18n>Please click "Apply" to delete line items or "Exit Dialog"
+ to exit without deleting line items.</h4>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-success"
+ (click)="close(true)" 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-delete-lineitems-dialog',
+ templateUrl: './delete-lineitems-dialog.component.html'
+})
+
+export class DeleteLineitemsDialogComponent extends DialogComponent {
+ @Input() ids: number[];
+ constructor(private modal: NgbModal) { super(modal); }
+}
+
+
<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
+ Changes to line items that are linked to catalog records will
not result in changes to the cataloged record.
</div>
<eg-marc-editor [recordXml]="lineitem.marc()" [inPlaceMode]="true"
--- /dev/null
+<ng-template #dialogContent>
+ <form class="form-validated">
+ <div class="modal-header bg-info">
+ <h3 class="modal-title" i18n>Export Single Attribute List for Selected Line Items</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>Line Item(s) selected:
+ <span *ngFor="let id of ids; last as isLast">
+ {{id}}<span *ngIf="!isLast">,</span>
+ </span>
+ </h4>
+ <h4 i18n>Download a text file of ISBN, ISSN, or UPC
+ values for selected line item(s).
+ </h4>
+ <div class="form-group form-inline">
+ <label for="export-attr-select" class="form-check-label mr-1">Filter by:</label>
+ <select name="export-attr-select" id="export-attr-select"
+ [(ngModel)]="selectedAttr"
+ class="form-control">
+ <option value="isbn" i18n>ISBN</option>
+ <option value="issn" i18n>ISSN</option>
+ <option value="upc" i18n>UPC</option>
+ </select>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-success"
+ (click)="close(selectedAttr)" i18n>Download</button>
+ <button type="button" class="btn btn-warning"
+ (click)="close()" i18n>Cancel</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-export-attributes-dialog',
+ templateUrl: './export-attributes-dialog.component.html'
+})
+
+export class ExportAttributesDialogComponent extends DialogComponent {
+ @Input() ids: number[];
+ selectedAttr = 'isbn';
+ constructor(private modal: NgbModal) { super(modal); }
+}
+
+
-
-<!-- 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-column name="audit_time" [datePlusTime]="true"></eg-grid-column>
+ <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="expected_recv_time" [datePlusTime]="true"></eg-grid-column>
</eg-grid>
</div>
--- /dev/null
+<eg-confirm-dialog #confirmAlertsDialog
+ i18n-dialogTitle i18n-dialogBody
+ dialogTitle="Confirm Alert" [dialogBodyTemplate]="confirmAlertsMsg">
+</eg-confirm-dialog>
+<ng-template #confirmAlertsMsg>
+ <div *ngIf="numAlerts > 0" class="alert alert-warning" i18n>
+ Alert {{alertIndex}} out of {{numAlerts}}
+ </div>
+ <div i18n>An alert has been placed on line item {{liId}} ({{title}})</div>
+ <div class="mt-2">{{alertText.code()}}</div>
+ <div>{{alertText.description()}}</div>
+ <div>{{alertComment}}</div>
+ <div class="mt-2" i18n>Choose "Confirm" to acknowledge this alert and continue with receiving.
+ Otherwise, choose "Cancel" to not receive the line item(s). If there is more than one alert,
+ all of them must be confirmed in order to complete the receiving.
+ </div>
+</ng-template>
--- /dev/null
+import {Component, Input, ViewChild} from '@angular/core';
+import {Observable} from 'rxjs';
+import {IdlObject} from '@eg/core/idl.service';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+
+@Component({
+ selector: 'eg-lineitem-alert-dialog',
+ templateUrl: './lineitem-alert-dialog.component.html'
+})
+
+export class LineitemAlertDialogComponent {
+ @Input() liId: number;
+ @Input() title: string;
+ @Input() alertText: IdlObject;
+ @Input() alertComment: string;
+ @Input() numAlerts = 0;
+ @Input() alertIndex = 0;
+
+ @ViewChild('confirmAlertsDialog') confirmAlertsDialog: ConfirmDialogComponent;
+
+ open(): Observable<any> {
+ return this.confirmAlertsDialog.open();
+ }
+}
.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; }
+.li-state-delayed { background-color: #B3D9FF; }
<!-- BATCH ACTIONS -->
-<eg-acq-cancel-dialog #cancelDialog></eg-acq-cancel-dialog>
+<eg-acq-cancel-dialog recordType="li" #cancelDialog></eg-acq-cancel-dialog>
+<eg-acq-delete-lineitems-dialog #deleteLineitemsDialog></eg-acq-delete-lineitems-dialog>
+<eg-acq-add-copies-dialog #addCopiesDialog></eg-acq-add-copies-dialog>
+<eg-acq-bib-finder-dialog #bibFinderDialog></eg-acq-bib-finder-dialog>
+<eg-acq-batch-update-copies-dialog #batchUpdateCopiesDialog></eg-acq-batch-update-copies-dialog>
+<eg-acq-link-invoice-dialog #linkInvoiceDialog></eg-acq-link-invoice-dialog>
+<eg-acq-claim-policy-dialog #claimPolicyDialog></eg-acq-claim-policy-dialog>
+<eg-acq-manage-claims-dialog #manageClaimsDialog></eg-acq-manage-claims-dialog>
+<eg-acq-export-attributes-dialog #exportAttributesDialog></eg-acq-export-attributes-dialog>
+<eg-lineitem-alert-dialog #confirmAlertsDialog></eg-lineitem-alert-dialog>
+
+<eg-string #lineItemsUpdatedString i18n-text text="Line Item(s) Updated"></eg-string>
+
+<eg-alert-dialog #noActionableLIs i18n-dialogBody
+ dialogBody="None of the selected line items are suitable for the action.">
+</eg-alert-dialog>
+<eg-confirm-dialog #selectorReadyConfirmDialog
+ i18n-dialogTitle i18n-dialogBody
+ dialogTitle="Confirm Line Item Change"
+ dialogBody="Mark selected line item(s) as ready for selector?">
+</eg-confirm-dialog>
+<eg-confirm-dialog #orderReadyConfirmDialog
+ i18n-dialogTitle i18n-dialogBody
+ dialogTitle="Confirm Line Item Change"
+ dialogBody="Mark selected line item(s) as ready for order?">
+</eg-confirm-dialog>
+
+<div class="col-lg-6 offset-lg-3" *ngIf="saving">
+ <eg-progress-inline [max]="progressMax" [value]="progressValue">
+ </eg-progress-inline>
+</div>
<div class="row mt-3" *ngIf="poId || picklistId">
<div class="col-lg-1">
<button class="btn btn-info btn-sm" ngbDropdownToggle i18n>Actions</button>
<div ngbDropdownMenu>
<a ngbDropdownItem routerLink="../brief-record"
+ [disabled]="isActivatedPo()"
queryParamsHandling="merge" i18n>Add Brief Record</a>
<button ngbDropdownItem (click)="deleteLineitems()"
- [disabled]="!canDeleteLis()" i18n>Delete Selected Lineitems</button>
+ [disabled]="!canDeleteLis() || !selectedIds().length" i18n>Delete Selected Line Items</button>
+ <button ngbDropdownItem (click)="addCopiesToLineitems()"
+ [disabled]="isActivatedPo() || !selectedIds().length" i18n>Add Items to Selected Line Items</button>
+ <button ngbDropdownItem (click)="batchUpdateCopiesOnLineitems()"
+ [disabled]="isActivatedPo() || !selectedIds().length" i18n>Batch Update Items on Selected Line Items</button>
+ <button ngbDropdownItem (click)="exportSingleAttributeList()"
+ [disabled]="!selectedIds().length" i18n>Export Single Attribute List for Selected Line Items</button>
<div class="dropdown-divider"></div>
<h6 class="dropdown-header" i18n>Selection List Actions</h6>
+ <button ngbDropdownItem (click)="markSelectorReady()"
+ [disabled]="!picklistId" i18n>Mark Selected Line Items as Ready for Selector</button>
+ <button ngbDropdownItem (click)="markOrderReady()"
+ [disabled]="!picklistId" i18n>Mark Selected Line Items as Ready for Order</button>
<button ngbDropdownItem (click)="createPo()"
- [disabled]="!picklistId" i18n>Create Purchase Order from Selected Lineitems</button>
+ [disabled]="!picklistId" i18n>Create Purchase Order from Selected Line Items</button>
<button ngbDropdownItem (click)="createPo(true)"
- [disabled]="!picklistId" i18n>Create Purchase Order from All Lineitems</button>
+ [disabled]="!picklistId" i18n>Create Purchase Order from All Line Items</button>
<div class="dropdown-divider"></div>
<h6 class="dropdown-header" i18n>Purchase Order Actions</h6>
+ <a ngbDropdownItem routerLink="../create-assets"
+ [disabled]="!isPendingPo()"
+ queryParamsHandling="merge" i18n>Load Bibs and Items</a>
<button ngbDropdownItem (click)="receiveSelected()"
- [disabled]="!poId" i18n>Mark Selected Lineitems as Received</button>
+ [disabled]="!isActivatedPo() || !selectedIds().length" i18n>Mark Selected Line Items as Received</button>
<button ngbDropdownItem (click)="unReceiveSelected()"
- [disabled]="!poId" i18n>Un-Receive Selected Lineitems</button>
+ [disabled]="!isActivatedPo() || !selectedIds().length" i18n>Un-Receive Selected Line Items</button>
<button ngbDropdownItem (click)="cancelSelected()"
- [disabled]="!poId" i18n>Cancel Selected Lineitems</button>
+ [disabled]="!isActivatedPo() || !selectedIds().length" i18n>Cancel Selected Line Items</button>
+ <button ngbDropdownItem (click)="applyClaimPolicyToSelected()"
+ [disabled]="!poId || !selectedIds().length" i18n>Apply Claim Policy to Selected Line Items</button>
+ <button ngbDropdownItem (click)="createInvoiceFromSelected()"
+ [disabled]="!isActivatedPo() || !selectedIds().length" i18n>Create Invoice from Selected Line Items</button>
+ <button ngbDropdownItem (click)="linkInvoiceFromSelected()"
+ [disabled]="!isActivatedPo() || !selectedIds().length" i18n>Link Selected Line Items to Invoice</button>
</div>
</div>
</div>
<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>
+ <label class="form-check-label" for='toggle-page-cbox' i18n>Line Items In Page</label>
</div>
</div>
<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>
+ <label class="form-check-label" for='toggle-all-cbox' i18n>All Line Items</label>
</div>
</div>
</div>
<div [hidden]="!showFilterSort">
<div class="col-lg-12 d-flex">
+ <form>
+ <div class="d-flex justify-content-center flex-column h-100">
+ <div class="form-group form-inline">
+ <label for="filter-field-select" class="form-check-label mr-1">Filter by:</label>
+ <select name="filter-field-select" id="filter-field-select"
+ [ngModel]="filterField" (ngModelChange)="filterFieldChange($event)"
+ class="form-control">
+ <option value="" i18n></option>
+ <option value="id" i18n>Line Item ID</option>
+ <option value="state" i18n>Status</option>
+ <option value="acqlia:title" i18n>Title</option>
+ <option value="acqlia:author" i18n>Author</option>
+ <option value="acqlia:publisher" i18n>Publisher</option>
+ <option value="acqlia:pubdate" i18n>Publication date</option>
+ <option value="acqlia:isbn" i18n>ISBN</option>
+ <option value="acqlia:issn" i18n>ISSN</option>
+ <option value="acqlia:upc" i18n>UPC</option>
+ <option value="claim_count" i18n>Claim count</option>
+ <option value="item_count" i18n>Item count</option>
+ <option value="estimated_unit_price" i18n>Estimated unit price</option>
+ </select>
+ <select name="filter-operator-select" id="filter-operator-select"
+ [(ngModel)]="filterOperator" (ngModelChange)="filterOperatorChange($event)"
+ class="form-control">
+ <option i18n value="">is</option>
+ <option i18n value="__not">is NOT</option>
+ <option i18n value="__fuzzy" [hidden]="searchTermDatatypes[filterField] != 'text'">contains</option>
+ <option i18n value="__not,__fuzzy" [hidden]="searchTermDatatypes[filterField]">does NOT contain</option>
+ <option i18n value="__starts" [hidden]="searchTermDatatypes[filterField] != 'text'">STARTS with</option>
+ <option i18n value="__ends" [hidden]="searchTermDatatypes[filterField] != 'text'">ENDS with</option>
+ <option i18n value="__lte" [hidden]="searchTermDatatypes[filterField] != 'timestamp' && !dateLikeSearchFields[filterField]">is on or BEFORE</option>
+ <option i18n value="__gte" [hidden]="searchTermDatatypes[filterField] != 'timestamp' && !dateLikeSearchFields[filterField]">is on or AFTER</option>
+ <option i18n value="__between" [hidden]="searchTermDatatypes[filterField] != 'timestamp'">is BETWEEN</option>
+ <option i18n value="__age" [hidden]="searchTermDatatypes[filterField] != 'timestamp'">age (relative date)</option>
+ <option i18n value="__gte" [hidden]="searchTermDatatypes[filterField] != 'number'">is greater than or equal</option>
+ <option i18n value="__lte" [hidden]="searchTermDatatypes[filterField] != 'number'">is less than or equal</option>
+<!-- TODO
+ <option i18n value="__isnotnull" [hidden]="searchTermDatatypes[filterField] == 'id'">exists</option>
+ <option i18n value="__isnull" [hidden]="searchTermDatatypes[filterField] == 'id'">does NOT exist</option>
+ <option i18n value="__in">matches a term from a file</option>
+-->
+ </select>
+ <input *ngIf="searchTermDatatypes[filterField] != 'state'" type="text" class="form-control" name="filter-value-input" id="filter-value-input" [(ngModel)]="filterValue">
+ <eg-combobox *ngIf="searchTermDatatypes[filterField] == 'state'"
+ [asyncSupportsEmptyTermClick]="true"
+ idlClass="jubstlbl"
+ [selectedId]="filterValue"
+ (onChange)="filterValue = $event ? $event.id : ''">
+ </eg-combobox>
+ <button type="submit" (click)="applyFilter()"
+ class="btn btn-sm btn-outline-dark mr-1 ml-1" [disabled]="!canApplyFilter()" i18n>Apply Filter</button>
+ <button type="button" (click)="resetFilter()"
+ class="btn btn-sm btn-outline-dark mr-1" i18n>Reset Filter</button>
+ </div>
+ </div>
+ </form>
+ </div>
+ <div class="col-lg-12 d-flex">
<div class="d-flex justify-content-center flex-column h-100">
<div class="form-group form-inline">
<label for="sort-order-select" class="form-check-label mr-1">Sort by:</label>
<select name="sort-order-select" id="sort-order-select"
[ngModel]="sortOrder" (ngModelChange)="sortOrderChange($event)"
class="form-control">
- <option value="li_id_asc" i18n>Lineitem ID Ascending</option>
- <option value="li_id_desc" i18n>Lineitem ID Descending</option>
+ <option value="li_id_asc" i18n>Line Item ID Ascending</option>
+ <option value="li_id_desc" i18n>Line Item ID Descending</option>
<option value="title_asc" i18n>Title Ascending</option>
<option value="title_desc" i18n>Title Descending</option>
<option value="author_asc" i18n>Author Ascending</option>
</ng-container>
<ng-container *ngFor="let li of pageOfLineitems">
- <div class="row mt-2 border-bottom pt-2 pb-2 li-state-{{li.state()}}">
+ <div class="row mt-2 border-bottom pt-2 pb-2 li-state-{{lineitemDisposition(li)}}">
<div class="col-lg-12 d-flex">
<div class="jacket-wrapper">
<ng-container *ngIf="jacketIdent(li)">
<span class="pr-1">{{li.source_label()}}</span>
</div>
</div>
+ <div class="row">
+ <div class="col-lg-12 d-flex">
+ <div class="mr-2">
+ <ng-container [ngSwitch]="li.state()">
+ <div i18n
+ class="p-1 text-dark border border-dark bg-light rounded-pill"
+ *ngSwitchCase="'new'">New</div>
+ <div i18n
+ class="p-1 text-dark border border-dark bg-light rounded-pill"
+ *ngSwitchCase="'selector-ready'">Selector-Ready</div>
+ <div i18n
+ class="p-1 text-dark border border-dark bg-light rounded-pill"
+ *ngSwitchCase="'order-ready'">Order-Ready</div>
+ <div i18n
+ class="p-1 text-dark border border-dark bg-light rounded-pill"
+ *ngSwitchCase="'approved'">Approved</div>
+ <div i18n
+ class="p-1 text-dark border border-dark bg-light rounded-pill"
+ *ngSwitchCase="'pending-order'">Pending-Order</div>
+ <div i18n
+ class="p-1 text-primary border border-primary bg-light rounded-pill"
+ *ngSwitchCase="'on-order'">On-Order</div>
+ <div i18n
+ class="p-1 text-success border border-success bg-light rounded-pill"
+ *ngSwitchCase="'received'">Received</div>
+ <div i18n
+ class="p-1 text-danger border border-danger bg-light rounded-pill"
+ *ngSwitchCase="'cancelled'">{{li.cancel_reason().label()}}</div>
+ </ng-container>
+ </div>
+ <!-- w-auto allows the input group to stick to the right
+ as the status label grows -->
+ <div class="input-group w-auto mr-2">
+ <div class="input-group-prepend">
+ <span *ngIf="identOptions(li).length > 1" class="text-danger mr-1"
+ i18n-title title="Multiple Order Identifier Options" i18n>
+ ({{identOptions(li).length}})
+ </span>
+ <div ngbDropdown>
+ <button class="btn btn-outline-dark btn-sm" ngbDropdownToggle
+ title="Order Identifier Type" i18n-title [disabled]="!canEditIdent(li)"
+ [ngClass]="{'btn-warning': !selectedIdent(li)}">
+ <ng-container *ngIf="orderIdentTypes[li.id()]=='isbn'" i18n>ISBN</ng-container>
+ <ng-container *ngIf="orderIdentTypes[li.id()]=='issn'" i18n>ISSN</ng-container>
+ <ng-container *ngIf="orderIdentTypes[li.id()]=='upc'" i18n>UPC</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()]='issn'" i18n>ISSN</button>
+ <button class="btn-sm" ngbDropdownItem
+ (click)="orderIdentTypes[li.id()]='upc'" i18n>UPC</button>
+ </div>
+ </div>
+ </div>
+ <eg-combobox [entries]="identOptions(li)" [smallFormControl]="true"
+ placeholder="Order Identifer..." i18n-placeholder
+ [disabled]="!canEditIdent(li)"
+ [allowFreeText]="true" [selectedId]="selectedIdent(li)"
+ (onChange)="orderIdentChanged(li, $event)">
+ </eg-combobox>
+ </div>
+ <div class="mr-2">
+ <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 ngbDropdown>
+ <button class="btn btn-info btn-sm" ngbDropdownToggle i18n>Actions</button>
+ <div ngbDropdownMenu>
+ <button ngbDropdownItem [disabled]="li.state() != 'on-order' && lineitemDisposition(li) != 'delayed'"
+ (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>Update Barcodes</button>
+ <button ngbDropdownItem [disabled]="!liHasRealCopies(li)"
+ (click)="jumpToHoldings(li)" i18n>Open Holdings View</button>
+ <button ngbDropdownItem [disabled]="!liHasRealCopies(li)"
+ (click)="manageClaims(li)" i18n>Claims ({{countClaims(li)}} existing)</button>
+ <a ngbDropdownItem routerLink="lineitem/{{li.id()}}/history"
+ queryParamsHandling="merge" i18n>View History</a>
+ </div>
+ </div>
+ </div>
+ </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 class="row">
<div class="col-lg-12">
- <span title="Lineitem ID" i18n-title i18n># {{li.id()}}</span>
+ <span title="Line Item 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}">
<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()">
+ <ng-container *ngIf="!expandLineitem[li.id()]">
<span class="material-icons small mr-1">unfold_more</span>
<span i18n>Expand</span>
</ng-container>
- <ng-container *ngIf="showExpandFor == li.id()">
+ <ng-container *ngIf="expandLineitem[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
+ <a class="label-with-material-icon" title="Notes and Alerts" 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 i18n>Notes and Alerts ({{li.lineitem_notes().length}})</span>
<span *ngIf="liHasAlerts(li)" class="text-danger material-icons"
title="Has Alerts" i18n-title>flag</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()}}">
+ routerLink="/staff/catalog/record/{{li.eg_bib_id()}}"
+ target="_blank">
<span class="material-icons small mr-1">library_books</span>
<span i18n>Catalog</span>
</a>
</ng-container>
- <!-- TODO link to catalog -->
+ <ng-container *ngIf="!li.eg_bib_id()">
+ <span class="ml-1 mr-1" i18n> | </span>
+ <a class="label-with-material-icon mr-2"
+ href="javascript:;" (click)="openBibFinder(li.id())"
+ title="Link to Catalog" i18n-title>
+ <span class="material-icons small mr-1">library_books</span>
+ <span i18n>Link to Catalog</span>
+ </a>
+ </ng-container>
<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>
+ <span i18n>{{li.purchase_order().name()}}</span>
</a>
</ng-container>
- <!-- TODO patron requests -->
+ <span class="ml-1 mr-1" i18n> | </span>
+ <a class="label-with-material-icon"
+ title="Request(s)" i18n-title
+ href="/eg/staff/acq/requests/lineitem/{{li.id()}}"
+ target="_blank">
+ <span class="material-icons small mr-1">help</span>
+ <span i18n>Request(s)</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">
+ routerLink="/staff/acq/search/invoices" target="_blank">
<span class="material-icons small mr-1">list</span>
<span i18n>Invoice(s)</span>
</a>
- <!-- TODO: claim policy -->
+ <ng-container *ngIf="li.claim_policy()">
+ <span class="ml-1 mr-1" i18n> | </span>
+ <span i18n>Claim policy: {{li.claim_policy().name()}}</span>
+ </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
+ title="Provider" i18n-title target="_blank"
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>
- <!-- TODO import queue -->
-
- </div>
- </div>
- </div>
+ <ng-container *ngIf="li.queued_record()">
+ <span class="ml-1 mr-1" i18n> | </span>
+ <a class="label-with-material-icon"
+ title="Import Queue" i18n-title
+ routerLink="/staff/cat/vandelay/queue/bib/{{li.queued_record().queue()}}"
+ target="_blank">
+ <span class="material-icons small mr-1">queue</span>
+ <span i18n>Import Queue</span>
+ </a>
+ </ng-container>
- <!-- 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">
- <span *ngIf="identOptions(li).length > 1" class="text-danger mr-1"
- i18n-title title="Multiple Order Identifier Options" i18n>
- ({{identOptions(li).length}})
- </span>
- <div ngbDropdown>
- <button class="btn btn-outline-dark btn-sm" ngbDropdownToggle
- title="Order Identifier Type" i18n-title [disabled]="!canEditIdent(li)"
- [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
- [disabled]="!canEditIdent(li)"
- [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>
</eg-lineitem-notes>
</div>
</div>
- <div class="row" *ngIf="showExpandFor == li.id() || expandAll">
+ <div class="row" *ngIf="expandLineitem[li.id()]">
<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>Shelving 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 class="flex-1 p-1" i18n>Receiver</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 [embedded]="true" [showReceiver]="true" [copy]="copy">
</eg-lineitem-copy-attrs>
</div>
</div>
import {Component, OnInit, Input, Output, ViewChild} from '@angular/core';
import {Router, ActivatedRoute, ParamMap} from '@angular/router';
-import {Observable, from} from 'rxjs';
+import {Observable, from, of, Subscription} from 'rxjs';
import {tap, concatMap} 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 {IdlService, IdlObject} from '@eg/core/idl.service';
import {NetService} from '@eg/core/net.service';
import {AuthService} from '@eg/core/auth.service';
+import {ToastService} from '@eg/share/toast/toast.service';
import {ServerStoreService} from '@eg/core/server-store.service';
-import {LineitemService} from './lineitem.service';
+import {LineitemService, LINEITEM_DISPOSITION} from './lineitem.service';
+import {PoService} from '../po/po.service';
import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
import {HoldingsService} from '@eg/staff/share/holdings/holdings.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
import {CancelDialogComponent} from './cancel-dialog.component';
+import {DeleteLineitemsDialogComponent} from './delete-lineitems-dialog.component';
+import {AddCopiesDialogComponent} from './add-copies-dialog.component';
+import {BibFinderDialogComponent} from './bib-finder-dialog.component';
+import {BatchUpdateCopiesDialogComponent} from './batch-update-copies-dialog.component';
+import {LinkInvoiceDialogComponent} from './link-invoice-dialog.component';
+import {ExportAttributesDialogComponent} from './export-attributes-dialog.component';
+import {ClaimPolicyDialogComponent} from './claim-policy-dialog.component';
+import {ManageClaimsDialogComponent} from './manage-claims-dialog.component';
+import {LineitemAlertDialogComponent} from './lineitem-alert-dialog.component';
const DELETABLE_STATES = [
'new', 'selector-ready', 'order-ready', 'approved', 'pending-order'
picklistId: number = null;
poId: number = null;
+ poWasActivated = false;
+ poSubscription: Subscription;
recordId: number = null; // lineitems related to a bib.
loading = false;
pageOfLineitems: IdlObject[] = [];
lineitemIds: number[] = [];
+ saving = false;
+ progressMax = 0;
+ progressValue = 0;
+
// Selected lineitems
selected: {[id: number]: boolean} = {};
// sorting and filtering
sortOrder = DEFAULT_SORT_ORDER;
showFilterSort = false;
+ filterField = '';
+ filterOperator = '';
+ filterValue = '';
+ filterApplied = false;
+
+ searchTermDatatypes = {
+ 'id': 'id',
+ 'state': 'state',
+ 'acqlia:title': 'text',
+ 'acqlia:author': 'text',
+ 'acqlia:publisher': 'text',
+ 'acqlia:pubdate': 'text',
+ 'acqlia:isbn': 'text',
+ 'acqlia:issn': 'text',
+ 'acqlia:upc': 'text',
+ 'claim_count': 'number',
+ 'item_count': 'number',
+ 'estimated_unit_price': 'money',
+ };
+ dateLikeSearchFields = {
+ 'acqlia:pubdate': true,
+ };
batchNote: string;
noteIsPublic = false;
batchSelectPage = false;
batchSelectAll = false;
showNotesFor: number;
- showExpandFor: number; // 'Expand'
+ expandLineitem: {[id: number]: boolean} = {};
expandAll = false;
action = '';
batchFailure: EgEvent;
// TODO: route guard might be better
@ViewChild('cancelDialog') cancelDialog: CancelDialogComponent;
+ @ViewChild('deleteLineitemsDialog') deleteLineitemsDialog: DeleteLineitemsDialogComponent;
+ @ViewChild('addCopiesDialog') addCopiesDialog: AddCopiesDialogComponent;
+ @ViewChild('bibFinderDialog') bibFinderDialog: BibFinderDialogComponent;
+ @ViewChild('batchUpdateCopiesDialog') batchUpdateCopiesDialog: BatchUpdateCopiesDialogComponent;
+ @ViewChild('linkInvoiceDialog') linkInvoiceDialog: LinkInvoiceDialogComponent;
+ @ViewChild('exportAttributesDialog') exportAttributesDialog: ExportAttributesDialogComponent;
+ @ViewChild('claimPolicyDialog') claimPolicyDialog: ClaimPolicyDialogComponent;
+ @ViewChild('manageClaimsDialog') manageClaimsDialog: ManageClaimsDialogComponent;
+ @ViewChild('lineItemsUpdatedString', { static: false }) lineItemsUpdatedString: StringComponent;
+ @ViewChild('noActionableLIs', { static: true }) private noActionableLIs: AlertDialogComponent;
+ @ViewChild('selectorReadyConfirmDialog', { static: true }) selectorReadyConfirmDialog: ConfirmDialogComponent;
+ @ViewChild('orderReadyConfirmDialog', { static: true }) orderReadyConfirmDialog: ConfirmDialogComponent;
+ @ViewChild('confirmAlertsDialog') confirmAlertsDialog: LineitemAlertDialogComponent;
constructor(
private router: Router,
private net: NetService,
private auth: AuthService,
private store: ServerStoreService,
+ private idl: IdlService,
+ private toast: ToastService,
private holdings: HoldingsService,
- private liService: LineitemService
+ private liService: LineitemService,
+ private poService: PoService
) {}
ngOnInit() {
+ this.liService.getLiAttrDefs();
+
this.route.queryParamMap.subscribe((params: ParamMap) => {
this.pager.offset = +params.get('offset');
this.pager.limit = +params.get('limit');
});
});
+ this.poSubscription = this.poService.poRetrieved.subscribe(() => {
+ this.poWasActivated = this.po().order_date() ? true : false;
+ });
+ }
+
+ po(): IdlObject {
+ return this.poService.currentPo;
}
pageSizeChange(count: number) {
});
}
+ filterFieldChange(event) {
+ this.filterOperator = '';
+ if (this.filterField === 'state') {
+ this.filterValue = '';
+ }
+ this.filterField = event;
+ }
+
+ filterOperatorChange() {
+ // empty for now
+ }
+
+ canApplyFilter(): boolean {
+ if (this.filterField !== '' &&
+ this.filterValue !== '') {
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ applyFilter() {
+ this.filterApplied = true;
+ if (this.pager.isFirstPage()) {
+ this.load();
+ } else {
+ this.pager.toFirst();
+ this.goToPage();
+ }
+ }
+
+ resetFilter() {
+ this.filterField = '';
+ this.filterOperator = '';
+ this.filterValue = '';
+ if (this.filterApplied) {
+ this.filterApplied = false;
+ if (this.pager.isFirstPage()) {
+ this.load();
+ } else {
+ this.pager.toFirst();
+ this.goToPage();
+ }
+ }
+ }
+
// Focus the selected lineitem, which may not yet exist in the
// DOM for focusing.
focusLineitem(id?: number) {
this.lineitemIds = [];
const searchTerms = {};
- const opts = { id_list: true, limit: 1000 };
+ const opts = { limit: 10000 };
if (this.picklistId) {
Object.assign(searchTerms, { jub: [ { picklist: this.picklistId } ] });
Object.assign(searchTerms, { jub: [ { purchase_order: this.poId } ] });
}
+ if (this.filterApplied) {
+ this._handleFiltering(searchTerms);
+ }
+
if (!(this.sortOrder in SORT_ORDER_MAP)) {
this.sortOrder = DEFAULT_SORT_ORDER;
}
Object.assign(opts, SORT_ORDER_MAP[this.sortOrder]);
+ let _doingClientSort = false;
+ if (this.filterField === 'item_count' ||
+ this.filterField === 'claim_count') {
+ opts['flesh_li_details'] = true;
+ }
+ if (this.sortOrder === 'title_asc' ||
+ this.sortOrder === 'title_desc' ||
+ this.sortOrder === 'author_asc' ||
+ this.sortOrder === 'author_desc' ||
+ this.sortOrder === 'publisher_asc' ||
+ this.sortOrder === 'publisher_desc') {
+ // if we're going to sort by an attribute, we'll need
+ // to actually fetch LI attributes so that we can
+ // do a client-side sorting pass that ignores
+ // articles and attempts international collation
+ _doingClientSort = true;
+ opts['flesh_attrs'] = true;
+ } else {
+ if (!opts['flesh_li_details']) {
+ opts['id_list'] = true;
+ }
+ }
+
return this.net.request(
'open-ils.acq',
'open-ils.acq.lineitem.unified_search.atomic',
null,
opts
).toPromise().then(resp => {
- this.lineitemIds = resp.map(i => Number(i));
+ let _mustDeflesh = false;
+ if (this.filterField === 'item_count') {
+ _mustDeflesh = true;
+ if (!isNaN(Number(this.filterValue))) {
+ const num = Number(this.filterValue);
+ resp = resp.filter(l => {
+ if (this.filterOperator === '' && l.item_count() === num) {
+ return true;
+ } else if (this.filterOperator === '__not' && l.item_count() !== num) {
+ return true;
+ } else if (this.filterOperator === '__gte' && l.item_count() >= num) {
+ return true;
+ } else if (this.filterOperator === '__lte' && l.item_count() <= num) {
+ return true;
+ } else {
+ return false;
+ }
+ });
+ }
+ } else if (this.filterField === 'claim_count') {
+ _mustDeflesh = true;
+ if (!isNaN(Number(this.filterValue))) {
+ const num = Number(this.filterValue);
+ resp.forEach(
+ l => l['_claim_count'] = l.lineitem_details().reduce(
+ (a, b) => (a ? a.claims().length : 0) + b.claims().length, 0
+ )
+ );
+ resp = resp.filter(l => {
+ if (this.filterOperator === '' && l['_claim_count'] === num) {
+ return true;
+ } else if (this.filterOperator === '__not' && l['_claim_count'] !== num) {
+ return true;
+ } else if (this.filterOperator === '__gte' && l['_claim_count'] >= num) {
+ return true;
+ } else if (this.filterOperator === '__lte' && l['_claim_count'] <= num) {
+ return true;
+ } else {
+ return false;
+ }
+ });
+ resp.forEach(l => delete l['_claim_count']);
+ }
+ }
+ if (_doingClientSort) {
+ const sortOrder = this.sortOrder;
+ const liService = this.liService;
+ function _compareLIs(a, b) {
+ const direction = sortOrder.match(/_asc$/) ? 'asc' : 'desc';
+ const field = sortOrder.replace(/_asc|_desc$/, '');
+
+ const a_val = liService.getLISortKey(a, field);
+ const b_val = liService.getLISortKey(b, field);
+
+ if (direction === 'asc') {
+ return liService.nullableCompare(a_val, b_val);
+ } else {
+ return -liService.nullableCompare(a_val, b_val);
+ }
+ }
+ this.lineitemIds = resp.sort(_compareLIs).map(l => Number(l.id()));
+ } else {
+ if (_mustDeflesh) {
+ this.lineitemIds = resp.map(l => Number(l.id()));
+ } else {
+ this.lineitemIds = resp.map(i => Number(i));
+ }
+ }
this.pager.resultCount = resp.length;
});
}
+ _handleFiltering(searchTerms: any) {
+ const searchTerm: Object = {};
+ const filterField = this.filterField;
+ let filterOp = this.filterOperator;
+ let filterVal = this.filterValue;
+
+ if (filterField === 'item_count' ||
+ filterField === 'claim_count') {
+ return;
+ }
+
+ if (filterOp === 'like' && filterVal.length > 1) {
+ if (filterVal[0] === '%' && filterVal[filterVal.length - 1] === '%') {
+ filterVal = filterVal.slice(1, filterVal.length - 1);
+ } else if (filterVal[filterVal.length - 1] === '%') {
+ filterVal = filterVal.slice(0, filterVal.length - 1);
+ filterOp = 'startswith';
+ } else if (filterVal[0] === '%') {
+ filterVal = filterVal.slice(1);
+ filterOp = 'endswith';
+ }
+ }
+
+ if (filterOp !== '') {
+ searchTerm[filterOp] = true;
+ }
+
+ if (filterField.match(/^acqlia:/)) {
+ const attrName = (filterField.split(':'))[1];
+ const def = this.liService.liAttrDefs.filter(
+ d => d.code() === attrName)[0];
+ if (def) {
+ searchTerm[def.id()] = filterVal;
+ searchTerms['acqlia'] = [ searchTerm ];
+ }
+ } else {
+ searchTerm[filterField] = filterVal;
+ searchTerms['jub'].push(searchTerm);
+ }
+ }
+
goToPage() {
this.focusLi = null;
this.router.navigate([], {
ingestOneLi(li: IdlObject, replace?: boolean) {
this.liMarcAttrs[li.id()] = {};
+ if (this.expandAll) {
+ this.expandLineitem[li.id()] = true;
+ } else {
+ this.expandLineitem[li.id()] = false;
+ }
+
li.attributes().forEach(attr => {
const name = attr.attr_name();
this.liMarcAttrs[li.id()][name] =
}
liPriceChange(li: IdlObject) {
- const price = li.estimated_unit_price();
if (this.liPriceIsValid(li)) {
- li.estimated_unit_price(Number(price).toFixed(2));
-
+ const price = Number(li.estimated_unit_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()));
+ 'open-ils.acq.lineitem.price.set',
+ this.auth.token(), li.id(), price
+ ).subscribe(resp => {
+ // update local copy
+ li.estimated_unit_price(price);
+ this.liService.activateStateChange.emit(li.id());
+ });
}
}
toggleShowNotes(liId: number) {
- this.showExpandFor = null;
this.showNotesFor = this.showNotesFor === liId ? null : liId;
+ this.expandLineitem[liId] = false;
}
toggleShowExpand(liId: number) {
this.showNotesFor = null;
- this.showExpandFor = this.showExpandFor === liId ? null : liId;
+ this.expandLineitem[liId] = !this.expandLineitem[liId];
}
toggleExpandAll() {
this.showNotesFor = null;
- this.showExpandFor = null;
this.expandAll = !this.expandAll;
+ if (this.expandAll) {
+ this.pageOfLineitems.forEach(li => this.expandLineitem[li.id()] = true);
+ } else {
+ this.pageOfLineitems.forEach(li => this.expandLineitem[li.id()] = false);
+ }
}
toggleFilterSort() {
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';
+ this.deleteLineitemsDialog.ids = ids.map(i => Number(i));
+ this.deleteLineitemsDialog.open().subscribe(doIt => {
+ if (!doIt) { return; }
+
+ const method = this.poId ?
+ 'open-ils.acq.purchase_order.lineitem.delete' :
+ 'open-ils.acq.picklist.lineitem.delete';
+
+ from(ids)
+ .pipe(concatMap(id =>
+ this.net.request('open-ils.acq', method, this.auth.token(), id)
+ // TODO: cap parallelism
+ ))
+ .pipe(concatMap(_ => of(true) ))
+ .subscribe(r => {}, err => {}, () => {
+ ids.forEach(id => {
+ delete this.liService.liCache[id];
+ delete this.selected[id];
+ });
+ this.batchSelectAll = false;
+ this.load();
+ });
+ });
+ }
+
+ addCopiesToLineitems() {
+ const ids = Object.keys(this.selected).filter(id => this.selected[id]);
+
+ this.addCopiesDialog.ids = ids.map(i => Number(i));
+ this.addCopiesDialog.open({size: 'xl'}).subscribe(templateLineitem => {
+ if (!templateLineitem) { return; }
+
+ const lids = [];
+ ids.forEach(li_id => {
+ templateLineitem.lineitem_details().forEach(lid => {
+ const c = this.idl.clone(lid);
+ c.isnew(true);
+ c.lineitem(li_id);
+ lids.push(c);
+ });
+ });
+
+ this.saving = true;
+ this.progressMax = null;
+ this.progressValue = 0;
+
+ this.liService.updateLiDetailsMulti(lids).subscribe(
+ struct => {
+ this.progressMax = struct.total;
+ this.progressValue++;
+ },
+ err => {},
+ () => {
+ // Remove the modified LI's from the cache so we are
+ // forced to re-fetch them.
+ ids.forEach(id => delete this.liService.liCache[id]);
+ this.saving = false;
+ this.loadPageOfLis();
+ this.liService.activateStateChange.emit(Number(ids[0]));
+ }
+ );
+
+ });
+ }
+
+ openBibFinder(liId: number) {
+ this.bibFinderDialog.liId = liId;
+ this.bibFinderDialog.open({size: 'xl'}).subscribe(bibId => {
+ if (!bibId) { return; }
+
+ const lis: IdlObject[] = [];
+ this.liService.getFleshedLineitems([liId], { fromCache: true }).subscribe(
+ liStruct => {
+ liStruct.lineitem.eg_bib_id(bibId);
+ liStruct.lineitem.attributes([]);
+ lis.push(liStruct.lineitem);
+ },
+ err => { },
+ () => {
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.lineitem.update',
+ this.auth.token(), lis
+ ).toPromise().then(resp => this.postBatchAction(resp, [liId]));
+ }
+ );
+ });
+ }
+
+ batchUpdateCopiesOnLineitems() {
+ const ids = Object.keys(this.selected).filter(id => this.selected[id]);
+
+ this.batchUpdateCopiesDialog.ids = ids.map(i => Number(i));
+ this.batchUpdateCopiesDialog.open({size: 'xl'}).subscribe(batchChanges => {
+ if (!batchChanges) { return; }
+
+ this.saving = true;
+ this.progressMax = ids.length;
+ this.progressValue = 0;
+
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.lineitem.batch_update',
+ this.auth.token(), { lineitems: ids },
+ batchChanges, batchChanges._dist_formula
+ ).subscribe(
+ response => {
+ const evt = this.evt.parse(response);
+ if (!evt) {
+ delete this.liService.liCache[response];
+ this.progressValue++;
+ }
+ },
+ err => {},
+ () => {
+ this.saving = false;
+ this.loadPageOfLis();
+ this.liService.activateStateChange.emit(Number(ids[0]));
+ }
+ );
+ });
+ }
+
+ exportSingleAttributeList() {
+ const ids = Object.keys(this.selected).filter(id => this.selected[id]).map(i => Number(i));
+ this.exportAttributesDialog.ids = ids;
+ this.exportAttributesDialog.open().subscribe(attr => {
+ if (!attr) { return; }
+
+ this.liService.doExportSingleAttributeList(ids, attr);
+ });
+ }
+
+ markSelectorReady(rows: IdlObject[]) {
+ const ids = this.selectedIds().map(i => Number(i));
+ if (ids.length === 0) { return; }
+
+ const lis: IdlObject[] = [];
+ this.liService.getFleshedLineitems(ids, { fromCache: true }).subscribe(
+ liStruct => {
+ if (liStruct.lineitem.state() === 'new') {
+ lis.push(liStruct.lineitem);
+ }
+ },
+ err => {},
+ () => {
+ if (lis.length === 0) {
+ this.noActionableLIs.open();
+ return;
+ }
+ this.selectorReadyConfirmDialog.open().subscribe(doIt => {
+ if (!doIt) { return; }
+ lis.forEach(li => li.state('selector-ready'));
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.lineitem.update',
+ this.auth.token(), lis
+ ).toPromise().then(resp => {
+ this.lineItemsUpdatedString.current()
+ .then(str => this.toast.success(str));
+ this.postBatchAction(resp, ids);
+ });
+ });
+ }
+ );
+ }
+
+ markOrderReady(rows: IdlObject[]) {
+ const ids = this.selectedIds().map(i => Number(i));
+ if (ids.length === 0) { return; }
- from(ids)
- .pipe(concatMap(id =>
- this.net.request('open-ils.acq', method, this.auth.token(), id)
- ))
- .pipe(concatMap(_ => from(this.load())))
- .subscribe();
+ const lis: IdlObject[] = [];
+ this.liService.getFleshedLineitems(ids, { fromCache: true }).subscribe(
+ liStruct => {
+ if (liStruct.lineitem.state() === 'new' || liStruct.lineitem.state() === 'selector-ready') {
+ lis.push(liStruct.lineitem);
+ }
+ },
+ err => {},
+ () => {
+ if (lis.length === 0) {
+ this.noActionableLIs.open();
+ return;
+ }
+ this.orderReadyConfirmDialog.open().subscribe(doIt => {
+ if (!doIt) { return; }
+ lis.forEach(li => li.state('order-ready'));
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.lineitem.update',
+ this.auth.token(), lis
+ ).toPromise().then(resp => {
+ this.lineItemsUpdatedString.current()
+ .then(str => this.toast.success(str));
+ this.postBatchAction(resp, ids);
+ });
+ });
+ }
+ );
}
liHasRealCopies(li: IdlObject): boolean {
);
}
+ jumpToHoldings(li: IdlObject) {
+ window.open('/eg2/staff/catalog/record/' + li.eg_bib_id() + '/holdings', '_blank');
+ }
+
+ manageClaims(li: IdlObject) {
+ this.manageClaimsDialog.li = li;
+ this.manageClaimsDialog.open().subscribe(result => {
+ if (result) {
+ delete this.liService.liCache[li.id()];
+ this.loadPageOfLis();
+ }
+ });
+ }
+
+ countClaims(li: IdlObject): number {
+ let total = 0;
+ li.lineitem_details().forEach(lid => total += lid.claims().length);
+ return total;
+ }
+
receiveSelected() {
this.markReceived(this.selectedIds());
}
});
}
+ applyClaimPolicyToSelected() {
+ const liIds = this.selectedIds();
+
+ if (liIds.length === 0) { return; }
+
+ this.claimPolicyDialog.ids = liIds.map(i => Number(i));
+ this.claimPolicyDialog.open().subscribe(claimPolicy => {
+ if (!claimPolicy) { return; }
+
+ const lis: IdlObject[] = [];
+ this.liService.getFleshedLineitems(liIds, { fromCache: true }).subscribe(
+ liStruct => {
+ liStruct.lineitem.claim_policy(claimPolicy);
+ lis.push(liStruct.lineitem);
+ },
+ err => { },
+ () => {
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.lineitem.update',
+ this.auth.token(), lis
+ ).toPromise().then(resp => this.postBatchAction(resp, liIds));
+ }
+ );
+ });
+ }
+
+ createInvoiceFromSelected() {
+ const liIds = this.selectedIds();
+ if (liIds.length === 0) { return; }
+
+ const path = '/eg/staff/acq/legacy/invoice/view?create=1&' +
+ liIds.map(x => 'attach_li=' + x.toString()).join('&');
+ window.location.href = path;
+ }
+
+ linkInvoiceFromSelected() {
+ const liIds = this.selectedIds();
+ if (liIds.length === 0) { return; }
+
+ this.linkInvoiceDialog.liIds = liIds.map(i => Number(i));
+ this.linkInvoiceDialog.open().subscribe(invId => {
+ if (!invId) { return; }
+
+ const path = '/eg/staff/acq/legacy/invoice/view/' + invId + '?' +
+ liIds.map(x => 'attach_li=' + x.toString()).join('&');
+ window.location.href = path;
+ });
+
+ }
+
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));
+ const lis: IdlObject[] = [];
+ this.liService.getFleshedLineitems(liIds, { fromCache: true }).subscribe(
+ liStruct => lis.push(liStruct.lineitem),
+ err => {},
+ () => {
+ this.liService.checkLiAlerts(lis, this.confirmAlertsDialog).then(ok => {
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.lineitem.receive.batch',
+ this.auth.token(), liIds
+ ).toPromise().then(resp => this.postBatchAction(resp, liIds));
+ }, err => {}); // avoid console errors
+ }
+ );
}
markUnReceived(liIds: number[]) {
});
}
+ // order was activated as some point in past
+ isActivatedPo(): boolean {
+ if (this.picklistId) {
+ return false; // not an order
+ } else {
+ if (this.po()) {
+ this.poWasActivated = this.po().order_date() ? true : false;
+ }
+ return this.poWasActivated;
+ }
+ }
+
+ isPendingPo(): boolean {
+ if (this.picklistId || !this.po()) {
+ return false;
+ } else {
+ return this.po().order_date() ? false : true;
+ }
+ }
+
// For PO's, lineitems can only be deleted if they are pending order.
canDeleteLis(): boolean {
const li = this.pageOfLineitems[0];
Boolean(this.poId)
);
}
+
+ lineitemDisposition(li: IdlObject): LINEITEM_DISPOSITION {
+ return this.liService.lineitemDisposition(li);
+ }
}
} from '@eg/share/item-location-select/item-location-select.module';
import {LineitemWorksheetComponent} from './worksheet.component';
import {LineitemService} from './lineitem.service';
+import {PoService} from '../po/po.service';
import {LineitemComponent} from './lineitem.component';
import {LineitemNotesComponent} from './notes.component';
import {LineitemDetailComponent} from './detail.component';
import {LineitemCopyAttrsComponent} from './copy-attrs.component';
import {LineitemHistoryComponent} from './history.component';
import {BriefRecordComponent} from './brief-record.component';
+import {CreateAssetsComponent} from './create-assets.component';
import {CancelDialogComponent} from './cancel-dialog.component';
+import {AddToPoDialogComponent} from './add-to-po-dialog.component';
+import {DeleteLineitemsDialogComponent} from './delete-lineitems-dialog.component';
+import {AddCopiesDialogComponent} from './add-copies-dialog.component';
+import {BibFinderDialogComponent} from './bib-finder-dialog.component';
+import {BatchUpdateCopiesDialogComponent} from './batch-update-copies-dialog.component';
+import {LinkInvoiceDialogComponent} from './link-invoice-dialog.component';
+import {ExportAttributesDialogComponent} from './export-attributes-dialog.component';
+import {ClaimPolicyDialogComponent} from './claim-policy-dialog.component';
+import {ManageClaimsDialogComponent} from './manage-claims-dialog.component';
+import {LineitemAlertDialogComponent} from './lineitem-alert-dialog.component';
import {MarcEditModule} from '@eg/staff/share/marc-edit/marc-edit.module';
+import {AcqCommonModule} from '../acq-common.module';
@NgModule({
declarations: [
LineitemCopyAttrsComponent,
LineitemHistoryComponent,
CancelDialogComponent,
+ AddToPoDialogComponent,
+ DeleteLineitemsDialogComponent,
+ AddCopiesDialogComponent,
+ BibFinderDialogComponent,
+ BatchUpdateCopiesDialogComponent,
+ LinkInvoiceDialogComponent,
+ ExportAttributesDialogComponent,
+ ClaimPolicyDialogComponent,
+ ManageClaimsDialogComponent,
+ LineitemAlertDialogComponent,
BriefRecordComponent,
+ CreateAssetsComponent,
LineitemWorksheetComponent
],
exports: [
LineitemListComponent,
- CancelDialogComponent
+ CancelDialogComponent,
+ AddToPoDialogComponent,
+ DeleteLineitemsDialogComponent,
+ AddCopiesDialogComponent,
+ LinkInvoiceDialogComponent,
+ ExportAttributesDialogComponent,
+ ClaimPolicyDialogComponent,
+ ManageClaimsDialogComponent,
+ LineitemAlertDialogComponent
],
imports: [
StaffCommonModule,
ItemLocationSelectModule,
- MarcEditModule
+ MarcEditModule,
+ HttpClientModule,
+ AcqCommonModule
],
providers: [
- LineitemService
+ LineitemService,
+ PoService
]
})
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';
+import {saveAs} from 'file-saver';
+import {LineitemAlertDialogComponent} from './lineitem-alert-dialog.component';
+
+const LINEITEM_DISPOSITIONS:
+ 'new' | 'selector-ready' | 'order-ready' | 'pending-order' | 'on-order' | 'received' | 'delayed' = null;
+export type LINEITEM_DISPOSITION = typeof LINEITEM_DISPOSITIONS;
const COPY_ORDER_DISPOSITIONS:
'canceled' | 'delayed' | 'received' | 'on-order' | 'pre-order' = null;
export type COPY_ORDER_DISPOSITION = typeof COPY_ORDER_DISPOSITIONS;
+const ORDER_IDENT_ATTRS = [
+ 'isbn',
+ 'issn',
+ 'upc'
+];
export interface BatchLineitemStruct {
id: number;
toCache?: boolean;
}
+interface LineitemAlertData {
+ liId: number;
+ title: string;
+ alertText: IdlObject;
+ alertComment: string;
+}
+
@Injectable()
export class LineitemService {
// Alerts the user has already confirmed are OK.
alertAcks: {[id: number]: boolean} = {};
+ naturalCollator = new Intl.Collator(undefined,
+ {numeric: true, sensitivity: 'base', ignorePunctuation: true});
+
constructor(
private idl: IdlService,
private net: NetService,
private loc: ItemLocationService
) {}
+ clearLiCache() {
+ this.liCache = [];
+ }
+
getFleshedLineitems(ids: number[],
params: FleshCacheParams = {}): Observable<BatchLineitemStruct> {
flesh_order_summary: true,
flesh_cancel_reason: true,
flesh_li_details: true,
+ flesh_li_details_receiver: true,
flesh_notes: true,
flesh_fund: true,
flesh_circ_modifier: true,
flesh_pl: true,
flesh_formulas: true,
flesh_copies: true,
+ flesh_claim_policy: true,
clear_marc: false,
- apply_order_identifiers: true
+ apply_order_identifiers: true,
+ flesh_queued_record: true
}, params.fleshMore || {});
return this.net.request(
));
}
+ updateLiDetailsMulti(inLids: IdlObject[]): Observable<BatchLineitemUpdateStruct> {
+ const lids = inLids.filter(copy =>
+ (copy.isnew() || copy.ischanged() || copy.isdeleted()));
+
+ return from(
+
+ // Ensure we have the updated fund/loc/mod values before
+ // sending the copies off to be updated and then re-drawn.
+ this.fetchFunds(lids.map(lid => lid.fund()))
+ .then(_ => this.fetchLocations(lids.map(lid => lid.location())))
+ .then(_ => this.fetchCircMods(lids.map(lid => lid.circ_modifier())))
+
+ ).pipe(switchMap(_ =>
+ 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
return 'on-order';
} else { return 'pre-order'; }
}
+
+ // state/disposition of a single lineitem
+ lineitemDisposition(lineitem: IdlObject): LINEITEM_DISPOSITION {
+ if (lineitem.cancel_reason() && lineitem.cancel_reason().keep_debits() === 't') {
+ return 'delayed';
+ } else {
+ return lineitem.state();
+ }
+ }
+
+ // convenience function for sorting values
+ nullableCompare(a_val: any, b_val: any): number {
+ return a_val === b_val ? 0 :
+ a_val === null ? 1 :
+ b_val === null ? -1 :
+ this.naturalCollator.compare(a_val, b_val);
+ }
+
+ // Given a line item, get its sort key
+ getLISortKey(li: IdlObject, field: string): any {
+ let vals = [];
+ switch (field) {
+ case 'li_id':
+ return li.id();
+ break;
+ case 'title':
+ vals = li.attributes().filter(x => x.attr_name() === 'title');
+ return vals.length ? vals[0].attr_value().replace(/^(a|an|the|el|la) /i, '') : null;
+ break;
+ case 'author':
+ vals = li.attributes().filter(x => x.attr_name() === 'author');
+ return vals.length ? vals[0].attr_value() : null;
+ break;
+ case 'publisher':
+ vals = li.attributes().filter(x => x.attr_name() === 'publisher');
+ return vals.length ? vals[0].attr_value() : null;
+ break;
+ case 'order_ident':
+ vals = li.attributes().filter(x => ORDER_IDENT_ATTRS.includes(x.attr_name()));
+ return vals.length ? vals[0].attr_value() : null;
+ break;
+ default:
+ return li.id();
+ }
+ }
+
+ doExportSingleAttributeList(ids: number[], attr: string) {
+ if (!attr) { return; }
+ const values: string[] = [];
+ this.getFleshedLineitems(ids, { fromCache: true }).subscribe(
+ li => values.push(this.getFirstAttributeValue(li.lineitem, attr, 'lineitem_marc_attr_definition')),
+ err => {},
+ () => {
+ const filtered = values.filter(x => x !== '');
+ saveAs(
+ new Blob(
+ [ filtered.join('\n') + '\n' ],
+ { type: 'text/plain;charset=utf-8' }
+ ),
+ 'export_attr_list.txt'
+ );
+ }
+ );
+ }
+
+ checkLiAlerts(lis: IdlObject[], dialog: LineitemAlertDialogComponent): Promise<boolean> {
+
+ let promise = Promise.resolve(true);
+
+ const alerts: LineitemAlertData[] = [];
+ lis.forEach(li => {
+ li.lineitem_notes().filter(
+ note => note.alert_text() && !this.alertAcks[note.id()]
+ ).forEach(alert =>
+ alerts.push({
+ liId: li.id(),
+ title: this.getFirstAttributeValue(li, 'title'),
+ alertText: alert.alert_text(),
+ alertComment: alert.value()
+ })
+ );
+ });
+
+ if (alerts.length === 0) { return promise; }
+
+ dialog.numAlerts = alerts.length;
+
+ alerts.forEach((alert, i) => {
+ promise = promise.then(_ => {
+ dialog.liId = alert.liId;
+ dialog.title = alert.title;
+ dialog.alertText = alert.alertText;
+ dialog.alertComment = alert.alertComment;
+ dialog.alertIndex = i + 1;
+ return dialog.open().toPromise().then(ok => {
+ if (!ok) { return Promise.reject(); }
+ this.alertAcks[alert.alertText.id()] = true;
+ return true;
+ });
+ });
+ });
+
+ return promise;
+ }
}
--- /dev/null
+<ng-template #dialogContent>
+ <form class="form-validated">
+ <div class="modal-header bg-info">
+ <h3 class="modal-title" i18n>Link Invoice</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 *ngIf="liIds && liIds.length">Line Item(s) selected:
+ <span *ngFor="let id of liIds; last as isLast">
+ {{id}}<span *ngIf="!isLast">,</span>
+ </span>
+ </h4>
+ <div class="d-flex">
+ <div class="flex-1">
+ <label for="provider-input" i18n>Provider</label>
+ </div>
+ <div class="flex-3">
+ <eg-combobox domId="provider-input" [(ngModel)]="provider"
+ style="border-left-width: 0px"
+ name="provider-input"
+ idlIncludeLibraryInLabel="owner"
+ [required]="true"
+ [asyncSupportsEmptyTermClick]="true"
+ [idlQueryAnd]="{active: 't'}" idlClass="acqpro">
+ </eg-combobox>
+ </div>
+ </div>
+ <div class="d-flex mt-2">
+ <div class="flex-1">
+ <label for="invoice-input" i18n>Invoice</label>
+ </div>
+ <div class="flex-3">
+ <eg-combobox domId="invoice-input" [(ngModel)]="invoice"
+ style="border-left-width: 0px"
+ [readOnly]="!provider || !provider?.id"
+ name="invoice-input"
+ [required]="true"
+ idlField="inv_ident"
+ [asyncSupportsEmptyTermClick]="true"
+ [idlQueryAnd]="{provider: provider?.id}" idlClass="acqinv">
+ </eg-combobox>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-success"
+ [disabled]="!invoice || !invoice.id"
+ (click)="close(invoice.id)" i18n>Link Invoice</button>
+ <button type="button" class="btn btn-warning"
+ (click)="close()" i18n>Cancel</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-link-invoice-dialog',
+ templateUrl: './link-invoice-dialog.component.html'
+})
+
+export class LinkInvoiceDialogComponent extends DialogComponent {
+ @Input() liIds: number[] = [];
+ @Input() poId: number = null;
+
+ provider: ComboboxEntry;
+ invoice: ComboboxEntry;
+
+ constructor(private modal: NgbModal) { super(modal); }
+}
--- /dev/null
+<ng-template #dialogContent>
+ <form class="form-validated">
+ <div class="modal-header bg-info">
+ <h3 class="modal-title" i18n>Manage Claims</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>Claims</h4>
+ <span i18n>Against line item {{liService.getFirstAttributeValue(li, 'title')}} ({{li.id()}})</span>
+ <ul>
+ <li *ngFor="let lid of lidsWithClaims" i18n>
+ {{lid.barcode()}} /
+ <ng-container *ngIf="lid.cancel_reason()">Cancelled ({{lid.cancel_reason().label()}})</ng-container>
+ <ng-container *ngIf="lid.recv_time() && !lid.cancel_reason()">Received {{lid.recv_time() | formatValue:'timestamp'}}</ng-container>
+ <ng-container *ngIf="!lid.recv_time() && !lid.cancel_reason()">Not received</ng-container>
+ <ul>
+ <li *ngFor="let claim of lid.claims()">
+ {{claim.type().code()}} <a href="javascript:;" (click)="printVoucher(lid.id())">Print Voucher</a>
+ </li>
+ </ul>
+ </li>
+ </ul>
+ <hr>
+ <h4 i18n>Initiate New Claims</h4>
+ <div *ngFor="let lid of li.lineitem_details()" i18n>
+ <input type="checkbox" name="lidsToClaim" [(ngModel)]="lid._selected_for_claim">
+ {{lid.barcode()}} /
+ <ng-container *ngIf="lid.cancel_reason()">Cancelled ({{lid.cancel_reason().label()}})</ng-container>
+ <ng-container *ngIf="lid.recv_time() && !lid.cancel_reason()">Received {{lid.recv_time() | formatValue:'timestamp'}}</ng-container>
+ <ng-container *ngIf="!lid.recv_time() && !lid.cancel_reason()">Not received</ng-container>
+ </div>
+ <ng-container *ngIf="claimEventTypes.length > 0">
+ <label for="selectClaimEventTypes" i18n>Select Claim Action(s)</label>
+ <select class="form-control" multiple="true" [size]="claimEventTypes.length"
+ [(ngModel)]="selectedClaimEventTypes" [ngModelOptions]="{standalone: true}" id="selectClaimEventTypes">
+ <option *ngFor="let clet of claimEventTypes" [value]="clet.id()" i18n>
+ {{clet.code()}} ({{clet.org_unit().shortname()}}) <i>{{clet.description()}}</i>
+ <ng-container *ngIf="clet.library_initiated()"> [Library initiated]</ng-container>
+ </option>
+ </select>
+ </ng-container>
+ <label for="claimType" i18n>Claim Type</label>
+ <eg-combobox domId="claimType" name="claimType"
+ [asyncSupportsEmptyTermClick]="true"
+ idlClass="acqclt" [(ngModel)]="claimType" [ngModelOptions]="{standalone: true}"></eg-combobox>
+ <label for="note" i18n>Claim Note</label>
+ <input class="form-control" type="text" i18n-placeholder placeholder="Note" [(ngModel)]="note"
+ [ngModelOptions]="{standalone: true}" id="note">
+ </div>
+
+ <div class="modal-footer">
+ <button type="button" class="btn btn-success"
+ [disabled]="!canPerformClaim()"
+ (click)="claimItems()" i18n>Claim Selected</button>
+ <button type="button" class="btn btn-warning"
+ (click)="close()" i18n>Exit Dialog</button>
+ </div>
+ </form>
+</ng-template>
+
+<ng-template #printTemplate let-context>
+ <div>
+ <h1>Claim Voucher</h1>
+ <hr>
+ <span [innerHtml]="context.voucher"></span>
+ </div>
+</ng-template>
--- /dev/null
+import {Component, Input, ViewChild, TemplateRef} from '@angular/core';
+import {Observable} from 'rxjs';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+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';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {LineitemService} from '../lineitem/lineitem.service';
+import {PrintService} from '@eg/share/print/print.service';
+
+@Component({
+ selector: 'eg-acq-manage-claims-dialog',
+ templateUrl: './manage-claims-dialog.component.html'
+})
+
+export class ManageClaimsDialogComponent extends DialogComponent {
+ @Input() li: IdlObject;
+
+ @ViewChild('printTemplate', { static: true }) private printTemplate: TemplateRef<any>;
+
+ lidsWithClaims: IdlObject[] = [];
+
+ note = '';
+ claimEventTypes: number[] = [];
+ selectedClaimEventTypes: number[] = [];
+ claimType: ComboboxEntry;
+
+ constructor(
+ private modal: NgbModal,
+ private net: NetService,
+ private auth: AuthService,
+ private pcrud: PcrudService,
+ private printer: PrintService,
+ private liService: LineitemService
+ ) { super(modal); }
+
+ open(args?: NgbModalOptions): Observable<any> {
+ if (!args) {
+ args = {};
+ }
+
+ this.lidsWithClaims = this.getLidsWithClaims();
+ this.note = '';
+ this.claimEventTypes = [];
+ this.selectedClaimEventTypes = [];
+ this.getClaimEventTypes();
+
+ return super.open(args);
+ }
+
+ getLidsWithClaims(): IdlObject[] {
+ return this.li.lineitem_details().filter(x => x.claims().length > 0);
+ }
+
+ getClaimEventTypes() {
+ this.pcrud.retrieveAll('acqclet',
+ { 'order_by': {'acqclet': 'code'}, flesh: 1, flesh_fields: {acqclet: ['org_unit']} },
+ {}
+ ).subscribe(t => this.claimEventTypes.push(t));
+ }
+
+ canPerformClaim(): boolean {
+ if (!this.claimType) { return false; }
+ if (!this.claimType.id) { return false; }
+ const lidsToClaim = this.li.lineitem_details().filter(x => x._selected_for_claim);
+ if (lidsToClaim.length < 1) { return false; }
+ return true;
+ }
+
+ claimItems() {
+ if (!this.canPerformClaim()) { return; }
+ const lidsToClaim = this.li.lineitem_details()
+ .filter(x => x._selected_for_claim)
+ .map(x => x.id());
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.claim.lineitem_detail.atomic',
+ this.auth.token(),
+ lidsToClaim, null,
+ this.claimType.id,
+ this.note,
+ null,
+ this.selectedClaimEventTypes
+ ).subscribe(result => {
+ if (result && result.length) {
+ const voucher = result.map(x => x.template_output().data()).join('<hr>');
+ this.printer.print({
+ template: this.printTemplate,
+ contextData: { voucher: voucher },
+ printContext: 'default'
+ });
+ }
+ this.close(true);
+ });
+ }
+
+ printVoucher(lidId: number) {
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.claim.voucher.by_lineitem_detail',
+ this.auth.token(), lidId
+ ).subscribe(result => {
+ if (!result) { return; }
+ this.printer.print({
+ template: this.printTemplate,
+ contextData: { voucher: result.template_output().data() },
+ printContext: 'default'
+ });
+ });
+ }
+}
</label>
</div>
<button class="btn btn-sm btn-success ml-2" [disabled]="!noteText"
- (click)="newNote()" i18n>New Note</button>
+ (click)="newNote()" i18n>Create Note</button>
+ <div class="ml-3 mr-3">|</div>
+ <input type="text" class="form-control form-control-sm" id="note-text-input"
+ [(ngModel)]="alertComments" placeholder="Alert Comments" i18n-placeholder/>
<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>
+ <button class="btn btn-sm btn-success ml-2" [disabled]="!alertEntry"
+ (click)="newNote(true)" i18n>Create Alert</button>
<a class="ml-auto" href="javascript:;" (click)="close()" title="Close" i18n-title>
<span class="material-icons text-danger">close</span>
</a>
@Input() lineitem: IdlObject;
noteText: string;
+ alertComments: string;
vendorPublic = false;
alertEntry: ComboboxEntry;
const note = this.idl.create('acqlin');
note.isnew(true);
note.lineitem(this.lineitem.id());
- note.value(this.noteText || '');
if (isAlert) {
+ note.value(this.alertComments || '');
note.alert_text(this.alertEntry.id);
} else {
+ note.value(this.noteText || '');
note.vendor_public(this.vendorPublic ? 't' : 'f');
}
flesh_cancel_reason: true,
flesh_li_details: true,
flesh_fund: true,
- flesh_li_details_copy: true,
- flesh_li_details_location: true,
+ flesh_copies: true,
+ flesh_location: true,
+ flesh_copy_location: true,
+ flesh_call_number: true,
flesh_li_details_receiver: true,
- distribution_formulas: true
+ flesh_formulas: true
}
).toPromise()
.then(li => this.lineitem = li)
import {PicklistComponent} from './picklist.component';
import {PicklistSummaryComponent} from './summary.component';
import {HttpClientModule} from '@angular/common/http';
-import {UploadComponent} from './upload.component';
+import {AcqCommonModule} from '../acq-common.module';
@NgModule({
declarations: [
PicklistComponent,
- PicklistSummaryComponent,
- UploadComponent
+ PicklistSummaryComponent
],
imports: [
StaffCommonModule,
LineitemModule,
HoldingsModule,
PicklistRoutingModule,
- HttpClientModule
+ HttpClientModule,
+ AcqCommonModule
],
providers: []
})
-<eg-staff-banner bannerText="Load MARC Order Records" i18n-bannerText>
+<eg-staff-banner bannerText="Load MARC Order Records" i18n-bannerText *ngIf="mode !== 'getImportParams'">
</eg-staff-banner>
-<div class="row">
+<div class="row" *ngIf="mode !== 'getImportParams'">
<div class="ml-auto mr-3"><a i18n href="/eg/staff/acq/legacy/picklist/upload">Legacy Upload Interface</a></div>
</div>
+<eg-string #loadMarcOrderTemplateSavedString i18n-text text="Load MARC Order Record Template Saved"></eg-string>
+<eg-string #loadMarcOrderTemplateDeletedString i18n-text text="Load MARC Order Record Template Deleted"></eg-string>
+<eg-string #loadMarcOrderTemplateSetAsDefaultString i18n-text text="Load MARC Order Record Template Set As Default"></eg-string>
<eg-alert-dialog #dupeQueueAlert i18n-dialogBody
dialogBody="A queue with the requested name already exists.">
<div class="common-form striped-odd form-validated ml-3 mr-3">
<div class="row">
<div class="col-lg-3">
- <label for="template-select" i18n>Apply/Create Form Template</label>
+ <label for="template-select" i18n *ngIf="mode !== 'getImportParams'">Apply/Create Form Template</label>
+ <label for="template-select" i18n *ngIf="mode === 'getImportParams'">Apply Form Template</label>
</div>
<div class="col-lg-3">
<eg-combobox #formTemplateSelector
[entries]="formatTemplateEntries()">
</eg-combobox>
</div>
- <div class="col-lg-6">
+ <div class="col-lg-6" *ngIf="mode !== 'getImportParams'">
<button class="btn btn-success"
[disabled]="!selectedTemplate"
- (click)="saveTemplate()" i18n>Save As New Template</button>
+ (click)="saveTemplate()" i18n>Save Template</button>
<button class="btn btn-outline-primary ml-3"
[disabled]="!selectedTemplate"
(click)="markTemplateDefault()" i18n>Mark Template as Default</button>
</div>
</div>
+ <ng-container *ngIf="mode !== 'getImportParams'">
<h2>Purchase Order</h2>
<div class="row">
<div class="col-lg-3">
<div class="col-lg-3">
<eg-combobox #providerSelector
- id="provider-select"
- [entries]="formatEntries('providersList')"
- (onChange)="selectEntry($event, 'providersList')"
+ domId="provider-select"
+ [selectedId]="selectedProvider" (onChange)="selectedProvider = $event.id"
+ style="border-left-width: 0px"
[required]="true"
- [startId]="selectedProvider">
+ [asyncSupportsEmptyTermClick]="true"
+ idlIncludeLibraryInLabel="owner"
+ [idlQueryAnd]="{active: 't'}" idlClass="acqpro">
</eg-combobox>
</div>
</div>
<div class="col-lg-3">
<eg-org-select
- [applyOrgId]="orderingAgency"
- (onChange)="orgOnChange($event)">
+ [applyOrgId]="orderingAgency"
+ (onChange)="orgOnChange($event)"
+ [limitPerms]="['CREATE_PICKLIST','CREATE_PURCHASE_ORDER']">
</eg-org-select>
</div>
</eg-combobox>
</div>
</div>
+ </ng-container> <!-- purchase order section -->
-
- <h2>Upload Settings</h2>
+ <h2 *ngIf="mode !== 'getImportParams'">Upload Settings</h2>
<div class="row">
<div class="col-lg-3">
<input type="number" step="0.1" id="min-quality-ratio"
class="form-control" [(ngModel)]="minQualityRatio">
</div>
+ <ng-container *ngIf="mode !== 'getImportParams'">
<div class="col-lg-3">
<label for="load-items" i18n>Load Items for Imported Records</label>
</div>
id="load-items"
[(ngModel)]="loadItems">
</div>
+ </ng-container>
</div>
- <h2>This Upload</h2>
+ <h2 *ngIf="mode !== 'getImportParams'">This Upload</h2>
<div class="row">
<div class="col-lg-3">
<label for="queue-select" i18n>Select or Create a Queue</label>
</eg-combobox>
</div>
</div>
- <div class="row" *ngIf="!importSelection()">
+ <div class="row" *ngIf="!importSelection() && mode !== 'getImportParams'">
<div class="col-lg-3">
<label for="upload-file" i18n>File to Upload:</label>
</div>
</button>
</div>
</div>
- <div class="row">
+ <div class="row" *ngIf="mode !== 'getImportParams'">
<div class="col-lg-6 offset-lg-3">
<button class="btn btn-success btn-lg btn-block font-weight-bold"
[disabled]="isUploading || !hasNeededData()"
(click)="upload()" i18n>Upload</button>
</div>
</div>
- <div class="row" [hidden]="!showProgress || uploadComplete">
+ <div class="row" *ngIf="mode === 'getImportParams'">
+ <div class="col-lg-6 offset-lg-3">
+ <button class="btn btn-success btn-lg btn-block font-weight-bold"
+ [disabled]="customActionProcessing || !hasNeededData()"
+ (click)="performCustomAction()" i18n>Submit</button>
+ </div>
+ </div>
+ <div class="row" [hidden]="!isUploading || uploadComplete">
<div class="col-lg-3">
<label i18n>Upload File to Server</label>
</div>
</div>
<div class="row" [hidden]="!uploadComplete">
- <div class="col-lg-3 offset-lg-3">
- <h2><label i18n>Upload Complete!</label></h2>
- </div>
- </div>
- <div class="row" [hidden]="!uploadComplete">
- <div class="col-sm-1 offset-lg-3">
- <label i18n>Go to:</label>
- </div>
- <div><a routerLink="/staff/cat/vandelay/queue/{{recordType}}/{{activeQueueId}}" target="_blank" i18n>Queue</a></div>
- <div class="col-sm-1" [hidden]="!selectedSelectionList"><a href="/eg/staff/acq/legacy/picklist/view/{{activeSelectionListId}}" target="_blank">Selection List</a></div>
- <div class="col-sm-2" [hidden]="!createPurchaseOrder"><a routerLink="/eg/staff/acq/po/{{newPO}}" target="_blank">Purchase Order</a></div>
- </div>
+ <ng-container *ngIf="uploadError">
+ <div class="col-lg-6 offset-lg-3">
+ <h2><label i18n>Upload Error!</label></h2>
+ <div class="row">
+ <div class="col alert-danger" i18n>Error {{uploadErrorCode}} ({{uploadErrorText}})</div>
+ </div>
+ </div>
+ </ng-container>
+ <ng-container *ngIf="!uploadError">
+ <div class="col-lg-6 offset-lg-3">
+ <h2><label i18n>Upload Complete!</label></h2>
+ <div class="row" [hidden]="!uploadComplete">
+ <div class="col-2">
+ <label i18n>Go to:</label>
+ </div>
+ <div class="col-2"><a routerLink="/staff/cat/vandelay/queue/{{recordType}}/{{activeQueueId}}" target="_blank" i18n>Queue</a></div>
+ <div class="col-2" [hidden]="!selectedSelectionList"><a routerLink="/staff/acq/picklist/{{activeSelectionListId}}" target="_blank">Selection List</a></div>
+ <div class="col-2" [hidden]="!createPurchaseOrder"><a routerLink="/staff/acq/po/{{newPO}}" target="_blank">Purchase Order</a></div>
+ </div>
+ </div>
+ </ng-container>
+ </div>
import {Component, OnInit, AfterViewInit, Input,
ViewChild, OnDestroy} from '@angular/core';
+import {Router} from '@angular/router';
import {Subject} from 'rxjs';
import {tap} from 'rxjs/operators';
import {IdlObject} from '@eg/core/idl.service';
import {EventService} from '@eg/core/event.service';
import {OrgService} from '@eg/core/org.service';
import {AuthService} from '@eg/core/auth.service';
+import {StringComponent} from '@eg/share/string/string.component';
import {ToastService} from '@eg/share/toast/toast.service';
import {ComboboxComponent,
ComboboxEntry} from '@eg/share/combobox/combobox.component';
@Component({
+ selector: 'eg-acq-upload',
templateUrl: './upload.component.html'
})
export class UploadComponent implements OnInit, AfterViewInit, OnDestroy {
+ // mode can be one of
+ // upload: actually upload and process a MARC order file
+ // getImportParams: gather import parameters to use when creating
+ // assets for a purchase order; the invoker
+ // would do the actual asset creation
+ @Input() mode = 'upload';
+
+ @Input() customAction: (args: any) => void;
+ customActionProcessing = false;
+
settings: Object = {};
recordType: string;
selectedQueue: ComboboxEntry;
isUploading: boolean;
uploadProcessing: boolean;
+ uploadError: boolean;
+ uploadErrorCode: string;
+ uploadErrorText: string;
uploadComplete: boolean;
// Generated by the server
private formTemplateSelector: ComboboxComponent;
@ViewChild('bibSourceSelector', { static: true })
private bibSourceSelector: ComboboxComponent;
- @ViewChild('providerSelector', {static: true})
+ @ViewChild('providerSelector', {static: false})
private providerSelector: ComboboxComponent;
- @ViewChild('fiscalYearSelector', { static: true })
+ @ViewChild('fiscalYearSelector', { static: false })
private fiscalYearSelector: ComboboxComponent;
@ViewChild('selectionListSelector', { static: true })
private selectionListSelector: ComboboxComponent;
private fallThruMergeProfileSelector: ComboboxComponent;
@ViewChild('dupeQueueAlert', { static: true })
private dupeQueueAlert: AlertDialogComponent;
+ @ViewChild('loadMarcOrderTemplateSavedString', { static: false })
+ private loadMarcOrderTemplateSavedString: StringComponent;
+ @ViewChild('loadMarcOrderTemplateDeletedString', { static: false })
+ private loadMarcOrderTemplateDeletedString: StringComponent;
+ @ViewChild('loadMarcOrderTemplateSetAsDefaultString', { static: false })
+ private loadMarcOrderTemplateSetAsDefaultString: StringComponent;
+
constructor(
private http: HttpClient,
+ private router: Router,
private toast: ToastService,
private evt: EventService,
private net: NetService,
private store: ServerStoreService,
private vlagent: PicklistUploadService
) {
+ // force a reload of the component if we navigate to it
+ // from itself
+ this.router.routeReuseStrategy.shouldReuseRoute = () => {
+ return false;
+ };
this.applyDefaults();
this.applySettings();
}
this.vlagent.getAllQueues('bib'),
this.vlagent.getMatchSets('bib'),
this.vlagent.getBibSources(),
- this.vlagent.getFiscalYears(),
- this.vlagent.getProvidersList(),
+ this.vlagent.getFiscalYears(this.auth.user().ws_ou()).then( years => {
+ this.vlagent.getDefaultFiscalYear(this.auth.user().ws_ou()).then(y => {
+ this.selectedFiscalYear = y.id();
+ if (this.fiscalYearSelector) {
+ this.fiscalYearSelector.applyEntryId(this.selectedFiscalYear);
+ }
+ });
+ }),
this.vlagent.getSelectionLists(),
this.vlagent.getItemImportDefs(),
- this.org.settings(['vandelay.default_match_set']).then(
- s => this.defaultMatchSet = s['vandelay.default_match_set']),
+ this.org.settings(['vandelay.default_match_set']).then(
+ s => this.defaultMatchSet = s['vandelay.default_match_set']),
this.loadTemplates()
];
orgOnChange(org: IdlObject) {
this.orderingAgency = org.id();
+ this.vlagent.getFiscalYears(this.orderingAgency).then( years => {
+ this.vlagent.getDefaultFiscalYear(this.orderingAgency).then(
+ y => { this.selectedFiscalYear = y.id(); this.fiscalYearSelector.applyEntryId(this.selectedFiscalYear); }
+ );
+ });
}
loadTemplates() {
return {id: s.id(), label: s.source()};
});
- case 'providersList':
- return (this.vlagent.providersList || []).map(
- p => {
- return {id: p.id(), label: p.code()};
- });
-
case 'fiscalYears':
return (this.vlagent.fiscalYears || []).map(
fy => {
this.recordType = id;
break;
- case 'providersList':
- this.selectedProvider = id;
- break;
-
case 'bibSources':
this.selectedBibSource = id;
break;
}
hasNeededData(): boolean {
+ if (this.mode === 'getImportParams') {
+ return this.selectedQueue ? true : false;
+ }
return this.selectedQueue &&
Boolean(this.selectedFile) &&
Boolean(this.selectedFiscalYear) &&
);
}
+ // helper method to return the year string rather than the FY ID
+ // TODO: can remove this once fiscal years are better managed
+ _getFiscalYearLabel(): string {
+ if (this.selectedFiscalYear) {
+ const found = (this.vlagent.fiscalYears || []).find(x => x.id() === this.selectedFiscalYear);
+ return found ? found.year() : '';
+ } else {
+ return '';
+ }
+ }
+
+ performCustomAction() {
+
+ const vandelayOptions = {
+ match_set: this.selectedMatchSet,
+ import_no_match: this.importNonMatching,
+ auto_overlay_exact: this.mergeOnExact,
+ auto_overlay_best_match: this.mergeOnBestMatch,
+ auto_overlay_1match: this.mergeOnSingleMatch,
+ merge_profile: this.selectedMergeProfile,
+ fall_through_merge_profile: this.selectedFallThruMergeProfile,
+ match_quality_ratio: this.minQualityRatio,
+ bib_source: this.selectedBibSource,
+ create_assets: this.loadItems,
+ queue_name: this.selectedQueue.label
+ };
+
+ const args = {
+ provider: this.selectedProvider,
+ ordering_agency: this.orderingAgency,
+ create_po: this.createPurchaseOrder,
+ activate_po: this.activatePurchaseOrder,
+ fiscal_year: this._getFiscalYearLabel(),
+ picklist: this.activeSelectionListId,
+ vandelay: vandelayOptions
+ };
+
+ this.customActionProcessing = true;
+ this.customAction(args);
+ }
+
resetProgressBars() {
this.uploadProgress.update({value: 0, max: 1});
}
processUpload(): Promise<any> {
this.uploadProcessing = true;
+ this.uploadError = false;
if (this.vlagent.importSelection) {
return Promise.resolve();
const spoolType = this.recordType;
const vandelayOptions = {
+ match_set: this.selectedMatchSet,
import_no_match: this.importNonMatching,
auto_overlay_exact: this.mergeOnExact,
auto_overlay_best_match: this.mergeOnBestMatch,
ordering_agency: this.orderingAgency,
create_po: this.createPurchaseOrder,
activate_po: this.activatePurchaseOrder,
- fiscal_year: this.selectedFiscalYear,
+ fiscal_year: this._getFiscalYearLabel(),
picklist: this.activeSelectionListId,
vandelay: vandelayOptions
};
progress => {
const resp = this.evt.parse(progress);
console.log(progress);
- if (resp) { console.error(resp); return reject(); }
+ if (resp) {
+ this.uploadError = true;
+ this.uploadErrorCode = resp.textcode;
+ this.uploadErrorText = resp.payload;
+ this.uploadProcessing = false;
+ this.uploadComplete = true;
+ return reject();
+ }
if (progress.complete) {
this.uploadProcessing = false;
this.uploadComplete = true;
const template = {};
TEMPLATE_ATTRS.forEach(key => template[key] = this[key]);
- console.debug('Saving import profile', template);
-
this.formTemplates[this.selectedTemplate] = template;
- return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
+ this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates).then(x =>
+ this.loadMarcOrderTemplateSavedString.current()
+ .then(str => this.toast.success(str))
+ );
}
markTemplateDefault() {
Object.keys(this.formTemplates).forEach(
- name => delete this.formTemplates.default
+ name => delete this.formTemplates[name].default
);
this.formTemplates[this.selectedTemplate].default = true;
- return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
+ this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates).then(x =>
+ this.loadMarcOrderTemplateSetAsDefaultString.current()
+ .then(str => this.toast.success(str))
+ );
}
templateSelectorChange(entry: ComboboxEntry) {
this.bibSourceSelector.applyEntryId(this.selectedBibSource);
this.matchSetSelector.applyEntryId(this.selectedMatchSet);
- this.providerSelector.applyEntryId(this.selectedProvider);
- this.fiscalYearSelector.applyEntryId(this.selectedFiscalYear);
+ if (this.providerSelector) {
+ this.providerSelector.selectedId = this.selectedProvider;
+ }
+ if (this.fiscalYearSelector) {
+ this.fiscalYearSelector.applyEntryId(this.selectedFiscalYear);
+ }
this.mergeProfileSelector.applyEntryId(this.selectedMergeProfile);
this.fallThruMergeProfileSelector.applyEntryId(this.selectedFallThruMergeProfile);
}
deleteTemplate() {
delete this.formTemplates[this.selectedTemplate];
this.formTemplateSelector.selected = null;
- return this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates);
+ this.store.setItem(TEMPLATE_SETTING_NAME, this.formTemplates).then(x =>
+ this.loadMarcOrderTemplateDeletedString.current()
+ .then(str => this.toast.success(str))
+ );
}
}
mergeProfiles: IdlObject[];
providersList: IdlObject[];
fiscalYears: IdlObject[];
+ defaultFiscalYear: IdlObject;
selectionLists: IdlObject[];
queueType: string;
recordType: string;
});
}
- getProvidersList(): Promise<IdlObject[]> {
- if (this.providersList) {
- return Promise.resolve(this.providersList);
- }
-
- const owners = this.org.ancestors(this.auth.user().ws_ou(), true);
- return this.pcrud.search('acqpro',
- {owner: owners}, {order_by: {acqpro: ['code']}}, {atomic: true})
- .toPromise().then(providers => {
- this.providersList = providers;
- return providers;
- });
- }
-
getSelectionLists(): Promise<IdlObject[]> {
if (this.selectionLists) {
return Promise.resolve(this.selectionLists);
});
}
- getFiscalYears(): Promise<IdlObject[]> {
- if (this.fiscalYears) {
- return Promise.resolve(this.fiscalYears);
- }
+ getDefaultFiscalYear(org: number): Promise<IdlObject> {
+ return this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.org_unit.current_fiscal_year',
+ this.auth.token(), org
+ ).pipe(tap(afy => {
+ this.defaultFiscalYear = this.fiscalYears.filter(fy => Number(fy.year()) === Number(afy))[0];
+ })).toPromise().then(() => {
+ return this.defaultFiscalYear;
+ });
+ }
+ getFiscalYears(org: number): Promise<IdlObject[]> {
return this.pcrud.retrieveAll('acqfy',
{order_by: {acqfy: 'year'}},
{atomic: true}
).toPromise().then(years => {
- this.fiscalYears = years;
- return years;
+ this.fiscalYears = years.filter( y => y.calendar() === this.org.get(org).fiscal_calendar());
+ // if there are no entries, inject a special entry for the current year
+ if (!this.fiscalYears.length) {
+ const afy = this.idl.create('acqfy');
+ afy.id(-1);
+ afy.calendar(-1);
+ const now = new Date();
+ afy.year(now.getFullYear());
+ this.fiscalYears = [ afy ];
+ }
+ return this.fiscalYears;
});
}
<h4 i18n>Direct Charges, Taxes, Fees, etc.
- <button class="btn btn-info btn-sm" (click)="newCharge()">New Charge</button>
+ <button class="btn btn-info btn-sm" (click)="newCharge()" *ngIf="canModify">New Charge</button>
</h4>
+<eg-acq-disencumber-charge-dialog #disencumberChargeDialog></eg-acq-disencumber-charge-dialog>
+
<ng-container *ngIf="showBody">
<div class="row d-flex">
<div class="flex-2 p-2 font-weight-bold">Charge Type</div>
[asyncSupportsEmptyTermClick]="true"
(onChange)="charge.inv_item_type($event ? $event.id : null)"
i18n-placeholder placeholder="Charge Type..."
- [required]="true" [readOnly]="!charge.isnew()"></eg-combobox>
+ [required]="true" [readOnly]="!charge.isnew() && !charge.ischanged()"></eg-combobox>
</div>
<div class="flex-2 p-2">
<!-- the IDL does not require a fund, but the Perl code assumes
<eg-combobox idlClass="acqf" [selectedId]="charge.fund()"
(onChange)="charge.fund($event ? $event.id : null)"
i18n-placeholder placeholder="Fund..."
- [required]="true" [readOnly]="!charge.isnew()"
+ [asyncSupportsEmptyTermClick]="true"
+ [required]="true" [readOnly]="!charge.isnew() && !charge.ischanged()"
[idlQuerySort]="{acqf: 'year DESC, code'}"
[idlQueryAnd]="{active: 't'}">
</eg-combobox>
+ <span *ngIf="charge.fund_debit() && charge.fund_debit().fund() !== charge.fund()">
+ <br>
+ <i>Fund actually debited is
+ <eg-combobox idlClass="acqf" [selectedId]="charge.fund_debit().fund()"
+ [readOnly]="true"></eg-combobox></i>
+ </span>
</div>
<div class="flex-2 p-2">
- <span *ngIf="!charge.isnew()">{{charge.title()}}</span>
- <input *ngIf="charge.isnew()" type="text" class="form-control"
+ <span *ngIf="!charge.isnew() && !charge.ischanged()">{{charge.title()}}</span>
+ <input *ngIf="charge.isnew() || charge.ischanged()" 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"
+ <span *ngIf="!charge.isnew() && !charge.ischanged()">{{charge.author()}}</span>
+ <input *ngIf="charge.isnew() || charge.ischanged()" 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"
+ <span *ngIf="!charge.isnew() && !charge.ischanged()">{{charge.note()}}</span>
+ <input *ngIf="charge.isnew() || charge.ischanged()" 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"
+ <span *ngIf="!charge.isnew() && !charge.ischanged()">{{charge.estimated_cost() | currency}}</span>
+ <input *ngIf="charge.isnew() || charge.ischanged()" type="number" min="0" class="form-control"
i18n-placeholder placeholder="Esimated Cost..." [required]="true"
[ngModel]="charge.estimated_cost()" (ngModelChange)="charge.estimated_cost($event)"/>
+ <span *ngIf="charge.fund_debit()">
+ <br>
+ <span *ngIf="charge.fund_debit().encumbrance() === 't'" i18n>
+ <i>Amount encumbered is {{charge.fund_debit().amount() | currency}}</i>
+ </span>
+ <span *ngIf="charge.fund_debit().encumbrance() === 'f'" i18n>
+ <i>Amount expended is {{charge.fund_debit().amount() | currency}}</i>
+ </span>
+ </span>
</div>
- <div class="flex-1 p-1">
- <button *ngIf="charge.isnew()" class="btn btn-success btn-sm"
+ <div class="flex-2 p-1">
+ <button *ngIf="canModify" [disabled]="!(charge.isnew() || charge.ischanged())" 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>
+ <button *ngIf="canModify" [disabled]="charge.isnew()" class="btn btn-outline-dark btn-sm ml-1"
+ (click)="charge.ischanged(true)" i18n>Edit</button>
+ <button class="btn btn-warning btn-sm ml-1"
+ (click)="disencumberCharge(charge)" *ngIf="canDisencumber(charge)" i18n>Disencumber</button>
+ <button class="btn btn-danger btn-sm ml-1"
+ (click)="removeCharge(charge)" *ngIf="canDelete(charge)" i18n>Remove</button>
</div>
</div>
</ng-container>
-import {Component, OnInit, OnDestroy, Input} from '@angular/core';
+import {Component, OnInit, OnDestroy, Input, ViewChild} from '@angular/core';
import {Subscription} from 'rxjs';
import {Router, ActivatedRoute, ParamMap} from '@angular/router';
import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {AuthService} from '@eg/core/auth.service';
+import {NetService} from '@eg/core/net.service';
+import {EventService} from '@eg/core/event.service';
import {PcrudService} from '@eg/core/pcrud.service';
import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
import {PoService} from './po.service';
+import {DisencumberChargeDialogComponent} from './disencumber-charge-dialog.component';
@Component({
templateUrl: 'charges.component.html',
export class PoChargesComponent implements OnInit, OnDestroy {
showBody = false;
+ canModify = false;
autoId = -1;
poSubscription: Subscription;
+ @ViewChild('disencumberChargeDialog') disencumberChargeDialog: DisencumberChargeDialogComponent;
+
constructor(
private idl: IdlService,
+ private net: NetService,
+ private evt: EventService,
+ private auth: AuthService,
private pcrud: PcrudService,
public poService: PoService
) {}
if (this.po()) {
// Sometimes our PO is already available at render time.
this.showBody = this.po().po_items().length > 0;
+ this.canModify = this.po().order_date() ? false : true;
}
// Other times we have to wait for it.
this.poSubscription = this.poService.poRetrieved.subscribe(() => {
this.showBody = this.po().po_items().length > 0;
+ this.canModify = this.po().order_date() ? false : true;
});
}
if (!charge.inv_item_type() || !charge.fund()) { return; }
if (typeof charge.estimated_cost() !== 'number') { return; }
- charge.id(undefined);
- this.pcrud.create(charge).toPromise()
- .then(item => {
- charge.id(item.id());
- charge.isnew(false);
- })
- .then(_ => this.poService.refreshOrderSummary());
+ if (charge.isnew()) {
+ charge.id(undefined);
+ this.pcrud.create(charge).toPromise()
+ .then(item => {
+ charge.id(item.id());
+ charge.isnew(false);
+ })
+ .then(_ => this.poService.refreshOrderSummary());
+ } else if (charge.ischanged()) {
+ this.pcrud.update(charge).toPromise()
+ .then(item => {
+ charge.ischanged(false);
+ })
+ .then(_ => this.poService.refreshOrderSummary());
+ }
+ }
+
+ canDisencumber(charge: IdlObject): boolean {
+ if (!this.po() || !this.po().order_date() || this.po().state() === 'cancelled') {
+ return false; // order must be loaded, activated, and not cancelled
+ }
+ if (!charge.fund_debit()) {
+ return false; // that which is not encumbered cannot be disencumbered
+ }
+
+ const debit = charge.fund_debit();
+ if (debit.encumbrance() === 'f') {
+ return false; // that which is expended cannot be disencumbered
+ }
+ if (debit.invoice_entry()) {
+ return false; // we shouldn't actually be a po_item that is
+ // linked to an invoice_entry, but if we are,
+ // do NOT touch
+ }
+ if (debit.invoice_items() && debit.invoice_items().length) {
+ return false; // we're linked to an invoice item, so the disposition of the
+ // invoice entry should govern things
+ }
+ if (Number(debit.amount()) === 0) {
+ return false; // we're already at zero
+ }
+ return true; // we're likely OK to disencumber
+ }
+
+ canDelete(charge: IdlObject): boolean {
+ if (!this.po()) {
+ return false;
+ }
+
+ const debit = charge.fund_debit();
+ if (debit && debit.encumbrance() === 'f') {
+ return false; // if it's expended, we can't just delete it
+ }
+ if (debit && debit.invoice_entry()) {
+ return false; // we shouldn't actually be a po_item that is
+ // linked to an invoice_entry, but if we are,
+ // do NOT touch
+ }
+ if (debit && debit.invoice_items() && debit.invoice_items().length) {
+ return false; // we're linked to an invoice item, so the disposition of the
+ // invoice entry should govern things
+ }
+ return true; // we're likely OK to delete
+ }
+
+ disencumberCharge(charge: IdlObject) {
+ this.disencumberChargeDialog.charge = charge;
+ this.disencumberChargeDialog.open().subscribe(doIt => {
+ if (!doIt) { return; }
+
+ return this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.po_item.disencumber',
+ this.auth.token(), charge.id()
+ ).toPromise().then(res => {
+ const evt = this.evt.parse(res);
+ if (evt) { return Promise.reject(evt + ''); }
+ }).then(_ => this.poService.refreshOrderSummary(true));
+ });
}
removeCharge(charge: IdlObject) {
);
if (!charge.isnew()) {
- this.pcrud.remove(charge).toPromise()
- .then(_ => this.poService.refreshOrderSummary());
+ return this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.po_item.delete',
+ this.auth.token(), charge.id()
+ ).toPromise().then(res => {
+ const evt = this.evt.parse(res);
+ if (evt) { return Promise.reject(evt + ''); }
+ }).then(_ => this.poService.refreshOrderSummary(true));
}
}
}
<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>
+<div class="col-lg-4 offset-lg-4" [hidden]="!initDone">
+ <div *ngIf="lineitems.length || origLiCount">
+ <span i18n>Creating for {{lineitems.length}} line items.</span>
+ <span i18n *ngIf="lineitems.length !== origLiCount" class="alert-warning">
+ (There were {{origLiCount}} selected, but not all were in a valid state
+ to be added to a purchase order.)
+ </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 (onChange)="orgChange($event)" domId="order-agency-input"
+ [limitPerms]="['CREATE_PURCHASE_ORDER']">
</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"/>
+ <input id="name-input" class="form-control" type="text" [ngModel]="poName"
+ (ngModelChange)="poName = $event; checkDuplicatePoName()"
+ />
+ </div>
+ <div *ngIf="dupeResults.dupeFound" class="alert alert-warning" i18n>
+ This name is already in used by another PO:
+ <a target="_blank" routerLink="/staff/acq/po/{{dupeResults.dupePoId}}">View PO</a>
</div>
<div class="form-group">
<label for="name-input" i18n>Provider</label>
- <eg-combobox domId="provider-input" [(ngModel)]="provider"
+ <eg-combobox domId="provider-input" [(ngModel)]="provider"
+ [asyncSupportsEmptyTermClick]="true"
+ idlIncludeLibraryInLabel="owner"
[idlQueryAnd]="{active: 't'}" idlClass="acqpro">
</eg-combobox>
</div>
Prepayment Required
</label>
</div>
- <div class="form-group form-check">
+ <div class="form-group form-check" *ngIf="lineitems.length">
<input type="checkbox" class="form-check-input"
[(ngModel)]="createAssets" id="create-assets">
<label class="form-check-label" for="create-assets" i18n>
import {LineitemService} from '../lineitem/lineitem.service';
import {CancelDialogComponent} from '../lineitem/cancel-dialog.component';
+const VALID_PRE_PO_LI_STATES = [
+ 'new',
+ 'selector-ready',
+ 'order-ready',
+ 'approved'
+];
@Component({
templateUrl: 'create.component.html',
initDone = false;
lineitems: number[] = [];
+ origLiCount = 0;
poName: string;
orderAgency: number;
provider: ComboboxEntry;
prepaymentRequired = false;
createAssets = false;
+ dupeResults = {
+ dupeFound: false,
+ dupePoId: -1
+ };
constructor(
private router: Router,
this.route.queryParamMap.subscribe((params: ParamMap) => {
this.lineitems = params.getAll('li').map(id => Number(id));
+ this.origLiCount = this.lineitems.length;
});
- this.load().then(_ => this.initDone = true);
+ this.load();
}
- load(): Promise<any> {
- return Promise.resolve();
+ load() {
+ this.dupeResults.dupeFound = false;
+ this.dupeResults.dupePoId = -1;
+ if (this.origLiCount > 0) {
+ const fleshed_lis: IdlObject[] = [];
+ this.liService.getFleshedLineitems(this.lineitems, { fromCache: false }).subscribe(
+ liStruct => {
+ fleshed_lis.push(liStruct.lineitem);
+ },
+ err => { },
+ () => {
+ this.lineitems = fleshed_lis.filter(li => VALID_PRE_PO_LI_STATES.includes(li.state()))
+ .map(li => li.id());
+ this.initDone = true;
+ }
+ );
+ } else {
+ this.initDone = true;
+ }
}
orgChange(org: IdlObject) {
this.orderAgency = org ? org.id() : null;
+ this.checkDuplicatePoName();
}
canCreate(): boolean {
- return (Boolean(this.orderAgency) && Boolean(this.provider));
+ return (Boolean(this.orderAgency) && Boolean(this.provider) &&
+ !this.dupeResults.dupeFound);
+ }
+
+ checkDuplicatePoName() {
+ this.poService.checkDuplicatePoName(this.orderAgency, this.poName, this.dupeResults);
}
create() {
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()]);
+ if (this.createAssets) {
+ this.router.navigate(
+ ['/staff/acq/po/' + resp.purchase_order.id() + '/create-assets']);
+ } else {
+ this.router.navigate(
+ ['/staff/acq/po/' + resp.purchase_order.id()]);
+ }
}
});
}
--- /dev/null
+<ng-template #dialogContent>
+ <form class="form-validated">
+ <div class="modal-header bg-info">
+ <h3 class="modal-title" i18n>Disencumber Direct Charge</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">
+ <div class="d-flex">
+ <div class="flex-2" i18n>Charge:</div>
+ <div class="flex-3">
+ <eg-combobox idlClass="aiit" [selectedId]="charge.inv_item_type()"
+ [readOnly]="true"></eg-combobox>
+ </div>
+ </div>
+ <div class="d-flex">
+ <div class="flex-2" i18n>Amount:</div>
+ <div class="flex-3">{{charge.estimated_cost()}}</div>
+ </div>
+ <div class="d-flex">
+ <div class="flex-2" i18n>Original Fund:</div>
+ <div class="flex-3">
+ <eg-combobox idlClass="acqf" [selectedId]="charge.fund()"
+ [readOnly]="true"></eg-combobox>
+ </div>
+ </div>
+ <div class="d-flex">
+ <div class="flex-2" i18n>Fund Debited:</div>
+ <div class="flex-3">
+ <eg-combobox idlClass="acqf" [selectedId]="charge.fund_debit().fund()"
+ [readOnly]="true"></eg-combobox>
+ </div>
+ </div>
+ <div class="d-flex">
+ <div class="flex-2" i18n>Amount Encumbered:</div>
+ <div class="flex-3">{{charge.fund_debit().amount()}}</div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-success"
+ (click)="close(true)" i18n>Disencumber</button>
+ <button type="button" class="btn btn-warning"
+ (click)="close()" i18n>Cancel</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-disencumber-charge-dialog',
+ templateUrl: './disencumber-charge-dialog.component.html'
+})
+
+export class DisencumberChargeDialogComponent extends DialogComponent {
+ @Input() charge: IdlObject;
+ constructor(private modal: NgbModal) { super(modal); }
+}
+
+
-
-<!-- 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="audit_time" [datePlusTime]="true"></eg-grid-column>
<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>
import {PoNotesComponent} from './notes.component';
import {PoCreateComponent} from './create.component';
import {PoChargesComponent} from './charges.component';
-
+import {PicklistUploadService} from '../picklist/upload.service';
+import {DisencumberChargeDialogComponent} from './disencumber-charge-dialog.component';
@NgModule({
declarations: [
PoNotesComponent,
PoCreateComponent,
PoChargesComponent,
- PrintComponent
+ PrintComponent,
+ DisencumberChargeDialogComponent
],
imports: [
StaffCommonModule,
PoRoutingModule
],
providers: [
- PoService
+ PoService,
+ PicklistUploadService
]
})
import {Observable, from} from 'rxjs';
import {switchMap, map, tap, merge} from 'rxjs/operators';
import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {OrgService} from '@eg/core/org.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';
import {LineitemService, FleshCacheParams} from '@eg/staff/acq/lineitem/lineitem.service';
+export interface PoDupeCheckResults {
+ dupeFound: boolean;
+ dupePoId: number;
+}
+
@Injectable()
export class PoService {
constructor(
private evt: EventService,
private net: NetService,
+ private org: OrgService,
+ private pcrud: PcrudService,
private auth: AuthService
) {}
flesh_provider: true,
flesh_notes: true,
flesh_po_items: true,
+ flesh_po_items_further: true,
flesh_price_summary: true,
flesh_lineitem_count: true
}, params.fleshMore || {});
// Fetch the PO again (with less fleshing) and update the
// order summary totals our main fully-fleshed PO.
- refreshOrderSummary(): Promise<any> {
+ refreshOrderSummary(update_po_items = false): Promise<any> {
+ const flesh = Object.assign({
+ flesh_price_summary: true
+ });
+ if (update_po_items) {
+ flesh['flesh_po_items'] = true;
+ flesh['flesh_po_items_further'] = true;
+ }
return this.net.request('open-ils.acq',
'open-ils.acq.purchase_order.retrieve.authoritative',
this.auth.token(), this.currentPo.id(),
- {flesh_price_summary: true}
+ flesh
).toPromise().then(po => {
this.currentPo.amount_encumbered(po.amount_encumbered());
this.currentPo.amount_spent(po.amount_spent());
this.currentPo.amount_estimated(po.amount_estimated());
+ if (update_po_items) {
+ this.currentPo.po_items(po.po_items());
+ }
});
}
+
+ checkIfImportNeeded(): Promise<boolean> {
+ return new Promise((resolve, reject) => {
+ this.pcrud.search('jub',
+ { purchase_order: this.currentPo.id(), eg_bib_id: null },
+ { limit: 1 }, { idlist: true, atomic: true }
+ ).toPromise().then(ids => {
+ if (ids && ids.length) {
+ resolve(true);
+ } else {
+ resolve(false);
+ }
+ });
+ });
+ }
+
+ checkDuplicatePoName(orderAgency: number, poName: string, results: PoDupeCheckResults) {
+ if (Boolean(orderAgency) && Boolean(poName)) {
+ this.pcrud.search('acqpo',
+ { name: poName, ordering_agency: this.org.descendants(orderAgency, true) },
+ {}, { idlist: true, atomic: true }
+ ).toPromise().then(ids => {
+ if (ids && ids.length) {
+ results.dupeFound = true;
+ results.dupePoId = ids[0];
+ } else {
+ results.dupeFound = false;
+ }
+ });
+ } else {
+ results.dupeFound = false;
+ }
+ }
}
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 {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';
+import {LineitemService} from '../lineitem/lineitem.service';
+
+const DEFAULT_SORT_ORDER = 'li_id_asc';
+const SORT_ORDERS = [
+ 'li_id_asc',
+ 'li_id_desc',
+ 'title_asc',
+ 'title_desc',
+ 'author_asc',
+ 'author_desc',
+ 'publisher_asc',
+ 'publisher_desc',
+ 'order_ident_asc',
+ 'order_ident_desc'
+];
+const ORDER_IDENT_ATTRS = [
+ 'isbn',
+ 'issn',
+ 'upc'
+];
@Component({
templateUrl: 'print.component.html'
private org: OrgService,
private net: NetService,
private auth: AuthService,
+ private store: ServerStoreService,
private pcrud: PcrudService,
private poService: PoService,
+ private liService: LineitemService,
private broadcaster: BroadcastService,
private printer: PrintService) {
}
}
})
.then(po => this.po = po)
+ .then(_ => this.sortLineItems())
.then(_ => this.populatePreview())
.then(_ => this.initDone = true);
}
+ sortLineItems(): Promise<any> {
+ return this.store.getItem('acq.lineitem.sort_order').then(sortOrder => {
+ if (!sortOrder || !SORT_ORDERS.includes(sortOrder)) {
+ sortOrder = DEFAULT_SORT_ORDER;
+ }
+ const liService = this.liService;
+ function _compareLIs(a, b) {
+ const direction = sortOrder.match(/_asc$/) ? 'asc' : 'desc';
+ const field = sortOrder.replace(/_asc|_desc$/, '');
+ const a_val = liService.getLISortKey(a, field);
+ const b_val = liService.getLISortKey(b, field);
+
+ if (direction === 'asc') {
+ return liService.nullableCompare(a_val, b_val);
+ } else {
+ return -liService.nullableCompare(a_val, b_val);
+ }
+ }
+ this.po.lineitems().sort(_compareLIs);
+ });
+ }
+
populatePreview(): Promise<any> {
return this.printer.compileRemoteTemplate({
-import {NgModule} from '@angular/core';
+import {NgModule, Injectable} from '@angular/core';
import {RouterModule, Routes} from '@angular/router';
+import {Router, Resolve, RouterStateSnapshot,
+ ActivatedRouteSnapshot, CanDeactivate} from '@angular/router';
+import {Observable} from 'rxjs';
import {PoComponent} from './po.component';
import {PrintComponent} from './print.component';
import {PoSummaryComponent} from './summary.component';
import {LineitemDetailComponent} from '../lineitem/detail.component';
import {LineitemCopiesComponent} from '../lineitem/copies.component';
import {BriefRecordComponent} from '../lineitem/brief-record.component';
+import {CreateAssetsComponent} from '../lineitem/create-assets.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';
+// following example of https://www.concretepage.com/angular-2/angular-candeactivate-guard-example
+export interface PoChildDeactivationGuarded {
+ canDeactivate(): Observable<boolean> | Promise<boolean> | boolean;
+}
+
+@Injectable()
+export class CanLeavePoChildGuard implements CanDeactivate<PoChildDeactivationGuarded> {
+ canDeactivate(component: PoChildDeactivationGuarded): Observable<boolean> | Promise<boolean> | boolean {
+ return component.canDeactivate ? component.canDeactivate() : true;
+ }
+}
+
const routes: Routes = [{
path: 'create',
component: PoCreateComponent
path: 'brief-record',
component: BriefRecordComponent
}, {
+ path: 'create-assets',
+ component: CreateAssetsComponent
+ }, {
path: 'lineitem/:lineitemId/detail',
component: LineitemDetailComponent
}, {
component: LineitemHistoryComponent
}, {
path: 'lineitem/:lineitemId/items',
- component: LineitemCopiesComponent
+ component: LineitemCopiesComponent,
+ canDeactivate: [CanLeavePoChildGuard]
}, {
path: 'lineitem/:lineitemId/worksheet',
component: LineitemWorksheetComponent
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
- providers: []
+ providers: [CanLeavePoChildGuard]
})
export class PoRoutingModule {}
-<eg-acq-cancel-dialog #cancelDialog></eg-acq-cancel-dialog>
+<eg-acq-cancel-dialog recordType="po" #cancelDialog></eg-acq-cancel-dialog>
+<eg-acq-link-invoice-dialog #linkInvoiceDialog></eg-acq-link-invoice-dialog>
<eg-progress-dialog #progressDialog></eg-progress-dialog>
<eg-confirm-dialog #confirmFinalize
i18n-dialogTitle i18n-dialogBody
dialogTitle="Finalize Blanket Order?"
dialogBody="This will disencumber all blanket charges and mark the PO as received.">
</eg-confirm-dialog>
+<eg-confirm-dialog #confirmActivate
+ i18n-dialogTitle i18n-dialogBody
+ dialogTitle="Confirm Order Activation?"
+ dialogBody="Please confirm that you want to activate the order; there are warnings.">
+</eg-confirm-dialog>
+
<div *ngIf="po()" class="p-1 border border-secondary rounded">
</div>
<span *ngIf="po().state() == 'on-order'" i18n>On Order</span>
+ <span *ngIf="po().state() == 'received'" i18n>Received</span>
<ng-container *ngIf="canActivate">
<span *ngIf="!activationEvent" i18n>Pending / Activatable</span>
<span *ngIf="activationEvent" i18n>
</span>
</ng-container>
+ <!-- activation warnings -->
+ <ng-container *ngIf='activationWarnings.length'>
+ <span i18n> (Warning: </span>
+ <ng-container *ngFor="let evt of activationWarnings">
+ <ng-container
+ *ngIf="evt.textcode == 'ACQ_FUND_EXCEEDS_WARN_PERCENT'">
+ <span class="bg-warning" i18n>
+ Fund exceeds warning percent:
+ {{evt.payload.fund.code()}} ({{evt.payload.fund.year()}}).
+ </span>
+ </ng-container>
+ </ng-container>
+ <span i18n>)</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">
+ *ngIf="evt.textcode == 'ACQ_FUND_EXCEEDS_STOP_PERCENT'; else noPrice">
<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>
+ <span i18n>One or more line items have no price.</span>
</ng-container>
</ng-template>
<ng-template #noCopies>
<ng-container
*ngIf="evt.textcode == 'ACQ_LINEITEM_NO_COPIES'; else noOwner">
- <span i18n>One or more lineitems have no items attached.</span>
+ <span i18n>One or more line items have no items attached.</span>
</ng-container>
</ng-template>
<ng-template #noOwner>
<div class="flex-4">
<ng-container *ngIf="editPoName">
<input id='pl-name-input' type="text" class="form-control"
- [(ngModel)]="newPoName" (keyup.enter)="toggleNameEdit(true)"
+ [ngModel]="newPoName" (ngModelChange)="newPoName = $event; checkDuplicatePoName()"
+ (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 *ngIf="dupeResults.dupeFound" class="alert alert-warning" i18n>
+ This name is already in used by another PO:
+ <a target="_blank" routerLink="/staff/acq/po/{{dupeResults.dupePoId}}">View PO</a>
+ </div>
</div>
</div>
<div class="col-lg-8 d-flex">
<div class="row">
<div class="col-lg-4 d-flex">
- <div class="flex-2" i18n>Lineitems:</div>
+ <div class="flex-2" i18n>Line Items:</div>
<div class="flex-4">{{po().lineitem_count()}}</div>
</div>
<div class="col-lg-8 d-flex">
- <div class="form-check form-check-inline">
+ <div class="form-check form-check-inline" *ngIf="po().state() == 'new' || po().state() == 'pending'">
<input class="ml-0 form-check-input" type="checkbox" (change)="setCanActivate()"
id="zero-copy-cbox" [(ngModel)]="zeroCopyActivate"/>
<label class="form-check-label" for="zero-copy-cbox" i18n>
- Allow Activation with Zero-Copy Lineitems?
+ Allow Activation with Zero-Item Line Items?
</label>
</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>
+ <eg-bool [value]="po().prepayment_required()"></eg-bool>
</div>
</div>
</div>
<span class="material-icons small mr-1">event_note</span>
<span>Notes ({{po().notes().length}})</span>
</a>
+ <ng-container *ngIf="po().order_date()"> <!-- show invoice actions only if order was activated -->
<span class="pl-2 pr-2" i18n> | </span>
<a [queryParams]="{f: 'acqpo:id', val1: poId}" class="label-with-material-icon"
routerLink="/staff/acq/search/invoices">
<span i18n>Create Invoice</span>
</a>
<span class="pl-2 pr-2" i18n> | </span>
+ <a (click)="linkInvoiceFromPo()" href="javascript:;"
+ class="label-with-material-icon">
+ <span class="material-icons small mr-1">receipt</span>
+ <span i18n>Link Invoice</span>
+ </a>
+ </ng-container> <!-- show invoice actions -->
+ <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="material-icons small mr-1">print</span>
<span i18n>Print</span>
</a>
- <ng-container *ngIf="po().state() == 'on-order' || po().state() == 'pending'">
+ <ng-container *ngIf="po().state() == 'on-order'">
<span class="pl-2 pr-2" i18n> | </span>
- <a (click)="cancelPo()" href="javascript:;" class="label-with-material-icon">
- <span class="material-icons small mr-1">cancel</span>
- <span i18n>Cancel Order</span>
- </a>
+ <button class="btn btn-sm btn-danger" (click)="cancelPo()" i18n>Cancel Order</button>
</ng-container>
<ng-container *ngIf="canActivate === true">
<span class="pl-2 pr-2" i18n> | </span>
- <a (click)="activatePo(true)" href="javascript:;" i18n>
- Activate Without Loading Items
- </a>
+ <button class="btn btn-sm btn-primary" (click)="activatePo(true)" [disabled]="doingActivation" i18n>Activate Without Loading Items</button>
</ng-container>
<ng-container *ngIf="canActivate === true">
<span class="pl-2 pr-2" i18n> | </span>
- <a (click)="activatePo()" href="javascript:;" class="label-with-material-icon">
- <span class="material-icons small mr-1">launch</span>
- <span i18n>Activate Order</span>
- </a>
+ <button class="btn btn-sm btn-success" (click)="activatePo()" [disabled]="doingActivation" i18n>Activate Order</button>
</ng-container>
<ng-container *ngIf="canFinalize">
<span class="pl-2 pr-2" i18n> | </span>
<span i18n>Finalize Blanket Order</span>
</a>
</ng-container>
+ <ng-container *ngIf="showLegacyLinks">
+ <span class="pl-2 pr-2" i18n> | </span>
+ <a href="/eg/staff/acq/legacy/po/view/{{poId}}" target="_blank">
+ Show PO in Legacy Interface
+ </a>
+ </ng-container>
</div>
</div>
import {PoService} from './po.service';
import {LineitemService} from '../lineitem/lineitem.service';
import {CancelDialogComponent} from '../lineitem/cancel-dialog.component';
+import {LinkInvoiceDialogComponent} from '../lineitem/link-invoice-dialog.component';
+const PO_ACTIVATION_WARNINGS = [
+ 'ACQ_FUND_EXCEEDS_WARN_PERCENT'
+];
@Component({
templateUrl: 'summary.component.html',
newPoName: string;
editPoName = false;
+ dupeResults = {
+ dupeFound: false,
+ dupePoId: -1
+ };
initDone = false;
ediMessageCount = 0;
invoiceCount = 0;
zeroCopyActivate = false;
canActivate: boolean = null;
canFinalize = false;
+ showLegacyLinks = false;
+ doingActivation = false;
+ finishPoActivation = false;
activationBlocks: EgEvent[] = [];
+ activationWarnings: EgEvent[] = [];
activationEvent: EgEvent;
nameEditEnterToggled = false;
stateChangeSub: Subscription;
@ViewChild('cancelDialog') cancelDialog: CancelDialogComponent;
+ @ViewChild('linkInvoiceDialog') linkInvoiceDialog: LinkInvoiceDialogComponent;
@ViewChild('progressDialog') progressDialog: ProgressDialogComponent;
@ViewChild('confirmFinalize') confirmFinalize: ConfirmDialogComponent;
+ @ViewChild('confirmActivate') confirmActivate: ConfirmDialogComponent;
constructor(
private router: Router,
return this.poService.currentPo;
}
- load(): Promise<any> {
+ load(useCache: boolean = true): Promise<any> {
if (!this.poId) { return Promise.resolve(); }
- return this.poService.getFleshedPo(this.poId, {fromCache: true, toCache: true})
+ this.dupeResults.dupeFound = false;
+ this.dupeResults.dupePoId = -1;
+
+ if (history.state.finishPoActivation) {
+ this.doingActivation = true;
+ useCache = false;
+ }
+
+ return this.poService.getFleshedPo(this.poId, {fromCache: useCache, toCache: true})
.then(po => {
// EDI message count
})
.then(_ => this.setCanActivate())
- .then(_ => this.setCanFinalize());
+ .then(_ => this.setCanFinalize())
+ .then(_ => this.loadUiPrefs())
+ .then(_ => this.activatePoIfRequested());
}
// 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) {
+
+ // don't allow change if new name is currently
+ // a duplicate
+ if (this.dupeResults.dupeFound) {
+ return;
+ }
+
if (fromEnter) {
this.nameEditEnterToggled = true;
} else {
if (node) { node.select(); }
});
- } else if (this.newPoName && this.newPoName !== this.po().name()) {
+ } else if (this.newPoName && this.newPoName !== this.po().name() &&
+ !this.dupeResults.dupeFound) {
const prevName = this.po().name();
this.po().name(this.newPoName);
this.newPoName = null;
+ this.dupeResults.dupeFound = false;
this.pcrud.update(this.po()).subscribe(resp => {
const evt = this.evt.parse(resp);
}
}
+ checkDuplicatePoName() {
+ this.poService.checkDuplicatePoName(
+ this.po().ordering_agency(), this.newPoName, this.dupeResults
+ );
+ }
+
cancelPo() {
this.cancelDialog.open().subscribe(reason => {
if (!reason) { return; }
this.net.request('open-ils.acq',
'open-ils.acq.purchase_order.cancel',
this.auth.token(), this.poId, reason
- ).subscribe(ok => {
+ ).subscribe(resp => {
this.progressDialog.close();
- location.href = location.href;
+
+ const evt = this.evt.parse(resp);
+ if (evt) {
+ alert(evt);
+ } else {
+ location.href = location.href;
+ }
});
});
}
+ linkInvoiceFromPo() {
+
+ this.linkInvoiceDialog.poId = this.poId;
+ this.linkInvoiceDialog.open().subscribe(invId => {
+ if (!invId) { return; }
+
+ const path = '/eg/staff/acq/legacy/invoice/view/' + invId + '?' +
+ 'attach_po=' + this.poId;
+ window.location.href = path;
+ });
+
+ }
+
setCanActivate() {
this.canActivate = null;
this.activationBlocks = [];
+ this.activationWarnings = [];
if (!(this.po().state().match(/new|pending/))) {
this.canActivate = false;
zero_copy_activate: this.zeroCopyActivate
};
- this.net.request('open-ils.acq',
+ return this.net.request('open-ils.acq',
'open-ils.acq.purchase_order.activate.dry_run',
this.auth.token(), this.poId, null, options
).pipe(tap(resp => {
const evt = this.evt.parse(resp);
- if (evt) { this.activationBlocks.push(evt); }
+ if (evt) {
+ if (PO_ACTIVATION_WARNINGS.includes(evt.textcode)) {
+ this.activationWarnings.push(evt);
+ } else {
+ this.activationBlocks.push(evt);
+ }
+ }
})).toPromise().then(_ => {
}
activatePo(noAssets?: boolean) {
+ this.doingActivation = true;
+ if (this.activationWarnings.length) {
+ this.confirmActivate.open().subscribe(confirmed => {
+ if (!confirmed) {
+ this.doingActivation = true;
+ return;
+ }
+
+ this._activatePo(noAssets);
+ });
+ } else {
+ this._activatePo(noAssets);
+ }
+ }
+
+ _activatePo(noAssets?: boolean) {
+ if (noAssets) {
+ // Bypass any Vandelay choices and force-load all records.
+ const vandelay = {
+ import_no_match: true,
+ queue_name: `ACQ ${new Date().toISOString()}`
+ };
+
+ const options = {
+ zero_copy_activate: this.zeroCopyActivate,
+ no_assets: noAssets
+ };
+
+ this._doActualActivate(vandelay, options);
+ } else {
+ this.poService.checkIfImportNeeded().then(importNeeded => {
+ if (importNeeded) {
+ this.router.navigate(
+ ['/staff/acq/po/' + this.po().id() + '/create-assets'],
+ { state: { activatePo: true } }
+ );
+ } else {
+ // LIs are linked to bibs, so charge forward and activate with no options set
+ this._doActualActivate({}, {});
+ }
+ });
+ }
+ }
+
+ _doActualActivate(vandelay: any, options: any) {
this.activationEvent = null;
this.progressDialog.open();
this.progressDialog.update({max: this.po().lineitem_count() * 3});
- // Bypass any Vandelay choices and force-load all records.
- // TODO: Add intermediate Vandelay options.
- const vandelay = {
- import_no_match: true,
- queue_name: `ACQ ${new Date().toISOString()}`
- };
-
- const options = {
- zero_copy_activate: this.zeroCopyActivate,
- no_assets: noAssets
- };
-
- this.net.request(
+ this.net.request(
'open-ils.acq',
'open-ils.acq.purchase_order.activate',
this.auth.token(), this.poId, vandelay, options
if (Number(resp) === 1) {
this.progressDialog.close();
// Refresh everything.
- location.href = location.href;
+ this.initDone = false;
+ this.doingActivation = false;
+ this.load(false).then(_ => {
+ this.initDone = true;
+ this.liService.clearLiCache();
+ this.router.navigate([]);
+ });
} else {
this.progressDialog.update(
.subscribe(_ => this.canFinalize = true);
}
+ loadUiPrefs() {
+ return this.store.getItemBatch(['ui.staff.acq.show_deprecated_links'])
+ .then(settings => {
+ this.showLegacyLinks = settings['ui.staff.acq.show_deprecated_links'];
+ });
+ }
+
+ activatePoIfRequested() {
+ if (this.canActivate && history.state.finishPoActivation) {
+ this.activatePo(false);
+ }
+ }
+
finalizePo() {
this.confirmFinalize.open().subscribe(confirmed => {
<ng-template #nameTmpl let-purchaseorder="row">
- <a href="/eg/staff/acq/legacy/po/view/{{purchaseorder.id()}}"
+ <a routerLink="/staff/acq/po/{{purchaseorder.id()}}"
target="_blank">
{{purchaseorder.name()}}
</a>
[ngModelOptions]="{standalone: true}" [(ngModel)]="runImmediately"/>
<label for="retrieve-immediately" class="form-check-label" i18n>Retrieve Results Immediately</label>
</div>
- <div class="col-xs-3 pl-2" *ngIf="showExpAngOptions()">
- <div class="form-check form-check-inline">
- <input class="form-check-input" type="checkbox"
- name="show-exp-ang-links" id="show-exp-ang-links"
- (change)="toggleExpSearchLinks()" [ngModel]="showExpAngLinks()"/>
- <label class="form-check-label" for="show-exp-ang-links" i18n>
- Activate Experimental Links
- </label>
- </div>
- </div>
</div>
</form>
</div>
import {PcrudService} from '@eg/core/pcrud.service';
import {StringComponent} from '@eg/share/string/string.component';
import {ToastService} from '@eg/share/toast/toast.service';
-import {AcqSearchService, AcqSearchTerm, AcqSearch} from './acq-search.service';
+import {AcqSearchTerm, AcqSearch} from './acq-search.service';
import {ServerStoreService} from '@eg/core/server-store.service';
@Component({
private pcrud: PcrudService,
private store: ServerStoreService,
private idl: IdlService,
- private toast: ToastService,
- private acqSearch: AcqSearchService
+ private toast: ToastService
) {}
ngOnInit() {
saveRunImmediately() {
return this.store.setItem(this.runImmediatelySetting, this.runImmediately);
}
-
- showExpAngOptions(): boolean {
- return this.acqSearch.angSelectionEnabled;
- }
-
- showExpAngLinks(): boolean {
- return this.acqSearch.angSearchLinksEnabled;
- }
-
- toggleExpSearchLinks() {
- this.acqSearch.angSearchLinksEnabled = !this.acqSearch.angSearchLinksEnabled;
- this.store.setItem('ui.staff.angular_acq_search.enabled',
- this.acqSearch.angSearchLinksEnabled);
- }
}
import {PicklistDeleteDialogComponent} from './picklist-delete-dialog.component';
import {PicklistMergeDialogComponent} from './picklist-merge-dialog.component';
import {AcqSearchService} from './acq-search.service';
+import {LineitemModule} from '@eg/staff/acq/lineitem/lineitem.module';
@NgModule({
declarations: [
],
imports: [
StaffCommonModule,
- AcqSearchRoutingModule
+ AcqSearchRoutingModule,
+ LineitemModule
],
providers: [AcqSearchService]
})
import {IdlObject} from '@eg/core/idl.service';
import {EventService} from '@eg/core/event.service';
import {AttrDefsService} from './attr-defs.service';
-import {ServerStoreService} from '@eg/core/server-store.service';
const baseIdlClass = {
lineitem: 'jub',
_conjunction = 'all';
firstRun = true;
- angSelectionEnabled = false;
- angSearchLinksEnabled = false;
-
constructor(
private net: NetService,
private evt: EventService,
private auth: AuthService,
private pcrud: PcrudService,
- private serverStore: ServerStoreService,
private attrDefs: AttrDefsService
) {
this.firstRun = true;
return gridSource;
}
- loadUiPrefs(): Promise<any> {
- return this.serverStore.getItemBatch([
- 'ui.staff.angular_acq_selection.enabled',
- 'ui.staff.angular_acq_search.enabled'
- ]).then(sets => {
- this.angSelectionEnabled = sets['ui.staff.angular_acq_selection.enabled'];
- this.angSearchLinksEnabled =
- sets['ui.staff.angular_acq_search.enabled'] && this.angSelectionEnabled;
- });
- }
}
i18n-searchTypeLabel searchTypeLabel="Line Item" runImmediatelySetting="eg.acq.search.lineitems.run_immediately"
defaultSearchSetting="eg.acq.search.default.lineitems"></eg-acq-search-form>
+<eg-acq-export-attributes-dialog #exportAttributesDialog></eg-acq-export-attributes-dialog>
+<eg-acq-claim-policy-dialog #claimPolicyDialog></eg-acq-claim-policy-dialog>
+<eg-acq-cancel-dialog #cancelDialog></eg-acq-cancel-dialog>
+<eg-acq-add-to-po-dialog #addToPoDialog></eg-acq-add-to-po-dialog>
+<eg-acq-delete-lineitems-dialog #deleteLineitemsDialog></eg-acq-delete-lineitems-dialog>
+<eg-acq-link-invoice-dialog #linkInvoiceDialog></eg-acq-link-invoice-dialog>
+<eg-lineitem-alert-dialog #confirmAlertsDialog></eg-lineitem-alert-dialog>
+
+<eg-string #claimPolicyAppliedString i18n-text text="Claim Policy Applied to Selected Line Item(s)"></eg-string>
+<eg-string #lineItemsReceivedString i18n-text text="Line Item(s) Received"></eg-string>
+<eg-string #lineItemsUnReceivedString i18n-text text="Line Item(s) Un-Received"></eg-string>
+<eg-string #lineItemsCancelledString i18n-text text="Line Item(s) Canceled"></eg-string>
+<eg-string #lineItemsDeletedString i18n-text text="Line Item(s) Deleted"></eg-string>
+<eg-string #lineItemsUpdatedString i18n-text text="Line Item(s) Updated"></eg-string>
+<eg-string #lineItemsAddedToPoString i18n-text text="Line Item(s) Added to Purchase Order"></eg-string>
+
+<eg-alert-dialog #noActionableLIs i18n-dialogBody
+ dialogBody="None of the selected line items are suitable for the action.">
+</eg-alert-dialog>
+
+<eg-confirm-dialog #selectorReadyConfirmDialog
+ i18n-dialogTitle i18n-dialogBody
+ dialogTitle="Confirm Line Item Change"
+ dialogBody="Mark selected line item(s) as ready for selector?">
+</eg-confirm-dialog>
+<eg-confirm-dialog #orderReadyConfirmDialog
+ i18n-dialogTitle i18n-dialogBody
+ dialogTitle="Confirm Line Item Change"
+ dialogBody="Mark selected line item(s) as ready for order?">
+</eg-confirm-dialog>
+
<ng-template #idTmpl let-lineitem="row">
- <ng-container *ngIf="showExpAngLinks(); else legacyId">
+ <ng-container>
<a *ngIf="lineitem.purchase_order()"
routerLink="/staff/acq/po/{{lineitem.purchase_order().id()}}"
fragment="{{lineitem.id()}}" target="_blank">
</a>
</ng-container>
- <ng-template #legacyId>
- <a *ngIf="lineitem.purchase_order()"
- href="/eg/staff/acq/legacy/po/view/{{lineitem.purchase_order().id()}}?focus_li={{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">
- {{lineitem.id()}}
- </a>
- </ng-template>
</ng-template>
<ng-template #poTmpl let-lineitem="row">
- <ng-container *ngIf="showExpAngLinks(); else legacyPo">
+ <ng-container>
<a *ngIf="lineitem.purchase_order()"
routerLink="/staff/acq/po/{{lineitem.purchase_order().id()}}"
fragment="{{lineitem.id()}}" target="_blank">
{{lineitem.purchase_order().name()}}
</a>
</ng-container>
- <ng-template #legacyPo>
- <a *ngIf="lineitem.purchase_order()"
- href="/eg/staff/acq/legacy/po/view/{{lineitem.purchase_order().id()}}?focus_li={{lineitem.id()}}"
- target="_blank">
- {{lineitem.purchase_order().name()}}
- </a>
- </ng-template>
</ng-template>
<ng-template #plTmpl let-lineitem="row">
- <ng-container *ngIf="showExpAngLinks(); else legacyPl">
+ <ng-container>
<a *ngIf="lineitem.picklist()"
routerLink="/staff/acq/picklist/{{lineitem.picklist().id()}}"
fragment="{{lineitem.id()}}" target="_blank">
{{lineitem.picklist().name()}}
</a>
</ng-container>
-
- <ng-template #legacyPl>
- <a *ngIf="lineitem.picklist()"
- href="/eg/staff/acq/legacy/picklist/view/{{lineitem.picklist().id()}}?focus_li={{lineitem.id()}}"
- target="_blank">
- {{lineitem.picklist().name()}}
- </a>
- </ng-template>
</ng-template>
<ng-template #liAttrTmpl let-lineitem="row" let-col="col">
<a routerLink="/staff/catalog/record/{{lineitem.eg_bib_id()}}"
target="_blank" i18n>Catalog</a></li>
<li>
- <ng-container *ngIf="showExpAngLinks(); else legacyWs">
- <a routerLink="/staff/acq/lineitem/{{lineitem.id()}}/worksheet"
+ <ng-container *ngIf="lineitem.purchase_order()">
+ <a routerLink="/staff/acq/po/{{lineitem.purchase_order().id()}}/lineitem/{{lineitem.id()}}/worksheet"
target="_blank" i18n>Worksheet</a>
</ng-container>
- <ng-template #legacyWs>
- <a href="/eg/staff/acq/legacy/lineitem/worksheet/{{lineitem.id()}}"
- target="_blank" i18n>Worksheet</a>
- </ng-template>
</li>
<li *ngIf="lineitem.purchase_order()">
- <ng-container *ngIf="showExpAngLinks(); else legacyPo2">
+ <ng-container>
<a routerLink="/staff/acq/po/{{lineitem.purchase_order().id()}}"
target="_blank" i18n>Purchase Order</a>
</ng-container>
- <ng-template #legacyPo2>
- <a href="/eg/staff/acq/legacy/po/view/{{lineitem.purchase_order().id()}}"
- target="_blank" i18n>Purchase Order</a>
- </ng-template>
</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()">
- <ng-container *ngIf="showExpAngLinks(); else legacyPl2">
+ <ng-container>
<a routerLink="/staff/acq/picklist/{{lineitem.picklist().id()}}"
target="_blank" i18n>Selection List</a>
</ng-container>
- <ng-template #legacyPl2>
- <a href="/eg/staff/acq/legacy/picklist/view/{{lineitem.picklist().id()}}"
- target="_blank" i18n>Selection List</a>
- </ng-template>
</li>
</ul>
</ng-template>
(onRowActivate)="showRow($event)"
[showDeclaredFieldsOnly]="true">
+ <eg-grid-toolbar-action label="Mark Ready for Selector" i18n-label
+ (onClick)="markSelectorReady($event)" [disableOnRows]="noSelectedRows">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Mark Ready for Order" i18n-label
+ (onClick)="markOrderReady($event)" [disableOnRows]="noSelectedRows">
+ </eg-grid-toolbar-action>
+<!-- TODO implement this when the SL interface is more fleshed out
+ <eg-grid-toolbar-action label="Move to Selection List" i18n-label
+ (onClick)="moveToSelectionList($event)" [disableOnRows]="noSelectedRows">
+ </eg-grid-toolbar-action>
+-->
+ <eg-grid-toolbar-action label="Create Purchase Order from Selected Line Items" i18n-label
+ (onClick)="createPurchaseOrder($event)" [disableOnRows]="noSelectedRows">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Add Selected to Purchase Order" i18n-label
+ (onClick)="addSelectedToPurchaseOrder($event)" [disableOnRows]="noSelectedRows">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Apply Claim Policy" i18n-label
+ (onClick)="applyClaimPolicy($event)" [disableOnRows]="noSelectedRows">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Mark Selected Line Items as Received" i18n-label
+ (onClick)="markReceived($event)" [disableOnRows]="noSelectedRows">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Un-receive Selected Line Items" i18n-label
+ (onClick)="markUnReceived($event)" [disableOnRows]="noSelectedRows">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Create Invoice from Selected Line Items" i18n-label
+ (onClick)="createInvoiceFromSelected($event)" [disableOnRows]="noSelectedRows">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Link Selected Line Items to Invoice" i18n-label
+ (onClick)="linkInvoiceFromSelected($event)" [disableOnRows]="noSelectedRows">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Cancel Selected" i18n-label
+ (onClick)="cancelLineitems($event)" [disableOnRows]="noSelectedRows">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Delete Selected" i18n-label
+ (onClick)="deleteLineitems($event)" [disableOnRows]="noSelectedRows">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Export Single Attribute List" i18n-label
+ (onClick)="exportSingleAttributeList($event)" [disableOnRows]="noSelectedRows">
+ </eg-grid-toolbar-action>
+
<eg-grid-column path="id" [cellTemplate]="idTmpl" [disableTooltip]="true"></eg-grid-column>
<eg-grid-column i18n-label label="Title" path="title" [cellTemplate]="liAttrTmpl"></eg-grid-column>
<eg-grid-column i18n-label label="Author" path="author" [cellTemplate]="liAttrTmpl"></eg-grid-column>
import {Component, OnInit, Input, ViewChild} from '@angular/core';
-import {Observable} from 'rxjs';
-import {map} from 'rxjs/operators';
+import {Observable, from, of} from 'rxjs';
+import {map, concatMap} from 'rxjs/operators';
import {Router, ActivatedRoute, ParamMap} from '@angular/router';
import {Pager} from '@eg/share/util/pager';
import {IdlObject} from '@eg/core/idl.service';
import {GridComponent} from '@eg/share/grid/grid.component';
import {GridDataSource, GridCellTextGenerator} from '@eg/share/grid/grid';
import {AcqSearchService, AcqSearchTerm, AcqSearch} from './acq-search.service';
+import {LineitemService} from '../lineitem/lineitem.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {ExportAttributesDialogComponent} from '../lineitem/export-attributes-dialog.component';
import {AcqSearchFormComponent} from './acq-search-form.component';
+import {StringComponent} from '@eg/share/string/string.component';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {ClaimPolicyDialogComponent} from '../lineitem/claim-policy-dialog.component';
+import {CancelDialogComponent} from '../lineitem/cancel-dialog.component';
+import {AddToPoDialogComponent} from '../lineitem/add-to-po-dialog.component';
+import {DeleteLineitemsDialogComponent} from '../lineitem/delete-lineitems-dialog.component';
+import {LinkInvoiceDialogComponent} from '../lineitem/link-invoice-dialog.component';
+import {LineitemAlertDialogComponent} from '../lineitem/lineitem-alert-dialog.component';
@Component({
selector: 'eg-lineitem-results',
gridSource: GridDataSource;
@ViewChild('acqSearchForm', { static: true}) acqSearchForm: AcqSearchFormComponent;
@ViewChild('acqSearchLineitemsGrid', { static: true }) lineitemResultsGrid: GridComponent;
+ @ViewChild('exportAttributesDialog') exportAttributesDialog: ExportAttributesDialogComponent;
+ @ViewChild('claimPolicyDialog') claimPolicyDialog: ClaimPolicyDialogComponent;
+ @ViewChild('cancelDialog') cancelDialog: CancelDialogComponent;
+ @ViewChild('addToPoDialog') addToPoDialog: AddToPoDialogComponent;
+ @ViewChild('deleteLineitemsDialog') deleteLineitemsDialog: DeleteLineitemsDialogComponent;
+ @ViewChild('linkInvoiceDialog') linkInvoiceDialog: LinkInvoiceDialogComponent;
+ @ViewChild('claimPolicyAppliedString', { static: false }) claimPolicyAppliedString: StringComponent;
+ @ViewChild('lineItemsReceivedString', { static: false }) lineItemsReceivedString: StringComponent;
+ @ViewChild('lineItemsUnReceivedString', { static: false }) lineItemsUnReceivedString: StringComponent;
+ @ViewChild('lineItemsCancelledString', { static: false }) lineItemsCancelledString: StringComponent;
+ @ViewChild('lineItemsAddedToPoString', { static: false }) lineItemsAddedToPoString: StringComponent;
+ @ViewChild('lineItemsDeletedString', { static: false }) lineItemsDeletedString: StringComponent;
+ @ViewChild('lineItemsUpdatedString', { static: false }) lineItemsUpdatedString: StringComponent;
+ @ViewChild('noActionableLIs', { static: true }) private noActionableLIs: AlertDialogComponent;
+ @ViewChild('selectorReadyConfirmDialog', { static: true }) selectorReadyConfirmDialog: ConfirmDialogComponent;
+ @ViewChild('orderReadyConfirmDialog', { static: true }) orderReadyConfirmDialog: ConfirmDialogComponent;
+ @ViewChild('confirmAlertsDialog') confirmAlertsDialog: LineitemAlertDialogComponent;
+
+ noSelectedRows: (rows: IdlObject[]) => boolean;
cellTextGenerator: GridCellTextGenerator;
private route: ActivatedRoute,
private net: NetService,
private auth: AuthService,
+ private toast: ToastService,
+ private liService: LineitemService,
private acqSearch: AcqSearchService) {
}
ngOnInit() {
this.gridSource = this.acqSearch.getAcqSearchDataSource('lineitem');
+ this.noSelectedRows = (rows: IdlObject[]) => (rows.length === 0);
this.cellTextGenerator = {
id: row => row.id(),
title: row => {
}
showRow(row: any) {
- window.open('/eg/staff/acq/legacy/lineitem/worksheet/' + row.id(), '_blank');
+ window.open('/eg2/staff/acq/po/' + row.purchase_order().id() +
+ '/lineitem/' + row.id() + '/worksheet', '_blank');
+ }
+
+ addSelectedToPurchaseOrder(rows: IdlObject[]) {
+ // must not be already attached to a PO
+ // and be in a pre-order state
+ const lis = rows.filter(
+ l => !l.purchase_order() &&
+ ['new', 'selector-ready', 'order-ready', 'approved'].includes(l.state())
+ );
+ if (lis.length === 0) {
+ this.noActionableLIs.open();
+ return;
+ }
+ const ids = lis.map(x => Number(x.id()));
+
+ this.addToPoDialog.ids = ids;
+ this.addToPoDialog.open().subscribe(poId => {
+ this.net.request('open-ils.acq',
+ 'open-ils.acq.purchase_order.add_lineitem',
+ this.auth.token(), poId, ids
+ ).toPromise().then(resp => {
+ window.open('/eg2/staff/acq/po/' + poId, '_blank');
+ this.lineItemsAddedToPoString.current()
+ .then(str => this.toast.success(str));
+ this.lineitemResultsGrid.reload();
+ });
+ });
+ }
+
+ applyClaimPolicy(rows: IdlObject[]) {
+ // must be attached to a PO; while this is not
+ // strictly necessary, seems to make sense that
+ // a claim policy is relevant only once you know
+ // who the vendor is
+ const lis = rows.filter(l => l.purchase_order());
+ if (lis.length === 0) {
+ this.noActionableLIs.open();
+ return;
+ }
+ const ids = lis.map(x => Number(x.id()));
+
+ this.claimPolicyDialog.ids = ids;
+ this.claimPolicyDialog.open().subscribe(claimPolicy => {
+ if (!claimPolicy) { return; }
+
+ const lisToUpdate: IdlObject[] = [];
+ this.liService.getFleshedLineitems(ids, { fromCache: true }).subscribe(
+ liStruct => {
+ liStruct.lineitem.claim_policy(claimPolicy);
+ lisToUpdate.push(liStruct.lineitem);
+ },
+ err => { },
+ () => {
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.lineitem.update',
+ this.auth.token(), lisToUpdate
+ ).toPromise().then(resp => {
+ this.claimPolicyAppliedString.current()
+ .then(str => this.toast.success(str));
+ });
+ }
+ );
+ });
+ }
+
+ cancelLineitems(rows: IdlObject[]) {
+ // must be attached to a PO and have a state of
+ // either 'on-order' or 'cancelled'
+ const lis = rows.filter(l =>
+ l.purchase_order() && ['on-order', 'cancelled'].includes(l.state())
+ );
+ if (lis.length === 0) {
+ this.noActionableLIs.open();
+ return;
+ }
+ const ids = lis.map(x => Number(x.id()));
+ this.cancelDialog.open().subscribe(reason => {
+ if (!reason) { return; }
+
+ this.net.request('open-ils.acq',
+ 'open-ils.acq.lineitem.cancel.batch',
+ this.auth.token(), ids, reason
+ ).toPromise().then(resp => {
+ this.lineItemsCancelledString.current()
+ .then(str => this.toast.success(str));
+ this.lineitemResultsGrid.reload();
+ });
+ });
+ }
+
+ createInvoiceFromSelected(rows: IdlObject[]) {
+ // must be attached to PO
+ const lis = rows.filter(l => l.purchase_order());
+ if (lis.length === 0) {
+ this.noActionableLIs.open();
+ return;
+ }
+ const path = '/eg/staff/acq/legacy/invoice/view?create=1&' +
+ lis.map(x => 'attach_li=' + x.id()).join('&');
+ window.location.href = path;
+ }
+
+ createPurchaseOrder(rows: IdlObject[]) {
+ // must not be already attached to a PO
+ const lis = rows.filter(l => !l.purchase_order());
+ if (lis.length === 0) {
+ this.noActionableLIs.open();
+ return;
+ }
+ const ids = lis.map(x => Number(x.id()));
+ this.router.navigate(['/staff/acq/po/create'], {
+ queryParams: {li: ids}
+ });
+ }
+
+ deleteLineitems(rows: IdlObject[]) {
+ const lis = rows.filter(l =>
+ l.picklist() || (
+ l.purchase_order() &&
+ ['new', 'selector-ready', 'order-ready', 'approved', 'pending-order'].includes(l.state())
+ )
+ );
+ // TODO - if the LI somehow has a claim attached to it, lineitem.delete
+ // current crashes
+ if (lis.length === 0) {
+ this.noActionableLIs.open();
+ return;
+ }
+ const ids = lis.map(x => Number(x.id()));
+ this.deleteLineitemsDialog.ids = ids;
+ this.deleteLineitemsDialog.open().subscribe(doIt => {
+ if (!doIt) { return; }
+
+ from(lis)
+ .pipe(concatMap(li => {
+ const method = li.purchase_order() ?
+ 'open-ils.acq.purchase_order.lineitem.delete' :
+ 'open-ils.acq.picklist.lineitem.delete';
+
+ return this.net.request('open-ils.acq', method, this.auth.token(), li.id());
+ // TODO: cap parallelism
+ }))
+ .pipe(concatMap(_ => of(true) ))
+ .subscribe(r => {}, err => {}, () => {
+ this.lineItemsDeletedString.current()
+ .then(str => this.toast.success(str));
+ this.lineitemResultsGrid.reload();
+ });
+ });
+ }
+
+ exportSingleAttributeList(rows: IdlObject[]) {
+ const ids = rows.map(x => Number(x.id()));
+ this.exportAttributesDialog.ids = ids;
+ this.exportAttributesDialog.open().subscribe(attr => {
+ if (!attr) { return; }
+
+ this.liService.doExportSingleAttributeList(ids, attr);
+ });
+ }
+
+ linkInvoiceFromSelected(rows: IdlObject[]) {
+ // must be attached to PO
+ const lis = rows.filter(l => l.purchase_order());
+ if (lis.length === 0) {
+ this.noActionableLIs.open();
+ return;
+ }
+
+ this.linkInvoiceDialog.liIds = lis.map(i => Number(i.id()));
+ this.linkInvoiceDialog.open().subscribe(invId => {
+ if (!invId) { return; }
+
+ const path = '/eg/staff/acq/legacy/invoice/view/' + invId + '?' +
+ lis.map(x => 'attach_li=' + x.id()).join('&');
+ window.location.href = path;
+ });
+ }
+
+ markOrderReady(rows: IdlObject[]) {
+ const lis = rows.filter(l => l.state() === 'selector-ready' || l.state() === 'new');
+ if (lis.length === 0) {
+ this.noActionableLIs.open();
+ return;
+ }
+ const ids = lis.map(x => Number(x.id()));
+
+ this.orderReadyConfirmDialog.open().subscribe(doIt => {
+ if (!doIt) { return; }
+ const lisToUpdate: IdlObject[] = [];
+ this.liService.getFleshedLineitems(ids, { fromCache: true }).subscribe(
+ liStruct => {
+ liStruct.lineitem.state('order-ready');
+ lisToUpdate.push(liStruct.lineitem);
+ },
+ err => { },
+ () => {
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.lineitem.update',
+ this.auth.token(), lisToUpdate
+ ).toPromise().then(resp => {
+ this.lineItemsUpdatedString.current()
+ .then(str => this.toast.success(str));
+ this.lineitemResultsGrid.reload();
+ });
+ }
+ );
+ });
}
- showExpAngLinks(): boolean {
- return this.acqSearch.angSearchLinksEnabled;
+ markSelectorReady(rows: IdlObject[]) {
+ const lis = rows.filter(l => l.state() === 'new');
+ if (lis.length === 0) {
+ this.noActionableLIs.open();
+ return;
+ }
+ const ids = lis.map(x => Number(x.id()));
+
+ this.selectorReadyConfirmDialog.open().subscribe(doIt => {
+ if (!doIt) { return; }
+ const lisToUpdate: IdlObject[] = [];
+ this.liService.getFleshedLineitems(ids, { fromCache: true }).subscribe(
+ liStruct => {
+ liStruct.lineitem.state('selector-ready');
+ lisToUpdate.push(liStruct.lineitem);
+ },
+ err => { },
+ () => {
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.lineitem.update',
+ this.auth.token(), lisToUpdate
+ ).toPromise().then(resp => {
+ this.lineItemsUpdatedString.current()
+ .then(str => this.toast.success(str));
+ this.lineitemResultsGrid.reload();
+ });
+ }
+ );
+ });
+ }
+
+ markReceived(rows: IdlObject[]) {
+ // must be on-order
+ const lis = rows.filter(l => l.state() === 'on-order');
+ if (lis.length === 0) {
+ this.noActionableLIs.open();
+ return;
+ }
+
+ const ids = lis.map(x => Number(x.id()));
+
+ this.liService.checkLiAlerts(lis, this.confirmAlertsDialog).then(ok => {
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.lineitem.receive.batch',
+ this.auth.token(), ids
+ ).toPromise().then(resp => {
+ this.lineItemsReceivedString.current()
+ .then(str => this.toast.success(str));
+ this.lineitemResultsGrid.reload();
+ });
+ }, err => {}); // avoid console errors
}
+
+ markUnReceived(rows: IdlObject[]) {
+ // must be received
+ const lis = rows.filter(l => l.state() === 'received');
+ if (lis.length === 0) {
+ this.noActionableLIs.open();
+ return;
+ }
+
+ const ids = lis.map(x => Number(x.id()));
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.lineitem.receive.rollback.batch',
+ this.auth.token(), ids
+ ).toPromise().then(resp => {
+ this.lineItemsUnReceivedString.current()
+ .then(str => this.toast.success(str));
+ this.lineitemResultsGrid.reload();
+ });
+ }
+
}
</eg-string>
<ng-template #nameTmpl let-selectionlist="row">
- <ng-container *ngIf="showExpAngLinks(); else legacyLinks">
+ <ng-container>
<a routerLink="/staff/acq/picklist/{{selectionlist.id()}}" target="_blank">
{{selectionlist.name()}}
</a>
</ng-container>
- <ng-template #legacyLinks>
- <a href="/eg/staff/acq/legacy/picklist/view/{{selectionlist.id()}}"
- target="_blank">
- {{selectionlist.name()}}
- </a>
- </ng-template>
</ng-template>
<eg-picklist-create-dialog #picklistCreateDialog>
};
}
- showExpAngOptions(): boolean {
- return this.acqSearch.angSelectionEnabled;
- }
-
- showExpAngLinks(): boolean {
- return this.acqSearch.angSearchLinksEnabled;
- }
-
openCreateDialog() {
this.picklistCreateDialog.open().subscribe(
modified => {
}
showRow(row: any) {
- window.open('/eg/staff/acq/legacy/picklist/view/' + row.id(), '_blank');
+ window.open('/eg2/staff/acq/picklist/' + row.id(), '_blank');
}
doSearch(search: AcqSearch) {
defaultSearchSetting="eg.acq.search.default.purchaseorders"></eg-acq-search-form>
<ng-template #nameTmpl let-purchaseorder="row">
- <ng-container *ngIf="showExpAngLinks(); else legacyPo">
+ <ng-container>
<a routerLink="/staff/acq/po/{{purchaseorder.id()}}" target="_blank">
{{purchaseorder.name()}}
</a>
</ng-container>
- <ng-template #legacyPo>
- <a href="/eg/staff/acq/legacy/po/view/{{purchaseorder.id()}}"
- target="_blank">
- {{purchaseorder.name()}}
- </a>
- </ng-template>
</ng-template>
<ng-template #providerTmpl let-purchaseorder="row">
}
showRow(row: any) {
- window.open('/eg/staff/acq/legacy/po/view/' + row.id(), '_blank');
+ window.open('/eg2/staff/acq/po/' + row.id(), '_blank');
}
doSearch(search: AcqSearch) {
this.purchaseOrderResultsGrid.reload();
});
}
-
- showExpAngLinks(): boolean {
- return this.acqSearch.angSearchLinksEnabled;
- }
-
}
import {Router, Resolve, RouterStateSnapshot,
ActivatedRouteSnapshot} from '@angular/router';
import {AttrDefsService} from './attr-defs.service';
-import {AcqSearchService} from './acq-search.service';
@Injectable()
export class AttrDefsResolver implements Resolve<Promise<any[]>> {
constructor(
private router: Router,
- private attrDefs: AttrDefsService,
- private acqSearch: AcqSearchService
+ private attrDefs: AttrDefsService
) {}
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot): Promise<any[]> {
- return this.attrDefs.fetchAttrDefs()
- .then(_ => this.acqSearch.loadUiPrefs());
+ return Promise.all([
+ this.attrDefs.fetchAttrDefs()
+ ]);
}
}
<eg-grid-column path="origin_currency_type"></eg-grid-column>
<eg-grid-column path="create_time" [datePlusTime]="true"></eg-grid-column>
<ng-template #liTmpl let-row="row">
- <a href="/eg/staff/acq/legacy/po/view/{{row.po_id}}?focus_li={{row.li_id}}" target="_blank">
+ <a routerLink="/staff/acq/po/{{row.po_id}}" fragment="{{row.li_id}}" target="_blank">
{{row.li_id}}
</a>
</ng-template>
<eg-grid-column path="li" i18n-label label="Line Item" [cellTemplate]="liTmpl" [filterable]="false" [sortable]="false"></eg-grid-column>
<ng-template #poTmpl let-row="row">
- <a href="/eg/staff/acq/legacy/po/view/{{row.po_id}}" target="_blank">
+ <a routerLink="/staff/acq/po/{{row.po_id}}" target="_blank">
{{row.po_name}}
</a>
</ng-template>
</div>
</div>
- <div class="navbar-nav" *ngIf="showAngularAcq">
- <div ngbDropdown class="nav-item dropdown">
- <a ngbDropdownToggle i18n class="nav-link dropdown-toggle">
- Acquisitions (Experimental)
- </a>
- <div class="dropdown-menu" ngbDropdownMenu>
- <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>
- </div>
- </div>
- </div>
-
<div class="navbar-nav">
<div ngbDropdown class="nav-item dropdown">
<a ngbDropdownToggle i18n class="nav-link dropdown-toggle">
.then(settings => this.showTraditionalCatalog =
Boolean(settings['ui.staff.traditional_catalog.enabled']));
- this.org.settings('ui.staff.angular_acq_selection.enabled')
- .then(settings => this.showAngularAcq =
- Boolean(settings['ui.staff.angular_acq_selection.enabled']));
-
this.org.settings('circ.curbside')
.then(settings => this.curbsideEnabled =
Boolean(settings['circ.curbside']));
'webstaff.format.date_and_time',
'ui.staff.max_recent_patrons',
'circ.curbside', // navbar
- 'ui.staff.angular_acq_selection.enabled', // navbar
'ui.staff.angular_catalog.enabled' // navbar
]).then(settings => {
// Avoid clobbering defaults
background-color: rgb(247, 247, 247);
}
+/**
+ * Similar to the CSS above for the search form, set some
+ * CSS for the line item worksheet. Ordinarily would be
+ * preferable to just add the CSS to the worksheet component,
+ * but untl a well-supported alternative to ng-deep comes along...
+ */
+#worksheet-outlet thead th { font-weight: bold; background-color: #ccc; text-align: center; border-bottom: 1px #000 solid; border-right: 1px #000 solid; padding: 0
+6px; }
+#worksheet-outlet tbody td { text-align: left; vertical-align: top; border: 1px #999 inset; padding: 0 2px; }
+
/* style for negative monetary values */
.negative-money-amount {
color: red;
INSERT INTO config.org_unit_setting_type (name, grp, datatype, label)
VALUES (
- 'ui.staff.angular_acq_selection.enabled', 'gui', 'bool',
+ 'ui.staff.acq.show_deprecated_links', 'gui', 'bool',
oils_i18n_gettext(
- 'ui.staff.angular_acq_selection.enabled',
- 'Enable Experimental ACQ Selection/Purchase Interfaces',
+ 'ui.staff.acq.show_deprecated_links',
+ 'Display Links to Deprecated Acquisitions Interfaces',
+ 'cwst', 'label'
+ )
+);
+
+INSERT INTO config.org_unit_setting_type (name, grp, datatype, label)
+VALUES (
+ 'ui.staff.acq.show_deprecated_links', 'gui', 'bool',
+ oils_i18n_gettext(
+ 'ui.staff.acq.show_deprecated_links',
+ 'Display Links to Deprecated Acquisitions Interfaces',
'cwst', 'label'
)
);
)
);
-INSERT INTO config.org_unit_setting_type (name, grp, datatype, label)
-VALUES (
- 'ui.staff.angular_acq_selection.enabled', 'gui', 'bool',
- oils_i18n_gettext(
- 'ui.staff.angular_acq_selection.enabled',
- 'Enable Experimental ACQ Selection/Purchase Interfaces',
- 'cwst', 'label'
- )
-);
-
-
INSERT INTO config.print_template
(id, name, label, owner, active, locale, template)
VALUES (
)
);
+INSERT INTO config.org_unit_setting_type (name, grp, datatype, label)
+VALUES (
+ 'ui.staff.acq.show_deprecated_links', 'gui', 'bool',
+ oils_i18n_gettext(
+ 'ui.staff.acq.show_deprecated_links',
+ 'Display Links to Deprecated Acquisitions Interfaces',
+ 'cwst', 'label'
+ )
+);
+
COMMIT;
</ul>
</li>
- <!-- acquisitions experimental -->
- <li class="dropdown" uib-dropdown>
- <a href uib-dropdown-toggle>[% l('Acquisitions (Experimental)') %]<b class="caret"
- aria-hidden="true"></b>
- </a>
- <ul uib-dropdown-menu>
- <li>
- <a href="/eg2/staff/acq/po/create">
- <span class="glyphicon glyphicon-plus" aria-hidden="true"></span>
- [% l('Create Purchase Order') %]
- </a>
- </li>
- </ul>
- </li>
-
-
<!-- booking -->
<li class="dropdown" uib-dropdown>
<a href uib-dropdown-toggle>[% l('Booking') %]<b class="caret"
pl = null;
if(po) {
- liLink = oilsBasePath + '/acq/po/view/' + po.id() + '/' + lineitem.id();
+ liLink = '/eg2/en-US/staff/acq/po/' + po.id() + '#' + lineitem.id();
if(po.order_date()) {
var date = dojo.date.stamp.fromISOString(po.order_date());
if(date) {
"INVOICE_ITEM_DETAILS" : "${0} <br/> ${1} <br/> ${2}. <br/> Estimated Price: $${3}. <br/> Lineitem ID: ${4} <br/> PO: ${5} <br/> Order Date: ${6}",
"INVOICE_CONFIRM_ITEM_DELETE" : "Remove this $${0} '${1}' charge from the invoice?",
"INVOICE_CONFIRM_ENTRY_DETACH" : "Remove $${0} charge for item '${1}, ${2} [${3}] from the invoice?",
- "LINEITEM_SUMMARY" : "<div class='acq-lineitem-summary'><a href='${19}?focus_li=${10}&source=${22}'>${0}</a>, by ${1} (${2})</div>\n<div class='acq-lineitem-summary-extra'>\n${3} Ordered, ${4} Received, ${7} Invoiced, ${8} Claimed, ${9} Cancelled, ${23} Delayed</div>\n<div class='acq-lineitem-summary-extra'>Estimated $${6}, Encumbered $${16}, Paid $${17}</div>\n<div class='acq-lineitem-summary-extra'>\n# ${10} <a style='padding-right: 10px;' class='hidden${20}' href='${11}/acq/po/view/${12}?focus_li=${10}&source=${22}'>⌘ ${13} ${18}</a>\n<a style='padding-right: 10px;' class='hidden${21}' href='${11}/acq/picklist/view/${14}?focus_li=${10}&source=${22}'>❖ ${15}</a></div>",
+ "LINEITEM_SUMMARY" : "<div class='acq-lineitem-summary'><a target='_top' href='${19}'>${0}</a>, by ${1} (${2})</div>\n<div class='acq-lineitem-summary-extra'>\n${3} Ordered, ${4} Received, ${7} Invoiced, ${8} Claimed, ${9} Cancelled, ${23} Delayed</div>\n<div class='acq-lineitem-summary-extra'>Estimated $${6}, Encumbered $${16}, Paid $${17}</div>\n<div class='acq-lineitem-summary-extra'>\n# ${10} <a style='padding-right: 10px;' class='hidden${20}' target='_top' href='/eg2/en-US/staff/acq/po/${12}#${10}'>⌘ ${13} ${18}</a>\n<a style='padding-right: 10px;' class='hidden${21}' target='_top' href='/eg2/en-US/staff/acq/picklist/${14}#${10}'>❖ ${15}</a></div>",
"INVOICE_CONFIRM_PRORATE" : "Prorate charges?\n\nAny subsequent changes to the invoice that would affect prorated amounts should be resolved manually.",
"INVOICE_EXTRA_COPIES" : "You are attempting to invoice <b>${0}</b> more copies than originally ordered. <br/><br/>To add these items to the original order, select a fund and choose 'Add New Items' below. <br/>After saving the invoice, you may finish editing and importing the new copies from the lineitem details page.",
- "INVOICE_ITEM_PO_DETAILS" : "<b>${0}</b><br/><a href='${1}/acq/po/view/${2}'>PO #${3} ${4}</a><br/>Total Estimated Cost: $${5}",
- "INVOICE_ITEM_PO_LABEL" : "<a href='${0}/acq/po/view/${1}'>PO #${2} ${3}</a><br/>Total Estimated Cost: $${4}",
+ "INVOICE_ITEM_PO_DETAILS" : "<b>${0}</b><br/><a target='_top' href='/eg2/en-US/staff/acq/po/${2}'>PO #${3} ${4}</a><br/>Total Estimated Cost: $${5}",
+ "INVOICE_ITEM_PO_LABEL" : "<a target='_top' href='/eg2/en-US/staff/acq/po/${1}'>PO #${2} ${3}</a><br/>Total Estimated Cost: $${4}",
"UNNAMED" : "Unnamed",
"NO_FIND_INVOICE" : "Could not find that invoice.\nNote that the Invoice # field is case-sensitive.",
"LI_BATCH_UPDATE": "Line item batch update",
if (acqData) {
if (acqData.a) {
acqData = egCore.idl.toHash(acqData);
- var url = '/eg/acq/po/view/' + acqData.purchase_order + '/' + acqData.id;
+ var url = '/eg2/staff/acq/po/' + acqData.purchase_order + '#' + acqData.id;
$timeout(function () { $window.open(url, '_blank') });
hasResults = true;
}