LP#1942220: more Angularization of the Acquisitions staff interfaces
authorGalen Charlton <gmc@equinoxOLI.org>
Mon, 29 Nov 2021 16:11:02 +0000 (11:11 -0500)
committerJane Sandberg <js7389@princeton.edu>
Sun, 2 Oct 2022 15:02:50 +0000 (08:02 -0700)
This patch extends on the work done for bugs 1929741 and 1929749
to finish converting the following interfaces to Angular:

- Selection Lists
- Load MARC Order Records
- Purchase Orders
- Create Purchase Order

Note: this project was often referred to as "Angular acquisitions
sprint 4"

Mike Rylander made some contributions to this patch.

Sponsored-by: Evergreen Community Development Initiative
Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>
Signed-off-by: Ruth Frasur <rfrasur@library.in.gov>
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Jane Sandberg <js7389@princeton.edu>
85 files changed:
Open-ILS/src/eg2/src/app/staff/acq/acq-common.module.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/add-copies-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/add-copies-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/add-to-po-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/add-to-po-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/batch-copies.component.html
Open-ILS/src/eg2/src/app/staff/acq/lineitem/batch-copies.component.ts
Open-ILS/src/eg2/src/app/staff/acq/lineitem/batch-update-copies-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/batch-update-copies-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/bib-finder-dialog.component.css [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/bib-finder-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/bib-finder-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/brief-record.component.ts
Open-ILS/src/eg2/src/app/staff/acq/lineitem/cancel-dialog.component.html
Open-ILS/src/eg2/src/app/staff/acq/lineitem/cancel-dialog.component.ts
Open-ILS/src/eg2/src/app/staff/acq/lineitem/claim-policy-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/claim-policy-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/copies.component.html
Open-ILS/src/eg2/src/app/staff/acq/lineitem/copies.component.ts
Open-ILS/src/eg2/src/app/staff/acq/lineitem/copy-attrs.component.css [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/copy-attrs.component.html
Open-ILS/src/eg2/src/app/staff/acq/lineitem/copy-attrs.component.ts
Open-ILS/src/eg2/src/app/staff/acq/lineitem/create-assets.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/create-assets.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/delete-lineitems-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/delete-lineitems-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/detail.component.html
Open-ILS/src/eg2/src/app/staff/acq/lineitem/export-attributes-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/export-attributes-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/history.component.html
Open-ILS/src/eg2/src/app/staff/acq/lineitem/lineitem-alert-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/lineitem-alert-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/lineitem-list.component.css
Open-ILS/src/eg2/src/app/staff/acq/lineitem/lineitem-list.component.html
Open-ILS/src/eg2/src/app/staff/acq/lineitem/lineitem-list.component.ts
Open-ILS/src/eg2/src/app/staff/acq/lineitem/lineitem.module.ts
Open-ILS/src/eg2/src/app/staff/acq/lineitem/lineitem.service.ts
Open-ILS/src/eg2/src/app/staff/acq/lineitem/link-invoice-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/link-invoice-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/manage-claims-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/manage-claims-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/lineitem/notes.component.html
Open-ILS/src/eg2/src/app/staff/acq/lineitem/notes.component.ts
Open-ILS/src/eg2/src/app/staff/acq/lineitem/worksheet.component.ts
Open-ILS/src/eg2/src/app/staff/acq/picklist/picklist.module.ts
Open-ILS/src/eg2/src/app/staff/acq/picklist/upload.component.html
Open-ILS/src/eg2/src/app/staff/acq/picklist/upload.component.ts
Open-ILS/src/eg2/src/app/staff/acq/picklist/upload.service.ts
Open-ILS/src/eg2/src/app/staff/acq/po/charges.component.html
Open-ILS/src/eg2/src/app/staff/acq/po/charges.component.ts
Open-ILS/src/eg2/src/app/staff/acq/po/create.component.html
Open-ILS/src/eg2/src/app/staff/acq/po/create.component.ts
Open-ILS/src/eg2/src/app/staff/acq/po/disencumber-charge-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/po/disencumber-charge-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/po/history.component.html
Open-ILS/src/eg2/src/app/staff/acq/po/po.module.ts
Open-ILS/src/eg2/src/app/staff/acq/po/po.service.ts
Open-ILS/src/eg2/src/app/staff/acq/po/print.component.ts
Open-ILS/src/eg2/src/app/staff/acq/po/routing.module.ts
Open-ILS/src/eg2/src/app/staff/acq/po/summary.component.html
Open-ILS/src/eg2/src/app/staff/acq/po/summary.component.ts
Open-ILS/src/eg2/src/app/staff/acq/provider/provider-purchase-orders.component.html
Open-ILS/src/eg2/src/app/staff/acq/search/acq-search-form.component.html
Open-ILS/src/eg2/src/app/staff/acq/search/acq-search-form.component.ts
Open-ILS/src/eg2/src/app/staff/acq/search/acq-search.module.ts
Open-ILS/src/eg2/src/app/staff/acq/search/acq-search.service.ts
Open-ILS/src/eg2/src/app/staff/acq/search/lineitem-results.component.html
Open-ILS/src/eg2/src/app/staff/acq/search/lineitem-results.component.ts
Open-ILS/src/eg2/src/app/staff/acq/search/picklist-results.component.html
Open-ILS/src/eg2/src/app/staff/acq/search/picklist-results.component.ts
Open-ILS/src/eg2/src/app/staff/acq/search/purchase-order-results.component.html
Open-ILS/src/eg2/src/app/staff/acq/search/purchase-order-results.component.ts
Open-ILS/src/eg2/src/app/staff/acq/search/resolver.service.ts
Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-details-dialog.component.html
Open-ILS/src/eg2/src/app/staff/nav.component.html
Open-ILS/src/eg2/src/app/staff/nav.component.ts
Open-ILS/src/eg2/src/app/staff/resolver.service.ts
Open-ILS/src/eg2/src/styles.css
Open-ILS/src/sql/Pg/950.data.seed-values.sql
Open-ILS/src/sql/Pg/upgrade/XXXX.data.picklist-po-angular.sql
Open-ILS/src/sql/Pg/upgrade/YYYY.data.acq-sprint-4.sql
Open-ILS/src/templates/staff/navbar.tt2
Open-ILS/web/js/dojo/openils/acq/Lineitem.js
Open-ILS/web/js/dojo/openils/acq/nls/acq.js
Open-ILS/web/js/ui/default/staff/cat/item/app.js

diff --git a/Open-ILS/src/eg2/src/app/staff/acq/acq-common.module.ts b/Open-ILS/src/eg2/src/app/staff/acq/acq-common.module.ts
new file mode 100644 (file)
index 0000000..7a32eda
--- /dev/null
@@ -0,0 +1,19 @@
+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 {
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/lineitem/add-copies-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/acq/lineitem/add-copies-dialog.component.html
new file mode 100644 (file)
index 0000000..29d1b3e
--- /dev/null
@@ -0,0 +1,26 @@
+<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">&times;</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>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/lineitem/add-copies-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/lineitem/add-copies-dialog.component.ts
new file mode 100644 (file)
index 0000000..6f2237c
--- /dev/null
@@ -0,0 +1,18 @@
+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); }
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/lineitem/add-to-po-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/acq/lineitem/add-to-po-dialog.component.html
new file mode 100644 (file)
index 0000000..9567e2e
--- /dev/null
@@ -0,0 +1,32 @@
+<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">&times;</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>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/lineitem/add-to-po-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/lineitem/add-to-po-dialog.component.ts
new file mode 100644 (file)
index 0000000..5e8739d
--- /dev/null
@@ -0,0 +1,18 @@
+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); }
+}
+
+
index 8a5bb4d..a654c87 100644 (file)
@@ -1,10 +1,6 @@
 
