LP#1942220: implement 'Add Items To Selected Lineitems'
authorGalen Charlton <gmc@equinoxOLI.org>
Tue, 7 Dec 2021 23:43:18 +0000 (18:43 -0500)
committerGalen Charlton <gmc@equinoxOLI.org>
Tue, 7 Dec 2021 23:43:54 +0000 (18:43 -0500)
Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>
12 files changed:
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/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/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.html
Open-ILS/src/eg2/src/app/staff/acq/lineitem/copy-attrs.component.ts
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

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..c55f842
--- /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>Lineitem(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); }
+}
+
+
index 310017e..7b7717b 100644 (file)
@@ -16,9 +16,9 @@
     <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"></div>
     <div class="flex-1 p-1"></div>
@@ -35,6 +35,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>
@@ -47,6 +48,7 @@
   <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)" 
index 5adb6e3..90cbf9d 100644 (file)
@@ -28,6 +28,7 @@ const BATCH_FIELDS = [
 export class LineitemBatchCopiesComponent implements OnInit {
 
     @Input() lineitem: IdlObject;
+    @Input() batchAdd = false;
 
     @ViewChild('confirmAlertsDialog') confirmAlertsDialog: ConfirmDialogComponent;
     @ViewChild('cancelDialog') cancelDialog: CancelDialogComponent;
@@ -43,7 +44,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) {
index a29b178..48de275 100644 (file)
@@ -23,7 +23,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 +38,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">
@@ -58,7 +58,7 @@
     </div>
   </div>
 
-  <eg-lineitem-batch-copies [lineitem]="lineitem"></eg-lineitem-batch-copies>
+  <eg-lineitem-batch-copies [lineitem]="lineitem" [batchAdd]="mode === 'multiAdd'"></eg-lineitem-batch-copies>
 </ng-container>
 
 
index ac5788e..60cb0f2 100644 (file)
@@ -26,11 +26,23 @@ 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;
@@ -63,13 +75,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 +100,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() {
@@ -163,11 +191,16 @@ 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);
+        } 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
index 43f13b3..660cc13 100644 (file)
@@ -49,7 +49,7 @@
       (onChange)="valueChange('circ_modifier', $event)">
     </eg-combobox>
   </div>
-  <div class="flex-1 p-1">
+  <div class="flex-1 p-1" *ngIf="!batchAdd">
     <ng-container *ngIf="fieldIsDisabled('cn_label')">
       <span>{{copy.cn_label()}}</span>
     </ng-container>
@@ -65,7 +65,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">
       <ng-container *ngIf="fieldIsDisabled('barcode')">
         <span>{{copy.barcode()}}</span>
       </ng-container>
index d6b5aaa..e30e45c 100644 (file)
@@ -18,6 +18,7 @@ export class LineitemCopyAttrsComponent implements OnInit {
 
     @Input() lineitem: IdlObject;
     @Input() rowIndex: number;
+    @Input() batchAdd = false;
 
     fundEntries: ComboboxEntry[];
     circModEntries: ComboboxEntry[];
index a560505..c240536 100644 (file)
@@ -2,8 +2,14 @@
 <!-- BATCH ACTIONS -->
 <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-claim-policy-dialog #claimPolicyDialog></eg-acq-claim-policy-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">
     <div ngbDropdown>
@@ -13,6 +19,8 @@
           queryParamsHandling="merge" i18n>Add Brief Record</a>
         <button ngbDropdownItem (click)="deleteLineitems()" 
           [disabled]="!canDeleteLis() || !selectedIds().length" i18n>Delete Selected Lineitems</button>
+        <button ngbDropdownItem (click)="addCopiesToLineitems()" 
+          [disabled]="!canAddLiCopies() || !selectedIds().length" i18n>Add Items to Selected Lineitems</button>
         <div class="dropdown-divider"></div>
         <h6 class="dropdown-header" i18n>Selection List Actions</h6>
         <button ngbDropdownItem (click)="createPo()" 
index c739bb3..57cbd14 100644 (file)
@@ -1,18 +1,20 @@
 import {Component, OnInit, Input, Output, ViewChild} from '@angular/core';
 import {Router, ActivatedRoute, ParamMap} from '@angular/router';
-import {Observable, from, of} 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 {ServerStoreService} from '@eg/core/server-store.service';
 import {LineitemService} 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 {CancelDialogComponent} from './cancel-dialog.component';
 import {DeleteLineitemsDialogComponent} from './delete-lineitems-dialog.component';
+import {AddCopiesDialogComponent} from './add-copies-dialog.component';
 import {ClaimPolicyDialogComponent} from './claim-policy-dialog.component';
 
 const DELETABLE_STATES = [
@@ -44,6 +46,8 @@ export class LineitemListComponent implements OnInit {
 
     picklistId: number = null;
     poId: number = null;
+    poAllowsNewCopies = false;
+    poSubscription: Subscription;
     recordId: number = null; // lineitems related to a bib.
 
     loading = false;
@@ -51,6 +55,10 @@ export class LineitemListComponent implements OnInit {
     pageOfLineitems: IdlObject[] = [];
     lineitemIds: number[] = [];
 
+    saving = false;
+    progressMax = 0;
+    progressValue = 0;
+
     // Selected lineitems
     selected: {[id: number]: boolean} = {};
 
@@ -106,6 +114,7 @@ export class LineitemListComponent implements OnInit {
 
     @ViewChild('cancelDialog') cancelDialog: CancelDialogComponent;
     @ViewChild('deleteLineitemsDialog') deleteLineitemsDialog: DeleteLineitemsDialogComponent;
+    @ViewChild('addCopiesDialog') addCopiesDialog: AddCopiesDialogComponent;
     @ViewChild('claimPolicyDialog') claimPolicyDialog: ClaimPolicyDialogComponent;
 
     constructor(
@@ -115,8 +124,10 @@ export class LineitemListComponent implements OnInit {
         private net: NetService,
         private auth: AuthService,
         private store: ServerStoreService,
+        private idl: IdlService,
         private holdings: HoldingsService,
-        private liService: LineitemService
+        private liService: LineitemService,
+        private poService: PoService
     ) {}
 
     ngOnInit() {
@@ -158,6 +169,13 @@ export class LineitemListComponent implements OnInit {
             });
         });
 
+        this.poSubscription = this.poService.poRetrieved.subscribe(() => {
+            this.poAllowsNewCopies = this.po().order_date() ? false : true;
+        });
+    }
+
+    po(): IdlObject {
+        return this.poService.currentPo;
     }
 
     pageSizeChange(count: number) {
@@ -644,6 +662,45 @@ export class LineitemListComponent implements OnInit {
         });
     }
 
+    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();
+                }
+            );
+
+        });
+    }
+
     liHasRealCopies(li: IdlObject): boolean {
         for (let idx = 0; idx < li.lineitem_details().length; idx++) {
             if (li.lineitem_details()[idx].eg_copy_id()) {
@@ -772,6 +829,19 @@ export class LineitemListComponent implements OnInit {
         });
     }
 
+    // can add lineitems unless the PO has been activated at some
+    // point in the past
+    canAddLiCopies(): boolean {
+        if (this.picklistId) {
+            return true;
+        } else {
+            if (this.po()) {
+                this.poAllowsNewCopies = this.po().order_date() ? false : true;
+            }
+            return this.poAllowsNewCopies;
+        }
+    }
+
     // For PO's, lineitems can only be deleted if they are pending order.
     canDeleteLis(): boolean {
         const li = this.pageOfLineitems[0];
index db48e9d..95a4b1b 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';
@@ -17,6 +18,7 @@ import {LineitemHistoryComponent} from './history.component';
 import {BriefRecordComponent} from './brief-record.component';
 import {CancelDialogComponent} from './cancel-dialog.component';
 import {DeleteLineitemsDialogComponent} from './delete-lineitems-dialog.component';
+import {AddCopiesDialogComponent} from './add-copies-dialog.component';
 import {ClaimPolicyDialogComponent} from './claim-policy-dialog.component';
 import {MarcEditModule} from '@eg/staff/share/marc-edit/marc-edit.module';
 
@@ -33,6 +35,7 @@ import {MarcEditModule} from '@eg/staff/share/marc-edit/marc-edit.module';
     LineitemHistoryComponent,
     CancelDialogComponent,
     DeleteLineitemsDialogComponent,
+    AddCopiesDialogComponent,
     ClaimPolicyDialogComponent,
     BriefRecordComponent,
     LineitemWorksheetComponent
@@ -41,6 +44,7 @@ import {MarcEditModule} from '@eg/staff/share/marc-edit/marc-edit.module';
     LineitemListComponent,
     CancelDialogComponent,
     DeleteLineitemsDialogComponent,
+    AddCopiesDialogComponent,
     ClaimPolicyDialogComponent
   ],
   imports: [
@@ -49,7 +53,8 @@ import {MarcEditModule} from '@eg/staff/share/marc-edit/marc-edit.module';
     MarcEditModule
   ],
   providers: [
-    LineitemService
+    LineitemService,
+    PoService
   ]
 })
 
index 315a116..e211c84 100644 (file)
@@ -302,6 +302,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