LP1904036 billing history
authorBill Erickson <berickxx@gmail.com>
Fri, 16 Apr 2021 16:44:10 +0000 (12:44 -0400)
committerGalen Charlton <gmc@equinoxOLI.org>
Fri, 28 Oct 2022 00:13:31 +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/billing-history.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/circ/patron/billing-history.component.ts [new file with mode: 0644]
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/patron.component.html
Open-ILS/src/eg2/src/app/staff/circ/patron/patron.component.ts
Open-ILS/src/eg2/src/app/staff/circ/patron/patron.module.ts
Open-ILS/src/eg2/src/app/staff/circ/patron/routing.module.ts
Open-ILS/src/perlmods/lib/OpenILS/Application/Actor.pm
Open-ILS/src/templates/staff/circ/patron/t_bills_list.tt2

diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/billing-history.component.html b/Open-ILS/src/eg2/src/app/staff/circ/patron/billing-history.component.html
new file mode 100644 (file)
index 0000000..d716636
--- /dev/null
@@ -0,0 +1,41 @@
+<h3 i18n>Bill History</h3>
+
+<eg-add-billing-dialog [patronId]="patronId" #billingDialog>
+</eg-add-billing-dialog>
+
+
+<ul ngbNav #nav="ngbNav" class="nav-tabs" [(activeId)]="tab">
+  <li ngbNavItem="transactions">
+    <a ngbNavLink i18n>Transactions</a>
+    <ng-template ngbNavContent>
+      <eg-grid idlClass="mbt" #xactsGrid
+        persistKey="circ.patron.billhistory_xacts"
+        (onRowActivate)="showStatement($event)"
+        i18n-toolbarLabel [dataSource]="xactsDataSource" [sortable]="true">
+
+        <eg-grid-toolbar-button i18n-label label="Add Billing"
+          (onClick)="addBilling()"></eg-grid-toolbar-button>
+
+        <eg-grid-toolbar-action
+          i18n-label label="Print Bills" (onClick)="printBills($event)">
+        </eg-grid-toolbar-action>
+
+
+        <eg-grid-toolbar-action label="Edit Note" i18n-label
+          (onClick)="openNoteDialog($event)">
+        </eg-grid-toolbar-action>
+        <eg-grid-toolbar-action label="Void Billings" i18n-label
+          (onClick)="openVoidDialog($event)">
+        </eg-grid-toolbar-action>
+      </eg-grid>
+    </ng-template>
+  </li>
+  <li ngbNavItem="payments">
+    <a ngbNavLink i18n>Payments</a>
+    <ng-template ngbNavContent>
+    </ng-template>
+  </li>
+</ul>
+
+<ng-container><div [ngbNavOutlet]="nav"></div></ng-container>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/circ/patron/billing-history.component.ts b/Open-ILS/src/eg2/src/app/staff/circ/patron/billing-history.component.ts
new file mode 100644 (file)
index 0000000..751140a
--- /dev/null
@@ -0,0 +1,138 @@
+import {Component, Input, OnInit, AfterViewInit, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {from, empty, range} from 'rxjs';
+import {concatMap, tap, takeLast} from 'rxjs/operators';
+import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+import {IdlObject, IdlService} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {OrgService} from '@eg/core/org.service';
+import {NetService} from '@eg/core/net.service';
+import {PcrudService, PcrudContext} from '@eg/core/pcrud.service';
+import {AuthService} from '@eg/core/auth.service';
+import {ServerStoreService} from '@eg/core/server-store.service';
+import {PatronService} from '@eg/staff/share/patron/patron.service';
+import {PatronContextService, BillGridEntry} from './patron.service';
+import {GridDataSource, GridColumn, GridCellTextGenerator} from '@eg/share/grid/grid';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {Pager} from '@eg/share/util/pager';
+import {CircService, CircDisplayInfo} from '@eg/staff/share/circ/circ.service';
+import {PrintService} from '@eg/share/print/print.service';
+import {PromptDialogComponent} from '@eg/share/dialog/prompt.component';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {BillingService} from '@eg/staff/share/billing/billing.service';
+import {AddBillingDialogComponent} from '@eg/staff/share/billing/billing-dialog.component';
+import {AudioService} from '@eg/share/util/audio.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+
+@Component({
+  templateUrl: 'billing-history.component.html',
+  selector: 'eg-patron-billing-history'
+})
+export class BillingHistoryComponent implements OnInit {
+
+    @Input() patronId: number;
+    @Input() tab: string;
+
+    xactsDataSource: GridDataSource = new GridDataSource();
+    paymentsDataSource: GridDataSource = new GridDataSource();
+
+    xactsTextGenerator: GridCellTextGenerator;
+    paymentsTextGenerator: GridCellTextGenerator;
+
+    @ViewChild('xactsGrid') private xactsGrid: GridComponent;
+    @ViewChild('paymentsGrid') private paymentsGrid: GridComponent;
+    @ViewChild('billingDialog') private billingDialog: AddBillingDialogComponent;
+
+    constructor(
+        private router: Router,
+        private route: ActivatedRoute,
+        private org: OrgService,
+        private evt: EventService,
+        private net: NetService,
+        private pcrud: PcrudService,
+        private auth: AuthService,
+        private idl: IdlService,
+        private circ: CircService,
+        private billing: BillingService,
+        private printer: PrintService,
+        public patronService: PatronService,
+        public context: PatronContextService
+    ) {}
+
+    ngOnInit() {
+
+        this.xactsDataSource.getRows = (pager: Pager, sort: any[]) => {
+            const orderBy: any = {};
+            if (sort.length) {
+                orderBy.mb = sort[0].name + ' ' + sort[0].dir;
+            }
+
+            return this.pcrud.search('mbt', {usr: this.patronId}, {
+                order_by: orderBy,
+                join: {
+                    mbts: {
+                        filter: {
+                            '-or': [
+                                {balance_owed: {'<>': 0}},
+                                {last_payment_ts: {'<>': null}}
+                            ]
+                        }
+                    }
+                }
+            });
+        };
+
+        /*
+        this.paymentsDataSource.getRows = (pager: Pager, sort: any[]) => {
+            const orderBy: any = {};
+            if (sort.length) {
+                orderBy.mp = sort[0].name + ' ' + sort[0].dir;
+            }
+            return this.pcrud.search(
+                'mp', {xact: this.xactId}, {order_by: orderBy});
+        };
+        */
+    }
+
+    showStatement(row: BillGridEntry) {
+        this.router.navigate(['/staff/circ/patron',
+            this.patronId, 'bills', row.xact.id(), 'statement']);
+    }
+
+    addBillingForXact(rows: BillGridEntry[]) {
+        if (rows.length === 0) { return; }
+        const xactIds = rows.map(r => r.xact.id());
+
+        this.billingDialog.newXact = false;
+        let changesApplied = false;
+
+        from(xactIds)
+        .pipe(concatMap(id => {
+            this.billingDialog.xactId = id;
+            return this.billingDialog.open();
+        }))
+        .pipe(tap(data => {
+            if (data) {
+                changesApplied = true;
+            }
+        }))
+        .subscribe(null, null, () => {
+            if (changesApplied) {
+                this.xactsGrid.reload();
+            }
+        });
+    }
+
+    printBills(rows: BillGridEntry[]) {
+        if (rows.length === 0) { return; }
+
+        this.printer.print({
+            templateName: 'bills_historical',
+            contextData: {xacts: rows.map(r => r.xact)},
+            printContext: 'default'
+        });
+    }
+}
+
+
index 9b4b07d..6e790ea 100644 (file)
   <eg-grid-toolbar-button i18n-label label="Add Billing"
     (onClick)="addBilling()"></eg-grid-toolbar-button>
 
-  <!-- ACTIONS FOR SELECTED -->
+  <eg-grid-toolbar-button i18n-label label="History"
+    routerLink="/staff/circ/patron/{{patronId}}/bills/history/transactions">
+  </eg-grid-toolbar-button>
 
   <eg-grid-toolbar-button i18n-label label="Select All Refunds"
     (onClick)="selectRefunds()"></eg-grid-toolbar-button>
 
+  <!-- ACTIONS FOR SELECTED -->
+
   <eg-grid-toolbar-action
     i18n-label label="Print Bills" (onClick)="printBills($event)">
   </eg-grid-toolbar-action>
index 0705c0a..035ffc0 100644 (file)
@@ -167,7 +167,8 @@ export class BillsComponent implements OnInit, AfterViewInit {
         return this.net.request(
             'open-ils.actor',
             'open-ils.actor.user.transactions.for_billing',
-            this.auth.token(), this.patronId, refreshXacts
+            this.auth.token(), this.patronId,
+            {have_balance: true, xact_ids: refreshXacts}
 
         ).pipe(tap(resp => {
 
index dba472e..61655c0 100644 (file)
               <eg-patron-bill-statement [patronId]="patronId" [xactId]="statementXact">
               </eg-patron-bill-statement>
             </ng-container>
-            <ng-container *ngIf="!statementXact">
+            <ng-container *ngIf="billingHistoryTab">
+              <eg-patron-billing-history [patronId]="patronId" [tab]="billingHistoryTab">
+              </eg-patron-billing-history>
+            </ng-container>
+            <ng-container *ngIf="!statementXact && !billingHistoryTab">
               <eg-patron-bills [patronId]="patronId"></eg-patron-bills>
             </ng-container>
           </ng-template>
index d87bac0..b48e106 100644 (file)
@@ -25,6 +25,7 @@ export class PatronComponent implements OnInit, AfterViewInit {
     patronTab = 'search';
     altTab: string;
     statementXact: number;
+    billingHistoryTab: string;
     showSummary = true;
     loading = true;
 
@@ -92,10 +93,12 @@ export class PatronComponent implements OnInit, AfterViewInit {
     }
 
     watchForTabChange() {
+
         this.route.paramMap.subscribe((params: ParamMap) => {
             this.patronTab = params.get('tab') || 'search';
             this.patronId = +params.get('id');
             this.statementXact = +params.get('xactId');
+            this.billingHistoryTab = params.get('billingHistoryTab');
 
             if (MAIN_TABS.includes(this.patronTab)) {
                 this.altTab = null;
index 4997a54..0417d3e 100644 (file)
@@ -31,6 +31,7 @@ import {PatronBarcodesDialogComponent} from './barcodes.component';
 import {HoldNotifyUpdateDialogComponent} from './hold-notify-update.component';
 import {PatronMessagesComponent} from './messages.component';
 import {PatronPermsComponent} from './perms.component';
+import {BillingHistoryComponent} from './billing-history.component';
 
 @NgModule({
   declarations: [
@@ -44,6 +45,7 @@ import {PatronPermsComponent} from './perms.component';
     ItemsComponent,
     BillsComponent,
     BillStatementComponent,
+    BillingHistoryComponent,
     TestPatronPasswordComponent,
     PatronMessagesComponent,
     PatronSurveyResponsesComponent,
index 728b86b..4e4e142 100644 (file)
@@ -48,6 +48,10 @@ const routes: Routes = [{
     component: PatronComponent,
     resolve: {resolver : PatronResolver}
   }, {
+    path: ':id/:tab/history/:billingHistoryTab',
+    component: PatronComponent,
+    resolve: {resolver : PatronResolver}
+  }, {
     path: ':id/:tab',
     component: PatronComponent,
     resolve: {resolver : PatronResolver},
index 5db789a..c591ad8 100644 (file)
@@ -5359,6 +5359,11 @@ __PACKAGE__->register_method(
         params => [
             {desc => 'Authentication token', type => 'string'},
             {desc => 'User ID', type => 'number'},
+            {desc => q/Options: {
+                xact_ids: load specific transactions
+                have_balance:
+                have_charge:
+            }/, type => 'object'},
             {desc => 'Xact IDs.  Optionally limit to specific transactions', 
              type => 'array'}
         ],
@@ -5370,7 +5375,13 @@ __PACKAGE__->register_method(
 );
 
 sub user_billing_xacts {
-    my ($self, $client, $auth, $user_id, $xact_ids) = @_;
+    my ($self, $client, $auth, $user_id, $options) = @_;
+
+    $options ||= {};
+    my $xact_ids = $options->{xact_ids};
+    my $have_balance = $options->{have_balance};
+    my $have_charge = $options->{have_charge};
+    my $have_payment = $options->{have_payment};
 
     my $e = new_editor(authtoken => $auth, xact => 1);
     return $e->die_event unless $e->checkauth;
@@ -5383,6 +5394,12 @@ sub user_billing_xacts {
     # Start with the user summary.
     $client->respond($e->retrieve_money_open_with_balance_user_summary($user_id));
 
+    my $where = {};
+    if ($xact_ids) { $where->{id} = $xact_ids; }
+    if ($have_balance) { $where->{balance_owed} = {'<>' => 0}; }
+    if ($have_charge) { $where->{last_billing_ts} = {'<>' => undef}; }
+    if ($have_payment) { $where->{last_payment_ts} = {'<>' => undef}; }
+
     # 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({
index ccd7320..e26a968 100644 (file)
@@ -50,7 +50,9 @@
   <eg-grid-action label="[% l('Full Details') %]" 
     handler="showFullDetails"></eg-grid-action>
 
+  <!--
   <eg-grid-field label="[% l('Balance Owed') %]" path='summary.balance_owed'></eg-grid-field>
+  -->
   <eg-grid-field required label="[% l('Bill #') %]" path='id'></eg-grid-field>
   <eg-grid-field label="[% l('Start') %]" path='xact_start' datatype="timestamp"></eg-grid-field>
   <eg-grid-field label="[% l('Total Billed') %]" path='summary.total_owed'></eg-grid-field>