From 9be06d2e27dcb3dd63041104ac68ba4b53d6ba84 Mon Sep 17 00:00:00 2001 From: Bill Erickson Date: Tue, 9 Mar 2021 10:14:01 -0500 Subject: [PATCH] LP1904036 open circ exists dialog; billing Signed-off-by: Bill Erickson Signed-off-by: Jane Sandberg Signed-off-by: Galen Charlton --- .../src/app/staff/circ/patron/bills.component.ts | 60 ++++++++++---------- .../app/staff/circ/patron/checkout.component.ts | 1 + .../eg2/src/app/staff/share/circ/circ.module.ts | 4 +- .../eg2/src/app/staff/share/circ/circ.service.ts | 65 +++++++++++++++++++++- .../app/staff/share/circ/components.component.html | 6 ++ .../app/staff/share/circ/components.component.ts | 2 + .../share/circ/open-circ-dialog.component.html | 46 +++++++++++++++ .../staff/share/circ/open-circ-dialog.component.ts | 39 +++++++++++++ .../src/perlmods/lib/OpenILS/Application/Actor.pm | 13 +++-- 9 files changed, 199 insertions(+), 37 deletions(-) create mode 100644 Open-ILS/src/eg2/src/app/staff/share/circ/open-circ-dialog.component.html create mode 100644 Open-ILS/src/eg2/src/app/staff/share/circ/open-circ-dialog.component.ts diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/bills.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/bills.component.ts index 74ba8ae3f1..5b4a7e0c2f 100644 --- a/Open-ILS/src/eg2/src/app/staff/circ/patron/bills.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/bills.component.ts @@ -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 { + // In refresh mode, only fetch the requested xacts, with updated user + // summary, and slot them back into the entries array. + load(refreshXacts?: number[]): Promise { + 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 { + handlePayReceipt(payments: Array>, paymentIds: number[]): Promise { if (this.disableAutoPrint || !this.receiptOnPayment) { return Promise.resolve(); diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.ts index 569b21264f..ce696e52e2 100644 --- a/Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.ts @@ -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 { diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/circ.module.ts b/Open-ILS/src/eg2/src/app/staff/share/circ/circ.module.ts index 6dafdfd5f1..dc639e94b2 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/circ/circ.module.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/circ/circ.module.ts @@ -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, diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/circ.service.ts b/Open-ILS/src/eg2/src/app/staff/share/circ/circ.service.ts index f92007370d..b5dbcfd04e 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/circ/circ.service.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/circ/circ.service.ts @@ -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 { - 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 { + + 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 { const params = result.params; @@ -398,11 +457,11 @@ export class CircService { processCheckinResult( params: CheckinParams, response: any): Promise { - 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; diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/components.component.html b/Open-ILS/src/eg2/src/app/staff/share/circ/components.component.html index d29bc19ed7..9afc0ef393 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/circ/components.component.html +++ b/Open-ILS/src/eg2/src/app/staff/share/circ/components.component.html @@ -1,8 +1,14 @@ + + + + + + diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/components.component.ts b/Open-ILS/src/eg2/src/app/staff/share/circ/components.component.ts index 7b506ff929..3808ca9b76 100644 --- a/Open-ILS/src/eg2/src/app/staff/share/circ/components.component.ts +++ b/Open-ILS/src/eg2/src/app/staff/share/circ/components.component.ts @@ -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 index 0000000000..c50d7c3def --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/share/circ/open-circ-dialog.component.html @@ -0,0 +1,46 @@ + + + + + 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 index 0000000000..4960473b66 --- /dev/null +++ b/Open-ILS/src/eg2/src/app/staff/share/circ/open-circ-dialog.component.ts @@ -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() {} +} diff --git a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm index 6af395dc7f..3bf2c14c44 100644 --- a/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm +++ b/Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm @@ -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'}} }); -- 2.11.0