LP1904036 add billing and more
authorBill Erickson <berickxx@gmail.com>
Thu, 11 Mar 2021 18:36:10 +0000 (13:36 -0500)
committerGalen Charlton <gmc@equinoxOLI.org>
Fri, 28 Oct 2022 00:13:26 +0000 (20:13 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Jane Sandberg <js7389@princeton.edu>
Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>
Open-ILS/src/eg2/src/app/staff/circ/patron/bills.component.html
Open-ILS/src/eg2/src/app/staff/circ/patron/bills.component.ts
Open-ILS/src/eg2/src/app/staff/circ/patron/resolver.service.ts
Open-ILS/src/eg2/src/app/staff/share/billing/billing-dialog.component.html
Open-ILS/src/eg2/src/app/staff/share/billing/billing-dialog.component.ts

index a64d1bf..bfd4bc3 100644 (file)
@@ -13,6 +13,9 @@
 <eg-credit-card-dialog [patron]="patron()" #creditCardDialog>
 </eg-credit-card-dialog>
 
+<eg-add-billing-dialog [patronId]="patronId" [newXact]="true" #billingDialog>
+</eg-add-billing-dialog>
+
 <!-- SUMMARY  -->
 
 <ng-container *ngIf="summary">
@@ -20,7 +23,7 @@
     <div class="col-lg-3 pr-0 mr-0 border-right">
       <div class="d-flex pt-1 pb-1 striped">
         <div class="flex-4" i18n>Total Owed:</div>
-        <div class="flex-1" 
+        <div class="flex-1"
           [ngClass]="{'font-weight-bold' : summary.balance_owed() > 0}">
           {{summary.balance_owed() | currency}}</div>
       </div>
         (ngModelChange)="updatePendingColumn()" id="pay-amount" [min]="0"/>
     </div>
     <div class="ml-2 form-check form-check-inline">
-      <input class="form-check-input" type="checkbox" 
+      <input class="form-check-input" type="checkbox"
+        (ngModelChange)="applySetting('eg.circ.bills.annotatepayment', $event)"
         id="annotate" [(ngModel)]="annotatePayment"/>
       <label class="form-check-label" for="annotate" i18n>Annotate</label>
     </div>
     <div class="ml-2">
-      <button class="btn btn-outline-dark" (click)="applyPayment()" 
+      <button class="btn btn-outline-dark" (click)="applyPayment()"
         [disabled]="disablePayment()" i18n>Apply Payment</button>
     </div>
   </div>
 
 <ng-template #callNumberTemplate let-r="row">
   <ng-container *ngIf="r.volume">
-    {{r.volume.prefix().label()}} {{r.volume.label()}} {{r.volume.suffix().label()}} 
+    {{r.volume.prefix().label()}} {{r.volume.label()}} {{r.volume.suffix().label()}}
   </ng-container>
   <ng-container *ngIf="!r.volume" i18n>UNCATALOGED</ng-container>
 </ng-template>
 
-<eg-grid #billGrid [dataSource]="gridDataSource" 
+<eg-grid #billGrid [dataSource]="gridDataSource"
   [sortable]="true" [useLocalSort]="true"
   [cellTextGenerator]="cellTextGenerator">
 
-  <eg-grid-toolbar-action                                                    
-    i18n-label label="Print Bills" (onClick)="printBills($event)">                            
-  </eg-grid-toolbar-action> 
+  <eg-grid-toolbar-button i18n-label label="Add Billing"
+    (onClick)="addBilling()"></eg-grid-toolbar-button>
+
+  <eg-grid-toolbar-button i18n-label label="Select All Refunds"
+    (onClick)="selectRefunds()"></eg-grid-toolbar-button>
+
+  <eg-grid-toolbar-action
+    i18n-label label="Print Bills" (onClick)="printBills($event)">
+  </eg-grid-toolbar-action>
 
   <eg-grid-column path="xact.id" [index]="true" label="Bill #" i18n-label>
   </eg-grid-column>
 
   <eg-grid-column path="billingLocation"
     label="Billing Location" i18n-label></eg-grid-column>
-    
+
   <eg-grid-column path="call_number" label="Call Number" i18n-label
     [cellTemplate]="callNumberTemplate"></eg-grid-column>
 
   <eg-grid-column name="copy_barcode" label="Item Barcode" i18n-label
     [cellTemplate]="barcodeTemplate"></eg-grid-column>
 
-  <eg-grid-column path="copy.location.name" label="Shelving Location" 
+  <eg-grid-column path="copy.location.name" label="Shelving Location"
     i18n-label></eg-grid-column>
 
-  <eg-grid-column name="title" label="Title" i18n-label 
+  <eg-grid-column name="title" label="Title" i18n-label
     [cellTemplate]="titleTemplate"></eg-grid-column>
 
-  <eg-grid-column path="xact.summary.balance_owed" datatype="money" 
+  <eg-grid-column path="xact.summary.balance_owed" datatype="money"
     label="Balance Owed" i18n-label></eg-grid-column>
 
-  <eg-grid-column path="xact.summary.total_owed" datatype="money" 
+  <eg-grid-column path="xact.summary.total_owed" datatype="money"
     label="Total Billed" i18n-label></eg-grid-column>
 
-  <eg-grid-column path="xact.summary.total_paid" datatype="money" 
+  <eg-grid-column path="xact.summary.total_paid" datatype="money"
     label="Total Paid" i18n-label></eg-grid-column>
 
   <eg-grid-column name="paymentPending" datatype="money"
     label="Payment Pending" i18n-label></eg-grid-column>
 
-  <eg-grid-column name="author" [hidden]="true" 
+  <eg-grid-column name="author" [hidden]="true"
     label="Author" i18n-label></eg-grid-column>
   <eg-grid-column path="usr" [hidden]="true"></eg-grid-column>
   <eg-grid-column path="unrecovered" [hidden]="true"></eg-grid-column>
     <div class="flex-1"></div>
     <div class="d-flex flex-colum justify-content-end">
       <div class="form-check form-check-inline">
-      <input class="form-check-input" type="checkbox" id="patron-credit-cbox" 
+      <input class="form-check-input" type="checkbox" id="patron-credit-cbox"
         [(ngModel)]="convertChangeToCredit"/>
       <label class="form-check-label" for="patron-credit-cbox" i18n>
         Convert Change To Patron Credit
     </div>
     <div class="d-flex flex-colum justify-content-end">
       <div class="form-check form-check-inline ml-2">
-      <input class="form-check-input" type="checkbox" id="receipt-on-payment-cbox" 
+      <input class="form-check-input" type="checkbox" id="receipt-on-payment-cbox"
+        (ngModelChange)="applySetting('circ.bills.receiptonpay', $event)"
         [(ngModel)]="receiptOnPayment"/>
       <label class="form-check-label" for="receipt-on-payment-cbox" i18n>
         Receipt On Payment
index 2bf5f2d..e1a4864 100644 (file)
@@ -22,6 +22,7 @@ import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
 import {CreditCardDialogComponent
     } from '@eg/staff/share/billing/credit-card-dialog.component';
 import {BillingService, CreditCardPaymentParams} from '@eg/staff/share/billing/billing.service';
+import {AddBillingDialogComponent} from '@eg/staff/share/billing/billing-dialog.component';
 
 interface BillGridEntry extends CircDisplayInfo {
     xact: IdlObject // mbt
@@ -63,6 +64,7 @@ export class BillsComponent implements OnInit, AfterViewInit {
     @ViewChild('maxPayDialog') private maxPayDialog: AlertDialogComponent;
     @ViewChild('warnPayDialog') private warnPayDialog: ConfirmDialogComponent;
     @ViewChild('creditCardDialog') private creditCardDialog: CreditCardDialogComponent;
+    @ViewChild('billingDialog') private billingDialog: AddBillingDialogComponent;
 
     constructor(
         private router: Router,
@@ -99,17 +101,22 @@ export class BillsComponent implements OnInit, AfterViewInit {
             return from(page);
         };
 
-        this.applySettings().then(_ => this.load());
+        this.loadSettings().then(_ => this.load());
     }
 
-    applySettings(): Promise<any> {
+    loadSettings(): Promise<any> {
         return this.serverStore.getItemBatch([
             'ui.circ.billing.amount_warn',
             'ui.circ.billing.amount_limit',
-            'circ.staff_client.do_not_auto_attempt_print'
+            'circ.staff_client.do_not_auto_attempt_print',
+            'circ.bills.receiptonpay',
+            'eg.circ.bills.annotatepayment'
+
         ]).then(sets => {
             this.maxPayAmount = sets['ui.circ.billing.amount_limit'] || 100000;
             this.warnPayAmount = sets['ui.circ.billing.amount_warn'] || 1000;
+            this.receiptOnPayment = sets['circ.bills.receiptonpay'];
+            this.annotatePayment = sets['eg.circ.bills.annotatepayment'];
 
             const noPrint = sets['circ.staff_client.do_not_auto_attempt_print'];
             if (noPrint && noPrint.includes('Bill Pay')) {
@@ -118,6 +125,10 @@ export class BillsComponent implements OnInit, AfterViewInit {
         });
     }
 
+    applySetting(name: string, value: any) {
+        this.serverStore.setItem(name, value);
+    }
+
     ngAfterViewInit() {
         // Recaclulate the amount owed per selected transaction as the
         // grid rows selections change.
@@ -138,7 +149,7 @@ export class BillsComponent implements OnInit, AfterViewInit {
     // summary, and slot them back into the entries array.
     load(refreshXacts?: number[]): Promise<any> {
 
-        let entries = [];
+        if (!refreshXacts) { this.entries = []; }
         this.summary = null;
         this.gridDataSource.requestingData = true;
 
@@ -155,24 +166,27 @@ export class BillsComponent implements OnInit, AfterViewInit {
             }
 
             if (refreshXacts) {
-
-                // Slot the updated xact back into place
-                entries.push(this.formatForDisplay(resp));
-                entries = entries.map(e => {
-                    if (e.xact.id() === resp.id()) {
-                        return this.formatForDisplay(resp);
-                    }
-                    return e;
-                });
+                let idx = 0;
+                for (;idx < this.entries.length; idx++) {
+                    const entry = this.entries[idx];
+                    if (entry.xact.id() === resp.id()) { break; }
+                }
+
+                if (idx < this.entries.length) {
+                    // Update the existing entry
+                    this.entries[idx] = this.formatForDisplay(resp);
+                } else {
+                    // Adding a new transaction (e.g. from new billing)
+                    this.entries.push(this.formatForDisplay(resp));
+                }
 
             } else {
-                entries.push(this.formatForDisplay(resp));
+                this.entries.push(this.formatForDisplay(resp));
             }
         })).toPromise()
 
         .then(_ => {
             this.gridDataSource.requestingData = false;
-            this.entries = entries;
             this.billGrid.reload();
         });
     }
@@ -289,7 +303,6 @@ export class BillsComponent implements OnInit, AfterViewInit {
     }
 
     applyPayment() {
-
         if (this.amountExceedsMax()) { return; }
 
         this.applyingPayment = true;
@@ -458,6 +471,22 @@ export class BillsComponent implements OnInit, AfterViewInit {
         });
     }
 
+    selectRefunds() {
+        this.billGrid.context.rowSelector.clear();
+        this.entries.forEach(entry => {
+            if (entry.xact.summary().balance_owed() < 0) {
+                this.billGrid.context.toggleSelectOneRow(entry.xact.id());
+            }
+        });
+    }
 
+    addBilling() {
+        this.billingDialog.open().subscribe(data => {
+            if (data) {
+                this.context.refreshPatron();
+                this.load([data.xactId]);
+            }
+        });
+    }
 }
 
index bb71414..8c6b217 100644 (file)
@@ -27,6 +27,8 @@ export class PatronResolver implements Resolve<Promise<any[]>> {
         // Some of these are used by the shared circ services.
         // Precache them since we're making the call anyway.
         return this.store.getItemBatch([
+          'circ.bills.receiptonpay',
+          'eg.circ.bills.annotatepayment',
           'eg.circ.patron.summary.collapse',
           'circ.do_not_tally_claims_returned',
           'circ.tally_lost',
index 04dd4c0..34c485a 100644 (file)
@@ -11,9 +11,9 @@
   <div class="modal-header bg-info">
     <h4 class="modal-title" i18n>
       Bill Patron: 
-        {{xact.usr().family_name()}}, 
-        {{xact.usr().first_given_name()}} : 
-        {{xact.usr().card().barcode()}}
+        {{patron.family_name()}}, 
+        {{patron.first_given_name()}} : 
+        {{patron.card().barcode()}}
     </h4>
     <button type="button" class="close"
       i18n-aria-label aria-label="Close" (click)="close()">
   </div>
   <div class="modal-body">
 
-    <div class="row">
-      <div class="col-lg-2" i18n>Bill #</div>
-      <div class="col-lg-4">{{xact.id()}}</div>
-      <div class="col-lg-4" i18n>Total Billed</div>
-      <div class="col-lg-2">{{xact.summary().total_owed() | currency}}</div>
-    </div>
-    <div class="row">
-      <div class="col-lg-2" i18n>Type</div>
-      <div class="col-lg-4">{{xact.summary().xact_type()}}</div>
-      <div class="col-lg-4" i18n>Total Paid</div>
-      <div class="col-lg-2">{{xact.summary().total_paid() | currency}}</div>
-    </div>
-    <div class="row">
-      <div class="col-lg-2" i18n>Start</div>
-      <div class="col-lg-4">{{xact.xact_start() | date:'short'}}</div>
-      <div class="col-lg-4" i18n>Balance Owed</div>
-      <div class="col-lg-2">{{xact.summary().balance_owed() | currency}}</div>
-    </div>
-    <div class="row">
-      <div class="col-lg-2" i18n>Finish</div>
-      <div class="col-lg-4">{{xact.xact_finish() | date:'short'}}</div>
-      <div class="col-lg-4" i18n>Renewal?</div>
-      <div class="col-lg-2"><eg-bool [value]="isRenewal()"></eg-bool></div>
-    </div>
+    <ng-container *ngIf="xact">
+      <div class="row">
+        <div class="col-lg-2" i18n>Bill #</div>
+        <div class="col-lg-4">{{xact.id()}}</div>
+        <div class="col-lg-4" i18n>Total Billed</div>
+        <div class="col-lg-2">{{xact.summary().total_owed() | currency}}</div>
+      </div>
+      <div class="row">
+        <div class="col-lg-2" i18n>Type</div>
+        <div class="col-lg-4">{{xact.summary().xact_type()}}</div>
+        <div class="col-lg-4" i18n>Total Paid</div>
+        <div class="col-lg-2">{{xact.summary().total_paid() | currency}}</div>
+      </div>
+      <div class="row">
+        <div class="col-lg-2" i18n>Start</div>
+        <div class="col-lg-4">{{xact.xact_start() | date:'short'}}</div>
+        <div class="col-lg-4" i18n>Balance Owed</div>
+        <div class="col-lg-2">{{xact.summary().balance_owed() | currency}}</div>
+      </div>
+      <div class="row">
+        <div class="col-lg-2" i18n>Finish</div>
+        <div class="col-lg-4">{{xact.xact_finish() | date:'short'}}</div>
+        <div class="col-lg-4" i18n>Renewal?</div>
+        <div class="col-lg-2"><eg-bool [value]="isRenewal()"></eg-bool></div>
+      </div>
+    </ng-container>
 
     <hr/>
 
index e4a0591..36dac62 100644 (file)
@@ -1,6 +1,6 @@
 import {Component, OnInit, Input, ViewChild} from '@angular/core';
-import {Observable} from 'rxjs';
-import {switchMap} from 'rxjs/operators';
+import {Observable, empty} from 'rxjs';
+import {switchMap, tap} from 'rxjs/operators';
 import {IdlObject, IdlService} from '@eg/core/idl.service';
 import {NetService} from '@eg/core/net.service';
 import {EventService} from '@eg/core/event.service';
@@ -27,7 +27,10 @@ export class AddBillingDialogComponent
     extends DialogComponent implements OnInit {
 
     @Input() xactId: number;
+    @Input() newXact = false;
+    @Input() patronId: number;
 
+    patron: IdlObject;
     xact: IdlObject;
     billingType: ComboboxEntry;
     billingTypes: ComboboxEntry[] = [];
@@ -57,6 +60,8 @@ export class AddBillingDialogComponent
             this.billingTypes = types.map(bt => {
                 return {id: bt.id(), label: bt.name(), fm: bt};
             });
+            this.billingType = this.billingTypes
+                .filter(t => t.id === DEFAULT_BILLING_TYPE)[0];
         });
 
         this.hereOrg = this.org.get(this.auth.user().ws_ou()).shortname();
@@ -71,18 +76,30 @@ export class AddBillingDialogComponent
     }
 
     open(options: NgbModalOptions = {}): Observable<any> {
+        let obs: Observable<any>;
+
+        // Load some data before opening.
+        if (this.newXact) {
+
+            obs = this.pcrud.retrieve('au', this.patronId,
+                {flesh: 1, flesh_fields: {au: ['card']}}
+            ).pipe(tap(user => this.patron = user));
+
+        } else {
+
+            obs = this.pcrud.retrieve('mbt', this.xactId, {
+                flesh: 2,
+                flesh_fields: {
+                    mbt: ['usr', 'summary', 'circulation'],
+                    au: ['card']
+                }
+            }).pipe(tap(xact => {
+                this.xact = xact;
+                this.patron = xact.usr();
+            }));
+        }
 
-        // Fetch the xact data before opening the dialog.
-        return this.pcrud.retrieve('mbt', this.xactId, {
-            flesh: 2,
-            flesh_fields: {
-                mbt: ['usr', 'summary', 'circulation'],
-                au: ['card']
-            }
-        }).pipe(switchMap(xact => {
-            this.xact = xact;
-            return super.open(options);
-        }));
+        return obs.pipe(switchMap(_ => super.open(options)));
     }
 
     isRenewal(): boolean {
@@ -105,28 +122,56 @@ export class AddBillingDialogComponent
     }
 
     submit() {
+        const promise = this.newXact ?
+            this.createGroceryXact() : Promise.resolve(this.xactId);
+
+        let xactId;
+        promise.then(id => {
+            xactId = id;
+            return this.createBill(id)
+        })
+        .then(billId => this.close({xactId: xactId, billId: billId}));
+    }
+
+    handleResponse(id: number): number {
+        const evt = this.evt.parse(id);
+        if (evt) {
+            console.error(evt);
+            alert(evt);
+            return null;
+        } else {
+            return id;
+        }
+    }
+
+    createGroceryXact(): Promise<number> {
+        const groc = this.idl.create('mg');
+        groc.billing_location(this.auth.user().ws_ou());
+        groc.note(this.note);
+        groc.usr(this.patronId);
+
+        return this.net.request(
+            'open-ils.circ',
+            'open-ils.circ.money.grocery.create',
+            this.auth.token(), groc
+        ).toPromise().then(xactId => this.handleResponse(xactId));
+    }
+
+    createBill(xactId: number): Promise<number> {
+        if (!xactId) { return Promise.reject('no xact'); }
+
         const bill = this.idl.create('mb');
-        bill.xact(this.xactId);
+        bill.xact(xactId);
         bill.amount(this.amount);
         bill.btype(this.billingType.id);
         bill.billing_type(this.billingType.label);
         bill.note(this.note);
 
-        this.net.request(
+        return this.net.request(
             'open-ils.circ',
             'open-ils.circ.money.billing.create',
             this.auth.token(), bill
-        ).subscribe(billId => {
-
-            const evt = this.evt.parse(billId);
-            if (evt) {
-                console.error(evt);
-                alert(evt);
-                this.close(null);
-            } else {
-                this.close(billId);
-            }
-        });
+        ).toPromise().then(billId => this.handleResponse(billId));
     }
 }