-<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>
@@ -35,6 +32,7 @@
   
   <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>
index 5adb6e3..f312b43 100644 (file)
@@ -8,8 +8,8 @@ import {AuthService} from '@eg/core/auth.service';
 import {LineitemService} from './lineitem.service';
 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
 import {LineitemCopyAttrsComponent} from './copy-attrs.component';
-import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
 import {CancelDialogComponent} from './cancel-dialog.component';
+import {LineitemAlertDialogComponent} from './lineitem-alert-dialog.component';
 
 const BATCH_FIELDS = [
     'owning_lib',
@@ -28,12 +28,18 @@ const BATCH_FIELDS = [
 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,
@@ -43,7 +49,14 @@ export class LineitemBatchCopiesComponent implements OnInit {
         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) {
@@ -53,6 +66,7 @@ export class LineitemBatchCopiesComponent implements OnInit {
             this.lineitem.lineitem_details().forEach(copy => {
                 copy[field](val);
                 copy.ischanged(true); // isnew() takes precedence
+                this.becameDirty.emit(true);
             });
         });
     }
@@ -66,6 +80,7 @@ export class LineitemBatchCopiesComponent implements OnInit {
         } else {
             // Requires a Save Changes action.
             copy.isdeleted(true);
+            this.becameDirty.emit(true);
         }
     }
 
@@ -94,7 +109,7 @@ export class LineitemBatchCopiesComponent implements OnInit {
     }
 
     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',
@@ -111,29 +126,6 @@ export class LineitemBatchCopiesComponent implements OnInit {
         ).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();
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/lineitem/batch-update-copies-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/acq/lineitem/batch-update-copies-dialog.component.html
new file mode 100644 (file)
index 0000000..72e71b1
--- /dev/null
@@ -0,0 +1,70 @@
+<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">&times;</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>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/lineitem/batch-update-copies-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/lineitem/batch-update-copies-dialog.component.ts
new file mode 100644 (file)
index 0000000..0ca7575
--- /dev/null
@@ -0,0 +1,91 @@
+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;
+    }
+
+}
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/lineitem/bib-finder-dialog.component.css b/Open-ILS/src/eg2/src/app/staff/acq/lineitem/bib-finder-dialog.component.css
new file mode 100644 (file)
index 0000000..56fc5d3
--- /dev/null
@@ -0,0 +1,3 @@
+.bib-finder-results-row:nth-child(even) {
+  background-color: rgba(0,0,0,.03);
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/lineitem/bib-finder-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/acq/lineitem/bib-finder-dialog.component.html
new file mode 100644 (file)
index 0000000..5adebb5
--- /dev/null
@@ -0,0 +1,57 @@
+<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">&times;</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>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/lineitem/bib-finder-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/lineitem/bib-finder-dialog.component.ts
new file mode 100644 (file)
index 0000000..630400d
--- /dev/null
@@ -0,0 +1,107 @@
+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]);
+                        }
+                    });
+                }
+            );
+        });
+    }
+}
+
+
index fa82cec..3d5912e 100644 (file)
@@ -79,7 +79,11 @@ export class BriefRecordComponent implements OnInit {
 
             // 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);
index 23ae488..a8e7dd1 100644 (file)
@@ -1,15 +1,23 @@
 <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">&times;</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">
index 630f57d..98b0448 100644 (file)
@@ -10,6 +10,7 @@ import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
 })
 
 export class CancelDialogComponent extends DialogComponent {
+    @Input() recordType = 'po';
     cancelReason: number;
     constructor(private modal: NgbModal) { super(modal); }
 }
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/lineitem/claim-policy-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/acq/lineitem/claim-policy-dialog.component.html
new file mode 100644 (file)
index 0000000..c295f32
--- /dev/null
@@ -0,0 +1,29 @@
+<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">&times;</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>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/lineitem/claim-policy-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/lineitem/claim-policy-dialog.component.ts
new file mode 100644 (file)
index 0000000..f1c407b
--- /dev/null
@@ -0,0 +1,16 @@
+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); }
+}
index a29b178..22e7650 100644 (file)
@@ -1,3 +1,10 @@
+<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">
@@ -16,6 +23,7 @@
     <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>
