LP1904036 open circ exists dialog; billing
authorBill Erickson <berickxx@gmail.com>
Tue, 9 Mar 2021 15:14:01 +0000 (10:14 -0500)
committerGalen Charlton <gmc@equinoxOLI.org>
Fri, 28 Oct 2022 00:13:25 +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.ts
Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.ts
Open-ILS/src/eg2/src/app/staff/share/circ/circ.module.ts
Open-ILS/src/eg2/src/app/staff/share/circ/circ.service.ts
Open-ILS/src/eg2/src/app/staff/share/circ/components.component.html
Open-ILS/src/eg2/src/app/staff/share/circ/components.component.ts
Open-ILS/src/eg2/src/app/staff/share/circ/open-circ-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/circ/open-circ-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm

index 74ba8ae..5b4a7e0 100644 (file)
@@ -28,26 +28,6 @@ interface BillGridEntry extends CircDisplayInfo {
     paymentPending?: number;
 }
 
-const XACT_FLESH_DEPTH = 5;
-const XACT_FLESH_FIELDS = {
-  mbt: ['summary', 'circulation', 'grocery'],
-  circ: ['target_copy', 'workstation', 'checkin_workstation', 'circ_lib'],
-  acp:  [
-    'call_number',
-    'holds_count',
-    'status',
-    'circ_lib',
-    'location',
-    'floating',
-    'age_protect',
-    'parts'
-  ],
-  acpm: ['part'],
-  acn:  ['record', 'owning_lib', 'prefix', 'suffix'],
-  bre:  ['wide_display_entry']
-};
-
-
 @Component({
   templateUrl: 'bills.component.html',
   selector: 'eg-patron-bills',
@@ -152,24 +132,45 @@ export class BillsComponent implements OnInit, AfterViewInit {
         });
     }
 
-    load(): Promise<any> {
+    // In refresh mode, only fetch the requested xacts, with updated user
+    // summary, and slot them back into the entries array.
+    load(refreshXacts?: number[]): Promise<any> {
 
+        let entries = [];
         this.summary = null;
-        this.entries = [];
         this.gridDataSource.requestingData = true;
 
-        return this.net.request('open-ils.actor',
+        return this.net.request(
+            'open-ils.actor',
             'open-ils.actor.user.transactions.for_billing',
-            this.auth.token(), this.patronId
+            this.auth.token(), this.patronId, refreshXacts
+
         ).pipe(tap(resp => {
+
             if (!this.summary) { // 1st response is summary
                 this.summary = resp;
+                return;
+            }
+
+            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;
+                });
+
             } else {
-                this.entries.push(this.formatForDisplay(resp));
+                entries.push(this.formatForDisplay(resp));
             }
         })).toPromise()
+
         .then(_ => {
             this.gridDataSource.requestingData = false;
+            this.entries = entries;
             this.billGrid.reload();
         });
     }
@@ -292,6 +293,7 @@ export class BillsComponent implements OnInit, AfterViewInit {
         this.applyingPayment = true;
         this.paymentNote = '';
         this.ccPaymentParams = {};
+        const payments = this.compilePayments();
 
         this.verifyPayAmount()
         .then(_ => this.annotate())
@@ -301,21 +303,21 @@ export class BillsComponent implements OnInit, AfterViewInit {
                 this.patronId,
                 this.patron().last_xact_id(),
                 this.paymentType,
-                this.compilePayments(),
+                payments,
                 this.paymentNote,
                 this.checkNumber,
                 this.ccPaymentParams,
                 this.convertChangeToCredit
             );
         })
-        .then(paymentIds => this.handlePayReceipt(paymentIds))
-        .then(_ => this.load())
+        .then(paymentIds => this.handlePayReceipt(payments, paymentIds))
+        .then(_ => this.load(payments.map(p => p[0]))) // load xact IDs
         .then(_ => this.context.refreshPatron())
         .catch(msg => console.debug('Payment Canceled:', msg))
         .finally(() => this.applyingPayment = false);
     }
 