@@ -23,7 +31,7 @@
       [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>
@@ -38,7 +46,7 @@
 
 <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>
 
 
index ac5788e..b6ae8e2 100644 (file)
@@ -1,7 +1,8 @@
 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';
@@ -11,6 +12,7 @@ import {AuthService} from '@eg/core/auth.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',
@@ -26,17 +28,30 @@ interface FormulaApplication {
 }
 
 @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;
@@ -47,6 +62,8 @@ export class LineitemCopiesComponent implements OnInit, AfterViewInit {
     // Can any changes be applied?
     liLocked = false;
 
+    @ViewChild('leaveConfirm', { static: true }) leaveConfirm: ConfirmDialogComponent;
+
     constructor(
         private route: ActivatedRoute,
         private idl: IdlService,
@@ -63,13 +80,19 @@ export class LineitemCopiesComponent implements OnInit, AfterViewInit {
         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();
     }
@@ -82,13 +105,23 @@ export class LineitemCopiesComponent implements OnInit, AfterViewInit {
             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() {
@@ -103,9 +136,11 @@ export class LineitemCopiesComponent implements OnInit, AfterViewInit {
         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) {
@@ -163,11 +198,17 @@ export class LineitemCopiesComponent implements OnInit, AfterViewInit {
         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
@@ -228,6 +269,7 @@ export class LineitemCopiesComponent implements OnInit, AfterViewInit {
 
             } else {
                 copy[field](val);
+                this.dirty = true;
             }
         });
 
@@ -248,6 +290,7 @@ export class LineitemCopiesComponent implements OnInit, AfterViewInit {
             () => this.load({toCache: true}).then(_ => {
                 this.liService.activateStateChange.emit(this.lineitem.id());
                 this.saving = false;
+                this.dirty = false;
             })
         );
     }
@@ -260,6 +303,26 @@ export class LineitemCopiesComponent implements OnInit, AfterViewInit {
             );
         });
     }
+
+    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);
+        }
+    }
 }
 
 
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/lineitem/copy-attrs.component.css b/Open-ILS/src/eg2/src/app/staff/acq/lineitem/copy-attrs.component.css
new file mode 100644 (file)
index 0000000..ca6cb05
--- /dev/null
@@ -0,0 +1,7 @@
+.fund-balance-state-stop {
+    color: #c00;
+    font-weight: bold;
+}
+.fund-balance-state-warning {
+    color: #c93;
+}
index 43f13b3..2d8ce9d 100644 (file)
     <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>
@@ -35,7 +37,9 @@
   <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>
@@ -65,7 +70,7 @@
       <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'">
@@ -87,7 +95,7 @@
             <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>
+          &nbsp;<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>
index 4c12012..f07bc3a 100644 (file)
@@ -4,6 +4,7 @@ import {Pager} from '@eg/share/util/pager';
 import {IdlObject, IdlService} from '@eg/core/idl.service';
 import {NetService} from '@eg/core/net.service';
 import {AuthService} from '@eg/core/auth.service';
+import {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';
@@ -11,14 +12,22 @@ import {ItemLocationSelectComponent} from '@eg/share/item-location-select/item-l
 
 @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;
@@ -47,6 +56,8 @@ export class LineitemCopyAttrsComponent implements OnInit {
     // 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>();
@@ -62,16 +73,22 @@ export class LineitemCopyAttrsComponent implements OnInit {
         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
@@ -95,6 +112,17 @@ export class LineitemCopyAttrsComponent implements OnInit {
 
         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) {
 
@@ -127,13 +155,31 @@ export class LineitemCopyAttrsComponent implements OnInit {
         }
     }
 
+    // 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()) {
@@ -142,8 +188,39 @@ export class LineitemCopyAttrsComponent implements OnInit {
         }
     }
 
+    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() ||
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/lineitem/create-assets.component.html b/Open-ILS/src/eg2/src/app/staff/acq/lineitem/create-assets.component.html
new file mode 100644 (file)
index 0000000..7144bc2
--- /dev/null
@@ -0,0 +1,43 @@
+
+<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>
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/lineitem/create-assets.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/lineitem/create-assets.component.ts
new file mode 100644 (file)
index 0000000..3152b0d
--- /dev/null
@@ -0,0 +1,106 @@
+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 } }
+                        );
+                    }
+                }
+            }
+        );
+    }
+}
+
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/lineitem/delete-lineitems-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/acq/lineitem/delete-lineitems-dialog.component.html
new file mode 100644 (file)
index 0000000..6b7e645
--- /dev/null
@@ -0,0 +1,28 @@
+<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">&times;</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>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/lineitem/delete-lineitems-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/lineitem/delete-lineitems-dialog.component.ts
new file mode 100644 (file)
index 0000000..21c6ece
--- /dev/null
@@ -0,0 +1,17 @@
+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); }
+}
+
+
index 5bcba98..386dfc1 100644 (file)
@@ -31,7 +31,7 @@
       <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"
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/lineitem/export-attributes-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/acq/lineitem/export-attributes-dialog.component.html
new file mode 100644 (file)
index 0000000..40456d3
--- /dev/null
@@ -0,0 +1,38 @@
+<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">&times;</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>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/lineitem/export-attributes-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/lineitem/export-attributes-dialog.component.ts
new file mode 100644 (file)
index 0000000..d6e8a66
--- /dev/null
@@ -0,0 +1,18 @@
+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); }
+}
+
+
index dda7ce5..6fc18ac 100644 (file)
@@ -1,9 +1,10 @@
-
-<!-- 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>
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/lineitem/lineitem-alert-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/acq/lineitem/lineitem-alert-dialog.component.html
new file mode 100644 (file)
index 0000000..05a02d8
--- /dev/null
@@ -0,0 +1,17 @@
+<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>
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/lineitem/lineitem-alert-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/lineitem/lineitem-alert-dialog.component.ts
new file mode 100644 (file)
index 0000000..37b480d
--- /dev/null
@@ -0,0 +1,24 @@
+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();
+    }
+}
index ac88314..7f3e605 100644 (file)
@@ -30,4 +30,4 @@ input[type="text"].form-control-sm { border-width: 1px; }
 .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; }
index c6413c1..a67193a 100644 (file)
@@ -1,6 +1,36 @@
 
 <!-- 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>
index 0e2caac..14d12e5 100644 (file)
@@ -1,17 +1,31 @@
 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'
@@ -42,6 +56,8 @@ export class LineitemListComponent implements OnInit {
 
     picklistId: number = null;
     poId: number = null;
+    poWasActivated = false;
+    poSubscription: Subscription;
     recordId: number = null; // lineitems related to a bib.
 
     loading = false;
@@ -49,6 +65,10 @@ export class LineitemListComponent implements OnInit {
     pageOfLineitems: IdlObject[] = [];
     lineitemIds: number[] = [];
 
+    saving = false;
+    progressMax = 0;
+    progressValue = 0;
+
     // Selected lineitems
     selected: {[id: number]: boolean} = {};
 
@@ -65,13 +85,35 @@ export class LineitemListComponent implements OnInit {
     // 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;
@@ -81,6 +123,19 @@ export class LineitemListComponent implements OnInit {
                       // 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,
@@ -89,12 +144,17 @@ export class LineitemListComponent implements OnInit {
         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');
@@ -130,6 +190,13 @@ export class LineitemListComponent implements OnInit {
             });
         });
 
+        this.poSubscription = this.poService.poRetrieved.subscribe(() => {
+            this.poWasActivated = this.po().order_date() ? true : false;
+        });
+    }
+
+    po(): IdlObject {
+        return this.poService.currentPo;
     }
 
     pageSizeChange(count: number) {
@@ -152,6 +219,52 @@ export class LineitemListComponent implements OnInit {
         });
     }
 
+    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) {
@@ -185,7 +298,7 @@ export class LineitemListComponent implements OnInit {
         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 } ] });
@@ -195,11 +308,38 @@ export class LineitemListComponent implements OnInit {
             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',
@@ -209,11 +349,119 @@ export class LineitemListComponent implements OnInit {
             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([], {
@@ -276,6 +524,12 @@ export class LineitemListComponent implements OnInit {
     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] =
@@ -431,33 +685,38 @@ export class LineitemListComponent implements OnInit {
     }
 
     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() {
@@ -471,16 +730,205 @@ export class LineitemListComponent implements OnInit {
     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 {
@@ -507,6 +955,26 @@ export class LineitemListComponent implements OnInit {
         );
     }
 
+    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());
     }
@@ -529,14 +997,74 @@ export class LineitemListComponent implements OnInit {
         });
     }
 
+    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[]) {
@@ -573,6 +1101,26 @@ export class LineitemListComponent implements OnInit {
         });
     }
 
+    // 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];
@@ -582,5 +1130,9 @@ export class LineitemListComponent implements OnInit {
             Boolean(this.poId)
         );
     }
+
+    lineitemDisposition(li: IdlObject): LINEITEM_DISPOSITION {
+        return this.liService.lineitemDisposition(li);
+    }
 }
 
index 888830f..89d2780 100644 (file)
@@ -5,6 +5,7 @@ import {ItemLocationSelectModule
     } 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';
@@ -15,8 +16,20 @@ import {LineitemBatchCopiesComponent} from './batch-copies.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: [
@@ -30,20 +43,42 @@ import {MarcEditModule} from '@eg/staff/share/marc-edit/marc-edit.module';
     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
   ]
 })
 
index e320294..f86de35 100644 (file)
@@ -7,10 +7,21 @@ import {AuthService} from '@eg/core/auth.service';
 import {PcrudService} from '@eg/core/pcrud.service';
 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
 import {ItemLocationService} from '@eg/share/item-location-select/item-location-select.service';
+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;
@@ -45,6 +56,13 @@ export interface FleshCacheParams {
     toCache?: boolean;
 }
 