-    handlePayReceipt(paymentIds: number[]): Promise<any> {
+    handlePayReceipt(payments: Array<Array<number>>, paymentIds: number[]): Promise<any> {
 
         if (this.disableAutoPrint || !this.receiptOnPayment) {
             return Promise.resolve();
index 569b212..ce696e5 100644 (file)
@@ -109,6 +109,7 @@ export class CheckoutComponent implements OnInit, AfterViewInit {
             return this.barcodeSelect.getBarcode('asset', this.checkoutBarcode)
             .then(selection => {
                 if (selection) {
+                    params.copy_id = selection.id;
                     params.copy_barcode = selection.barcode;
                     return params;
                 } else {
index 6dafdfd..dc639e9 100644 (file)
@@ -9,6 +9,7 @@ import {PrecatCheckoutDialogComponent} from './precat-dialog.component';
 import {ClaimsReturnedDialogComponent} from './claims-returned-dialog.component';
 import {CircComponentsComponent} from './components.component';
 import {CircEventsComponent} from './events-dialog.component';
+import {OpenCircDialogComponent} from './open-circ-dialog.component';
 
 @NgModule({
     declarations: [
@@ -17,7 +18,8 @@ import {CircEventsComponent} from './events-dialog.component';
         DueDateDialogComponent,
         PrecatCheckoutDialogComponent,
         ClaimsReturnedDialogComponent,
-        CircEventsComponent
+        CircEventsComponent,
+        OpenCircDialogComponent
     ],
     imports: [
         StaffCommonModule,
index f920073..b5dbcfd 100644 (file)
@@ -120,6 +120,7 @@ export interface CheckoutParams {
     dummy_author?: string;
     dummy_isbn?: string;
     circ_modifier?: string;
+    void_overdues?: boolean;
 
     // internal tracking
     _override?: boolean;
@@ -144,6 +145,7 @@ export interface CheckinParams {
     copy_id?: number;
     copy_barcode?: string;
     claims_never_checked_out?: boolean;
+    void_overdues?: boolean;
 
     // internal tracking
     _override?: boolean;
@@ -269,12 +271,13 @@ export class CircService {
     processCheckoutResult(
         params: CheckoutParams, response: any): Promise<CheckoutResult> {
 
-        console.debug('checkout resturned', response);
 
         const allEvents = Array.isArray(response) ?
             response.map(r => this.evt.parse(r)) :
             [this.evt.parse(response)];
 
+        console.debug('checkout returned', allEvents.map(e => e.textcode));
+
         const firstEvent = allEvents[0];
         const payload = firstEvent.payload;
 
@@ -311,11 +314,67 @@ export class CircService {
 
             case 'ITEM_NOT_CATALOGED':
                 return this.handlePrecat(result);
+
+            case 'OPEN_CIRCULATION_EXISTS':
+                return this.handleOpenCirc(result);
         }
 
         return Promise.resolve(result);
     }
 
+
+    // Ask the user if we should resolve the circulation and check
+    // out to the user or leave it alone.
+    // When resolving and checking out, renew if it's for the same
+    // user, otherwise check it in, then back out to the current user.
+    handleOpenCirc(result: CheckoutResult): Promise<CheckoutResult> {
+
+        let sameUser = false;
+
+        return this.net.request(
+            'open-ils.circ',
+            'open-ils.circ.copy_checkout_history.retrieve',
+            this.auth.token(), result.params.copy_id, 1).toPromise()
+
+        .then(circs => {
+            const circ = circs[0];
+
+            sameUser = result.params.patron_id === circ.usr();
+            this.components.openCircDialog.sameUser = sameUser;
+            this.components.openCircDialog.circDate = circ.xact_start();
+
+            return this.components.openCircDialog.open().toPromise();
+        })
+
+        .then(fromDialog => {
+
+            // Leave the open circ checked out.
+            if (!fromDialog) { return result; }
+
+            const coParams = Object.assign({}, result.params); // clone
+
+            if (sameUser) {
+                coParams.void_overdues = fromDialog.forgiveFines;
+                return this.renew(coParams);
+            }
+
+            const ciParams: CheckinParams = {
+                noop: true,
+                copy_id: coParams.copy_id,
+                void_overdues: fromDialog.forgiveFines
+            };
+
+            return this.checkin(ciParams)
+            .then(res => {
+                if (res.success) {
+                    return this.checkout(coParams);
+                } else {
+                    return Promise.reject('Unable to check in item');
+                }
+            });
+        });
+    }
+
     handleOverridableCheckoutEvents(
         result: CheckoutResult, events: EgEvent[]): Promise<CheckoutResult> {
         const params = result.params;
@@ -398,11 +457,11 @@ export class CircService {
     processCheckinResult(
         params: CheckinParams, response: any): Promise<CheckinResult> {
 
-        console.debug('checkout resturned', response);
-
         const allEvents = Array.isArray(response) ?
             response.map(r => this.evt.parse(r)) : [this.evt.parse(response)];
 
+        console.debug('checkin returned', allEvents.map(e => e.textcode));
+
         const firstEvent = allEvents[0];
         const payload = firstEvent.payload;
 
index d29bc19..9afc0ef 100644 (file)
@@ -1,8 +1,14 @@
 
 <eg-precat-checkout-dialog #precatDialog></eg-precat-checkout-dialog>
+
 <eg-circ-events-dialog #circEventsDialog></eg-circ-events-dialog>
+
 <eg-alert-dialog #routeToCatalogingDialog
   i18n-dialogTitle dialogTitle="Route To Cataloging"
   i18n-dialogBody dialogBody="This item needs to be routed to CATALOGING">
 </eg-alert-dialog>
 
+<eg-open-circ-dialog #openCircDialog></eg-open-circ-dialog>
+
+
+
index 7b506ff..3808ca9 100644 (file)
@@ -4,6 +4,7 @@ import {PrecatCheckoutDialogComponent} from './precat-dialog.component';
 import {CircEventsComponent} from './events-dialog.component';
 import {StringComponent} from '@eg/share/string/string.component';
 import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+import {OpenCircDialogComponent} from './open-circ-dialog.component';
 
 /* Container component for sub-components used by circulation actions.
  *
@@ -21,6 +22,7 @@ export class CircComponentsComponent {
     @ViewChild('precatDialog') precatDialog: PrecatCheckoutDialogComponent;
     @ViewChild('circEventsDialog') circEventsDialog: CircEventsComponent;
     @ViewChild('routeToCatalogingDialog') routeToCatalogingDialog: AlertDialogComponent;
+    @ViewChild('openCircDialog') openCircDialog: OpenCircDialogComponent;
 
     constructor(private circ: CircService) {
         this.circ.components = this;
diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/open-circ-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/share/circ/open-circ-dialog.component.html
new file mode 100644 (file)
index 0000000..c50d7c3
--- /dev/null
@@ -0,0 +1,46 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 class="modal-title">
+      <span i18n>Open Circulation</span>
+    </h4>
+    <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">
+    <h5 class="font-weight-bold" i18n>
+      There is an open circulation on the requested item.
+    </h5>
+
+    <div class="mt-2" *ngIf="sameUser">
+      This item was already checked out to this user on {{circDate | date:'short'}}
+    </div>
+    <div class="mt-2" *ngIf="!sameUser">
+      This item was checked out by another patron on {{circDate | date:'short'}}
+    </div>
+  </div>
+  <div class="modal-footer d-flex">
+
+    <div class="form-check form-check-inline">
+      <input class="form-check-input" type="checkbox" id="forgive-fines-cbox" 
+        [(ngModel)]="forgiveFines"/>
+      <label class="form-check-label" for="forgive-fines-cbox" i18n>Forgive Fines?</label>
+    </div>
+
+    <div class="flex-1"></div>
+
+    <div>
+      <button type="button" class="btn btn-success" *ngIf="!sameUser"
+        (click)="close({forgiveFines: forgiveFines})" i18n>
+        Normal Checkin Then Checkout</button>
+
+      <button type="button" class="btn btn-success" *ngIf="sameUser"
+        (click)="close({forgiveFines: forgiveFines})" i18n>Renew</button>
+
+      <button type="button" class="btn btn-warning ml-2"
+        (click)="close()" i18n>Cancel</button>
+    </div>
+
+  </div>
+</ng-template>
diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/open-circ-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/share/circ/open-circ-dialog.component.ts
new file mode 100644 (file)
index 0000000..4960473
--- /dev/null
@@ -0,0 +1,39 @@
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {Observable} from 'rxjs';
+import {IdlObject} from '@eg/core/idl.service';
+import {NetService} from '@eg/core/net.service';
+import {EventService} from '@eg/core/event.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {AuthService} from '@eg/core/auth.service';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {StringComponent} from '@eg/share/string/string.component';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+/* Dialog for alerting of an existing open circulation */
+
+@Component({
+  selector: 'eg-open-circ-dialog',
+  templateUrl: 'open-circ-dialog.component.html'
+})
+
+export class OpenCircDialogComponent
+    extends DialogComponent implements OnInit {
+
+    @Input() sameUser: boolean;
+    @Input() circDate: string; // iso
+    forgiveFines = false;
+
+    constructor(
+        private modal: NgbModal, // required for passing to parent
+        private toast: ToastService,
+        private net: NetService,
+        private evt: EventService,
+        private pcrud: PcrudService,
+        private auth: AuthService) {
+        super(modal); // required for subclassing
+    }
+
+    ngOnInit() {}
+}
index 6af395d..3bf2c14 100644 (file)
@@ -5352,7 +5352,9 @@ __PACKAGE__->register_method(
             display in the user bills UI.  API is natively "authoritative"./,
         params => [
             {desc => 'Authentication token', type => 'string'},
-            {desc => 'User ID', type => 'number'}
+            {desc => 'User ID', type => 'number'},
+            {desc => 'Xact IDs.  Optionally limit to specific transactions', 
+             type => 'array'}
         ],
         return => {
             desc => q/First response is the user money summary, following
@@ -5362,7 +5364,7 @@ __PACKAGE__->register_method(
 );
 
 sub user_billing_xacts {
-    my ($self, $client, $auth, $user_id) = @_;
+    my ($self, $client, $auth, $user_id, $xact_ids) = @_;
 
     my $e = new_editor(authtoken => $auth, xact => 1);
     return $e->die_event unless $e->checkauth;
@@ -5375,12 +5377,15 @@ sub user_billing_xacts {
     # Start with the user summary.
     $client->respond($e->retrieve_money_user_summary($user_id));
 
-    my $xact_ids = $e->json_query({
+    # Even if xact_ids are specified, run this query to confirm the
+    # provided IDs are linked to the specified user and have a balance.
+    $xact_ids = $e->json_query({
         select => {mbts => ['id']},
         from => 'mbts',
         where => {
             usr => $user_id,
-            balance_owed => {'<>' => 0}
+            balance_owed => {'<>' => 0},
+            $xact_ids ? (id => $xact_ids) : ()
         },
         order_by => {mbts => {xact_start => 'asc'}}
     });