+interface LineitemAlertData {
+    liId: number;
+    title: string;
+    alertText: IdlObject;
+    alertComment: string;
+}
+
 @Injectable()
 export class LineitemService {
 
@@ -70,6 +88,9 @@ 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,
@@ -78,6 +99,10 @@ export class LineitemService {
         private loc: ItemLocationService
     ) {}
 
+    clearLiCache() {
+        this.liCache = [];
+    }
+
     getFleshedLineitems(ids: number[],
         params: FleshCacheParams = {}): Observable<BatchLineitemStruct> {
 
@@ -92,6 +117,7 @@ export class LineitemService {
             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,
@@ -101,8 +127,10 @@ export class LineitemService {
             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(
@@ -296,6 +324,27 @@ export class LineitemService {
         ));
     }
 
+    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
@@ -371,5 +420,109 @@ export class LineitemService {
             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;
+    }
 }
 
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/lineitem/link-invoice-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/acq/lineitem/link-invoice-dialog.component.html
new file mode 100644 (file)
index 0000000..1f0c5ef
--- /dev/null
@@ -0,0 +1,57 @@
+<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">&times;</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>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/lineitem/link-invoice-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/lineitem/link-invoice-dialog.component.ts
new file mode 100644 (file)
index 0000000..83b238c
--- /dev/null
@@ -0,0 +1,20 @@
+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); }
+}
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/lineitem/manage-claims-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/acq/lineitem/manage-claims-dialog.component.html
new file mode 100644 (file)
index 0000000..4b30bd5
--- /dev/null
@@ -0,0 +1,70 @@
+<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">&times;</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>
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/lineitem/manage-claims-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/lineitem/manage-claims-dialog.component.ts
new file mode 100644 (file)
index 0000000..0cd56d5
--- /dev/null
@@ -0,0 +1,113 @@
+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'
+            });
+        });
+    }
+}
index 25f393c..c4b32c2 100644 (file)
       </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>
index 963470d..14f82e6 100644 (file)
@@ -14,6 +14,7 @@ export class LineitemNotesComponent implements OnInit, AfterViewInit {
 
     @Input() lineitem: IdlObject;
     noteText: string;
+    alertComments: string;
     vendorPublic = false;
     alertEntry: ComboboxEntry;
 
@@ -46,10 +47,11 @@ export class LineitemNotesComponent implements OnInit, AfterViewInit {
         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');
         }
 
index 21c2a13..3fabcff 100644 (file)
@@ -57,10 +57,12 @@ export class LineitemWorksheetComponent implements OnInit, AfterViewInit {
                 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)
index 91ee36c..644ce48 100644 (file)
@@ -7,13 +7,12 @@ import {PicklistRoutingModule} from './routing.module';
 import {PicklistComponent} from './picklist.component';
 import {PicklistSummaryComponent} from './summary.component';
 import {HttpClientModule} from '@angular/common/http';
-import {UploadComponent} from './upload.component';
+import {AcqCommonModule} from '../acq-common.module';
 
 @NgModule({
   declarations: [
     PicklistComponent,
-    PicklistSummaryComponent,
-    UploadComponent
+    PicklistSummaryComponent
   ],
   imports: [
     StaffCommonModule,
@@ -21,7 +20,8 @@ import {UploadComponent} from './upload.component';
     LineitemModule,
     HoldingsModule,
     PicklistRoutingModule,
-    HttpClientModule
+    HttpClientModule,
+    AcqCommonModule
   ],
   providers: []
 })
index d2f4c15..7ef7962 100644 (file)
@@ -1,10 +1,13 @@
-<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.">
@@ -13,7 +16,8 @@
 <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>
@@ -38,6 +42,7 @@
     </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>
 
@@ -69,8 +76,9 @@
     </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>
index 22ae79d..a27da98 100644 (file)
@@ -1,5 +1,6 @@
 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';
@@ -7,6 +8,7 @@ import {NetService} from '@eg/core/net.service';
 import {EventService} from '@eg/core/event.service';
 import {OrgService} from '@eg/core/org.service';
 import {AuthService} from '@eg/core/auth.service';
+import {StringComponent} from '@eg/share/string/string.component';
 import {ToastService} from '@eg/share/toast/toast.service';
 import {ComboboxComponent,
     ComboboxEntry} from '@eg/share/combobox/combobox.component';
@@ -58,10 +60,21 @@ const ORG_SETTINGS = [
 
 
 @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;
@@ -95,6 +108,9 @@ export class UploadComponent implements OnInit, AfterViewInit, OnDestroy {
 
     isUploading: boolean;
     uploadProcessing: boolean;
+    uploadError: boolean;
+    uploadErrorCode: string;
+    uploadErrorText: string;
     uploadComplete: boolean;
 
     // Generated by the server
@@ -112,9 +128,9 @@ export class UploadComponent implements OnInit, AfterViewInit, OnDestroy {
         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;
@@ -126,9 +142,17 @@ export class UploadComponent implements OnInit, AfterViewInit, OnDestroy {
         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,
@@ -137,6 +161,11 @@ export class UploadComponent implements OnInit, AfterViewInit, OnDestroy {
         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();
     }
@@ -198,12 +227,18 @@ export class UploadComponent implements OnInit, AfterViewInit, OnDestroy {
             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()
         ];
 
@@ -213,6 +248,11 @@ export class UploadComponent implements OnInit, AfterViewInit, OnDestroy {
 
     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() {
@@ -249,12 +289,6 @@ export class UploadComponent implements OnInit, AfterViewInit, OnDestroy {
                         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 => {
@@ -297,10 +331,6 @@ export class UploadComponent implements OnInit, AfterViewInit, OnDestroy {
                 this.recordType = id;
                 break;
 
-            case 'providersList':
-                this.selectedProvider = id;
-                break;
-
             case 'bibSources':
                 this.selectedBibSource = id;
                 break;
@@ -333,6 +363,9 @@ export class UploadComponent implements OnInit, AfterViewInit, OnDestroy {
     }
 
     hasNeededData(): boolean {
+        if (this.mode === 'getImportParams') {
+            return this.selectedQueue ? true : false;
+        }
         return this.selectedQueue &&
         Boolean(this.selectedFile) &&
         Boolean(this.selectedFiscalYear) &&
@@ -371,6 +404,47 @@ export class UploadComponent implements OnInit, AfterViewInit, OnDestroy {
         );
     }
 
+    // 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});
     }
@@ -464,6 +538,7 @@ export class UploadComponent implements OnInit, AfterViewInit, OnDestroy {
     processUpload():  Promise<any> {
 
         this.uploadProcessing = true;
+        this.uploadError = false;
 
         if (this.vlagent.importSelection) {
             return Promise.resolve();
@@ -472,6 +547,7 @@ export class UploadComponent implements OnInit, AfterViewInit, OnDestroy {
         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,
@@ -489,7 +565,7 @@ export class UploadComponent implements OnInit, AfterViewInit, OnDestroy {
             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
         };
@@ -504,7 +580,14 @@ export class UploadComponent implements OnInit, AfterViewInit, OnDestroy {
                 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;
@@ -526,21 +609,25 @@ export class UploadComponent implements OnInit, AfterViewInit, OnDestroy {
         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) {
@@ -562,8 +649,12 @@ export class UploadComponent implements OnInit, AfterViewInit, OnDestroy {
 
         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);
     }
@@ -571,7 +662,10 @@ export class UploadComponent implements OnInit, AfterViewInit, OnDestroy {
     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))
+        );
     }
 }
 
index 7b1f739..8e555ae 100644 (file)
@@ -25,6 +25,7 @@ export class PicklistUploadService {
     mergeProfiles: IdlObject[];
     providersList: IdlObject[];
     fiscalYears: IdlObject[];
+    defaultFiscalYear: IdlObject;
     selectionLists: IdlObject[];
     queueType: string;
     recordType: string;
@@ -79,20 +80,6 @@ export class PicklistUploadService {
         });
     }
 
-    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);
@@ -137,17 +124,34 @@ export class PicklistUploadService {
         });
     }
 
-    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;
         });
     }
 
index 4ea1891..9ecff20 100644 (file)
@@ -1,8 +1,10 @@
 
 <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>
@@ -20,7 +22,7 @@
         [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>
index 5f443c1..d38f54b 100644 (file)
@@ -1,10 +1,14 @@
-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',
@@ -13,11 +17,17 @@ import {PoService} from './po.service';
 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
     ) {}
@@ -26,11 +36,13 @@ export class PoChargesComponent implements OnInit, OnDestroy {
         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;
         });
     }
 
@@ -57,13 +69,85 @@ export class PoChargesComponent implements OnInit, OnDestroy {
         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) {
@@ -72,8 +156,14 @@ export class PoChargesComponent implements OnInit, OnDestroy {
         );
 
         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));
         }
     }
 }
index a094da6..f65caa5 100644 (file)
@@ -1,23 +1,36 @@
 <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>
@@ -28,7 +41,7 @@
       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>
index b0b3314..7655529 100644 (file)
@@ -16,6 +16,12 @@ import {PoService} from './po.service';
 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',
@@ -25,11 +31,16 @@ export class PoCreateComponent implements OnInit {
 
     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,
@@ -50,21 +61,45 @@ export class PoCreateComponent implements OnInit {
 
         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() {
@@ -80,22 +115,18 @@ export class PoCreateComponent implements OnInit {
             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()]);
+                }
             }
         });
     }
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/po/disencumber-charge-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/acq/po/disencumber-charge-dialog.component.html
new file mode 100644 (file)
index 0000000..03a6cc4
--- /dev/null
@@ -0,0 +1,49 @@
+<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">&times;</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>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/po/disencumber-charge-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/po/disencumber-charge-dialog.component.ts
new file mode 100644 (file)
index 0000000..4bd442c
--- /dev/null
@@ -0,0 +1,17 @@
+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); }
+}
+
+
index 04a9b12..4c687f1 100644 (file)
@@ -1,10 +1,8 @@
-
-<!-- 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>
index 3d27b94..11345e4 100644 (file)
@@ -14,7 +14,8 @@ import {PoEdiMessagesComponent} from './edi.component';
 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: [
@@ -25,7 +26,8 @@ import {PoChargesComponent} from './charges.component';
     PoNotesComponent,
     PoCreateComponent,
     PoChargesComponent,
-    PrintComponent
+    PrintComponent,
+    DisencumberChargeDialogComponent
   ],
   imports: [
     StaffCommonModule,
@@ -35,7 +37,8 @@ import {PoChargesComponent} from './charges.component';
     PoRoutingModule
   ],
   providers: [
-    PoService
+    PoService,
+    PicklistUploadService
   ]
 })
 
index 25d1924..2d39d23 100644 (file)
@@ -2,12 +2,18 @@ import {Injectable, EventEmitter} from '@angular/core';
 import {Observable, from} from 'rxjs';
 import {switchMap, map, tap, merge} from 'rxjs/operators';
 import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {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 {
 
@@ -18,6 +24,8 @@ export class PoService {
     constructor(
         private evt: EventService,
         private net: NetService,
+        private org: OrgService,
+        private pcrud: PcrudService,
         private auth: AuthService
     ) {}
 
@@ -34,6 +42,7 @@ export class PoService {
             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 || {});
@@ -56,20 +65,63 @@ export class PoService {
 
     // 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;
+        }
+    }
 }
 
 
index beadb5d..d0f0ea3 100644 (file)
@@ -5,12 +5,33 @@ import {ActivatedRoute, ParamMap} from '@angular/router';
 import {IdlObject} from '@eg/core/idl.service';
 import {NetService} from '@eg/core/net.service';
 import {AuthService} from '@eg/core/auth.service';
+import {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'
@@ -30,8 +51,10 @@ export class PrintComponent implements OnInit, AfterViewInit {
         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) {
     }
@@ -68,10 +91,33 @@ export class PrintComponent implements OnInit, AfterViewInit {
             }
         })
         .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({
index 02a7918..8f3508d 100644 (file)
@@ -1,5 +1,8 @@
-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';
@@ -7,12 +10,25 @@ import {LineitemListComponent} from '../lineitem/lineitem-list.component';
 import {LineitemDetailComponent} from '../lineitem/detail.component';
 import {LineitemCopiesComponent} from '../lineitem/copies.component';
 import {BriefRecordComponent} from '../lineitem/brief-record.component';
+import {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
@@ -32,6 +48,9 @@ const routes: Routes = [{
     path: 'brief-record',
     component: BriefRecordComponent
   }, {
+    path: 'create-assets',
+    component: CreateAssetsComponent
+  }, {
     path: 'lineitem/:lineitemId/detail',
     component: LineitemDetailComponent
   }, {
@@ -39,7 +58,8 @@ const routes: Routes = [{
     component: LineitemHistoryComponent
   }, {
     path: 'lineitem/:lineitemId/items',
-    component: LineitemCopiesComponent
+    component: LineitemCopiesComponent,
+    canDeactivate: [CanLeavePoChildGuard]
   }, {
     path: 'lineitem/:lineitemId/worksheet',
     component: LineitemWorksheetComponent
@@ -58,7 +78,7 @@ const routes: Routes = [{
 @NgModule({
   imports: [RouterModule.forChild(routes)],
   exports: [RouterModule],
-  providers: []
+  providers: [CanLeavePoChildGuard]
 })
 
 export class PoRoutingModule {}
index 262b020..30bae1d 100644 (file)
@@ -1,11 +1,18 @@
 
-<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">
 
@@ -25,6 +32,7 @@
             </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>
 
index 5f2faa7..8484cf0 100644 (file)
@@ -15,7 +15,11 @@ import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
 import {PoService} from './po.service';
 import {LineitemService} from '../lineitem/lineitem.service';
 import {CancelDialogComponent} from '../lineitem/cancel-dialog.component';
+import {LinkInvoiceDialogComponent} from '../lineitem/link-invoice-dialog.component';
 
+const PO_ACTIVATION_WARNINGS = [
+    'ACQ_FUND_EXCEEDS_WARN_PERCENT'
+];
 
 @Component({
   templateUrl: 'summary.component.html',
@@ -33,6 +37,10 @@ export class PoSummaryComponent implements OnInit, OnDestroy {
 
     newPoName: string;
     editPoName = false;
+    dupeResults = {
+        dupeFound: false,
+        dupePoId: -1
+    };
     initDone = false;
     ediMessageCount = 0;
     invoiceCount = 0;
@@ -40,15 +48,21 @@ export class PoSummaryComponent implements OnInit, OnDestroy {
     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,
@@ -83,10 +97,18 @@ export class PoSummaryComponent implements OnInit, OnDestroy {
         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
@@ -106,13 +128,22 @@ export class PoSummaryComponent implements OnInit, OnDestroy {
 
         })
         .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 {
@@ -132,11 +163,13 @@ export class PoSummaryComponent implements OnInit, OnDestroy {
                 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);
@@ -148,6 +181,12 @@ export class PoSummaryComponent implements OnInit, OnDestroy {
         }
     }
 
+    checkDuplicatePoName() {
+        this.poService.checkDuplicatePoName(
+            this.po().ordering_agency(), this.newPoName, this.dupeResults
+        );
+    }
+
     cancelPo() {
         this.cancelDialog.open().subscribe(reason => {
             if (!reason) { return; }
@@ -157,16 +196,36 @@ export class PoSummaryComponent implements OnInit, OnDestroy {
             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;
@@ -177,14 +236,20 @@ export class PoSummaryComponent implements OnInit, OnDestroy {
             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(_ => {
 
@@ -198,23 +263,56 @@ export class PoSummaryComponent implements OnInit, OnDestroy {
     }
 
     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
@@ -230,7 +328,13 @@ export class PoSummaryComponent implements OnInit, OnDestroy {
             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(
@@ -259,6 +363,19 @@ export class PoSummaryComponent implements OnInit, OnDestroy {
         .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 => {
index caed7ae..1cf4d8f 100644 (file)
@@ -1,5 +1,5 @@
 <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>
index b78a2bd..81c1d7e 100644 (file)
              [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>
index 0f0646f..1b2e3da 100644 (file)
@@ -4,7 +4,7 @@ import {IdlService, IdlObject} from '@eg/core/idl.service';
 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({
@@ -46,8 +46,7 @@ export class AcqSearchFormComponent implements OnInit, OnChanges {
         private pcrud: PcrudService,
         private store: ServerStoreService,
         private idl: IdlService,
-        private toast: ToastService,
-        private acqSearch: AcqSearchService
+        private toast: ToastService
     ) {}
 
     ngOnInit() {
@@ -240,18 +239,4 @@ export class AcqSearchFormComponent implements OnInit, OnChanges {
     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);
-    }
 }
index 1fea224..05226d5 100644 (file)
@@ -12,6 +12,7 @@ import {PicklistCloneDialogComponent} from './picklist-clone-dialog.component';
 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: [
@@ -28,7 +29,8 @@ import {AcqSearchService} from './acq-search.service';
   ],
   imports: [
     StaffCommonModule,
-    AcqSearchRoutingModule
+    AcqSearchRoutingModule,
+    LineitemModule
   ],
   providers: [AcqSearchService]
 })
index b1367da..577f6ba 100644 (file)
@@ -9,7 +9,6 @@ import {Pager} from '@eg/share/util/pager';
 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',
@@ -109,15 +108,11 @@ export class AcqSearchService {
     _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;
@@ -287,14 +282,4 @@ export class AcqSearchService {
         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;
-        });
-    }
 }
index 9b39ef3..334d357 100644 (file)
@@ -2,9 +2,40 @@
   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>
index 6c757f8..6a30b78 100644 (file)
@@ -1,6 +1,6 @@
 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';
@@ -9,7 +9,19 @@ import {AuthService} from '@eg/core/auth.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',
@@ -22,6 +34,25 @@ export class LineitemResultsComponent implements OnInit {
     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;
 
@@ -30,11 +61,14 @@ export class LineitemResultsComponent implements OnInit {
         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 => {
@@ -68,10 +102,293 @@ export class LineitemResultsComponent implements OnInit {
     }
 
     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();
+        });
+    }
+
 }
index 9aaeea0..7d03199 100644 (file)
 </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>
index 4fafe83..65d27e8 100644 (file)
@@ -85,14 +85,6 @@ export class PicklistResultsComponent implements OnInit {
         };
     }
 
-    showExpAngOptions(): boolean {
-        return this.acqSearch.angSelectionEnabled;
-    }
-
-    showExpAngLinks(): boolean {
-        return this.acqSearch.angSearchLinksEnabled;
-    }
-
     openCreateDialog() {
         this.picklistCreateDialog.open().subscribe(
             modified => {
@@ -138,7 +130,7 @@ export class PicklistResultsComponent implements OnInit {
     }
 
     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) {
index 6798708..14f78de 100644 (file)
@@ -4,17 +4,11 @@
   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">
index b4556d8..254e124 100644 (file)
@@ -55,7 +55,7 @@ export class PurchaseOrderResultsComponent implements OnInit {
     }
 
     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) {
@@ -64,9 +64,4 @@ export class PurchaseOrderResultsComponent implements OnInit {
             this.purchaseOrderResultsGrid.reload();
         });
     }
-
-    showExpAngLinks(): boolean {
-        return this.acqSearch.angSearchLinksEnabled;
-    }
-
 }
index c3b559d..bdc4638 100644 (file)
@@ -2,7 +2,6 @@ import {Injectable} from '@angular/core';
 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[]>> {
@@ -11,16 +10,16 @@ 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()
+        ]);
     }
 
 }
index ed3d7bf..f1f5916 100644 (file)
               <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>
index 2989018..ab959ca 100644 (file)
       </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">
index 1de372d..ccd6848 100644 (file)
@@ -65,10 +65,6 @@ export class StaffNavComponent implements OnInit, OnDestroy {
             .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']));
index 13969a6..0ce843c 100644 (file)
@@ -142,7 +142,6 @@ export class StaffResolver implements Resolve<Observable<any>> {
             '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
index 216ba84..4e1366e 100644 (file)
@@ -292,6 +292,16 @@ body>.dropdown-menu {z-index: 2100;}
   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;
index 271443f..51b8d70 100644 (file)
@@ -21964,10 +21964,20 @@ VALUES (
 
 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'
     )
 );
index 610adc3..7f8c492 100644 (file)
@@ -40,17 +40,6 @@ VALUES (
     )
 );
 
-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 (
index c9f9e4d..4f3d556 100644 (file)
@@ -12,4 +12,14 @@ 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;
index 50b7928..4132037 100644 (file)
        </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" 
index d5a58a0..a97f9b1 100644 (file)
@@ -148,7 +148,7 @@ openils.acq.Lineitem.fetchAndRender = function(liId, args, callback) {
                     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) {
index 0947f21..c5d407b 100644 (file)
     "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}'>&#x2318; ${13} ${18}</a>\n<a style='padding-right: 10px;' class='hidden${21}' href='${11}/acq/picklist/view/${14}?focus_li=${10}&source=${22}'>&#x2756; ${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}'>&#x2318; ${13} ${18}</a>\n<a style='padding-right: 10px;' class='hidden${21}' target='_top' href='/eg2/en-US/staff/acq/picklist/${14}#${10}'>&#x2756; ${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",
index d024917..f722013 100644 (file)
@@ -131,7 +131,7 @@ function($scope , $q , $window , $location , $timeout , egCore , egNet , egGridD
                     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;
                         }