<!-- BILLS GRID -->
<ng-template #titleTemplate let-r="row">
- <ng-container *ngIf="r.record">
- <a routerLink="/staff/catalog/record/{{r.record.id()}}">{{r.title}}</a>
+ <ng-container *ngIf="r.record_id">
+ <a routerLink="/staff/catalog/record/{{r.record_id}}">{{r.title}}</a>
</ng-container>
<ng-container *ngIf="!r.record">{{r.title}}</ng-container>
</ng-template>
<ng-template #barcodeTemplate let-r="row">
- <ng-container *ngIf="r.copy">
- <a href="/eg/staff/cat/item/{{r.copy.id()}}">{{r.copy.barcode()}}</a>
+ <ng-container *ngIf="r.copy_id">
+ <a href="/eg/staff/cat/item/{{r.copy_id}}">{{r.copy_barcode}}</a>
</ng-container>
</ng-template>
-<ng-template #callNumberTemplate let-r="row">
- <ng-container *ngIf="r.volume">
- {{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 idlClass="mbt" [dataSource]="gridDataSource"
persistKey="circ.patron.bills" [sortable]="true"
- [useLocalSort]="true" (onRowActivate)="showStatement($event)"
+ (onRowActivate)="showStatement($event)"
+ [reloadOnColumnChange]="true"
+ [showDeclaredFieldsOnly]="true"
[cellTextGenerator]="cellTextGenerator">
<eg-grid-toolbar-button i18n-label label="Add Billing"
<!-- COLUMNS -->
- <eg-grid-column path="xact.id" [index]="true" label="Bill #" i18n-label>
- </eg-grid-column>
+ <eg-grid-column path="id" [index]="true" [required]="true"
+ label="Bill #" i18n-label> </eg-grid-column>
- <eg-grid-column path="xact.xact_start" datatype="timestamp" [datePlusTime]="true"
+ <eg-grid-column path="xact_start" datatype="timestamp" [datePlusTime]="true"
label="Start" i18n-label></eg-grid-column>
- <eg-grid-column path="xact.summary.xact_type"
+ <eg-grid-column path="summary.xact_type"
label="Type" i18n-label></eg-grid-column>
- <eg-grid-column path="xact.summary.last_billing_type"
+ <eg-grid-column path="summary.last_billing_type"
label="Last Billing Type" i18n-label></eg-grid-column>
- <eg-grid-column path="billingLocation"
+ <eg-grid-column name="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="call_number_label"
+ path="circulation.target_copy.call_number.label"
+ i18n-label label="Call Number" [hidden]="true"></eg-grid-column>
+
+ <eg-grid-column path="circulation.target_copy.call_number.prefix.label"
+ i18n-label label="CN Prefix" [hidden]="true"></eg-grid-column>
+ <eg-grid-column path="circulation.target_copy.call_number.suffix.label"
+ i18n-label label="CN Suffix" [hidden]="true"></eg-grid-column>
<eg-grid-column name="copy_barcode" label="Item Barcode" i18n-label
- [cellTemplate]="barcodeTemplate"></eg-grid-column>
+ path="circulation.target_copy.barcode" [cellTemplate]="barcodeTemplate">
+ </eg-grid-column>
- <eg-grid-column path="copy.location.name" label="Shelving Location"
- i18n-label></eg-grid-column>
+ <eg-grid-column name="copy_id" path="circulation.target_copy.id"
+ [required]="true" [hidden]="true"></eg-grid-column>
- <eg-grid-column name="title" label="Title" i18n-label
- [cellTemplate]="titleTemplate"></eg-grid-column>
+ <eg-grid-column path="circulation.target_copy.location.name"
+ label="Shelving Location" i18n-label></eg-grid-column>
- <eg-grid-column path="xact.summary.balance_owed" datatype="money"
- label="Balance Owed" i18n-label></eg-grid-column>
+ <eg-grid-column i18n-label label="Title" name="title"
+ [cellTemplate]="titleTemplate"
+ path="circulation.target_copy.call_number.record.simple_record.title">
+ </eg-grid-column>
+
+ <eg-grid-column name="record_id"
+ path="circulation.target_copy.call_number.record.id"
+ [required]="true" [hidden]="true"></eg-grid-column>
+
+ <eg-grid-column path="summary.balance_owed" datatype="money"
+ [required]="true" label="Balance Owed" i18n-label></eg-grid-column>
- <eg-grid-column path="xact.summary.total_owed" datatype="money"
+ <eg-grid-column path="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="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"
- label="Author" i18n-label></eg-grid-column>
+ <eg-grid-column [hidden]="true" label="Author" i18n-label
+ path="circulation.target_copy.call_number.record.simple_record.author">
+ </eg-grid-column>
+
<eg-grid-column path="usr" [hidden]="true"></eg-grid-column>
<eg-grid-column path="unrecovered" [hidden]="true"></eg-grid-column>
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 {PatronContextService} 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 {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';
+import {GridFlatDataService} from '@eg/share/grid/grid-flat-data.service';
@Component({
templateUrl: 'bills.component.html',
paymentAmount: number;
annotatePayment = false;
paymentNote: string;
- entries: BillGridEntry[];
convertChangeToCredit = false;
receiptOnPayment = false;
applyingPayment = false;
private serverStore: ServerStoreService,
private circ: CircService,
private billing: BillingService,
+ private flatData: GridFlatDataService,
public patronService: PatronService,
public context: PatronContextService
) {}
this.cellTextGenerator = {
title: row => row.title,
- copy_barcode: row => row.copy ? row.copy.barcode() : '',
- call_number: row => row.volume ? row.volume.label() : ''
+ copy_barcode: row => row.copy_barcode,
+ call_number: row => row.call_number_label
};
- // The grid never fetches data directly, it only serves what
- // we have manually retrieved.
this.gridDataSource.getRows = (pager: Pager, sort: any[]) => {
- if (!this.entries) { return empty(); }
- const page =
- this.entries.slice(pager.offset, pager.offset + pager.limit)
- .filter(entry => entry !== undefined);
+ const query: any = {
+ usr: this.patronId,
+ xact_finish: null,
+ 'summary.balance_owed' : {'<>' : 0}
+ };
- return from(page);
+ return this.flatData.getRows(
+ this.billGrid.context, query, pager, sort)
+ .pipe(tap(row => {
+ row.paymentPending = 0;
+ }));
};
- this.loadSettings().then(_ => this.load());
+ this.pcrud.retrieve('mowbus', this.patronId).toPromise()
+ // Summary will be null for users with no billing history.
+ .then(summary => this.summary = summary || this.idl.create('mowbus'))
+ .then(_ => this.loadSettings());
}
loadSettings(): 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> {
-
- const entriesFetched: number[] = [];
- this.gridDataSource.requestingData = true;
-
- if (!refreshXacts) { this.entries = []; }
-
- // Could nullify summary, but that causes a minor screen
- // flicker as the new data loads.
- let first = true;
-
- return this.net.request(
- 'open-ils.actor',
- 'open-ils.actor.user.transactions.for_billing',
- this.auth.token(), this.patronId,
- {have_balance: true, xact_ids: refreshXacts}
-
- ).pipe(tap(resp => {
-
- if (first) { // 1st response is summary
- this.summary = resp;
- first = false;
- return;
- }
-
- if (!refreshXacts) {
- this.entries.push(this.context.formatXactForDisplay(resp));
- return;
- }
-
- entriesFetched.push(resp.id());
-
- let idx;
- for (idx = 0; 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.context.formatXactForDisplay(resp);
- } else {
- // Adding a new transaction (e.g. from new billing)
- this.entries.push(this.context.formatXactForDisplay(resp));
- }
-
- })).toPromise()
-
- .then(_ => {
-
- if (!this.summary) {
- // If the patron has no billing history, there will be
- // no money summary.
- this.summary = this.idl.create('mus');
- }
-
- if (!refreshXacts) { return; }
-
- // Refreshing means some transactions may be removed from the list
- // Remove them from the local entries array.
- refreshXacts.forEach(xactId => {
- if (entriesFetched.includes(xactId)) { return; }
-
- let idx;
- for (idx = 0; idx < this.entries.length; idx++) {
- const entry = this.entries[idx];
- if (entry.xact.id() === xactId) { break; }
- }
-
- this.billGrid.context.rowSelector.deselect(xactId + '');
- this.entries.splice(idx, 1);
- });
- })
-
- .then(_ => {
- this.gridDataSource.requestingData = false;
- if (refreshXacts) { this.context.refreshPatron(); }
- this.billGrid.reload();
- });
- }
-
patron(): IdlObject {
return this.context.summary ? this.context.summary.patron : null;
}
selectedPaymentInfo(): {owed: number, billed: number, paid: number} {
const info = {owed : 0, billed : 0, paid : 0};
+ if (!this.billGrid) { return info; } // page loading
+
this.billGrid.context.rowSelector.selected().forEach(id => {
const row = this.billGrid.context.getRowByIndex(id);
- const sum = row.xact.summary();
- info.owed += Number(sum.balance_owed()) * 100;
- info.billed += Number(sum.total_owed()) * 100;
- info.paid += Number(sum.total_paid()) * 100;
+ if (!row) { return; } // Called mid-reload
+
+ info.owed += Number(row['summary.balance_owed']) * 100;
+ info.billed += Number(row['summary.total_owed']) * 100;
+ info.paid += Number(row['summary.total_paid']) * 100;
});
info.owed /= 100;
return info;
}
+
pendingPaymentInfo(): {payment: number, change: number} {
const amt = this.paymentAmount || 0;
refundsAvailable(): number {
let amount = 0;
this.gridDataSource.data.forEach(row => {
- const balance = row.xact.summary().balance_owed();
+ const balance = row['summary.balance_owed'];
if (balance < 0) { amount += balance * 100; }
});
.then(paymentIds => this.handlePayReceipt(payments, paymentIds))
// refresh affected xact IDs
- .then(_ => this.load(payments.map(p => p[0])))
+ .then(_ => this.billGrid.reload())
.then(_ => {
this.paymentAmount = null;
compilePayments(): Array<Array<number>> { // [ [xactId, payAmount], ... ]
const payments = [];
- this.entries.forEach(row => {
+ this.gridDataSource.data.forEach(row => {
if (row.paymentPending) {
- payments.push([row.xact.id(), row.paymentPending]);
+ payments.push([row.id, row.paymentPending]);
}
});
return payments;
updatePendingColumn() {
// Reset...
- this.entries.forEach(row => row.paymentPending = 0);
+ this.gridDataSource.data.forEach(row => row.paymentPending = 0);
let amount = this.pendingPayment();
let done = false;
if (done) { return; }
const row = this.billGrid.context.getRowByIndex(index);
- const owed = Number(row.xact.summary().balance_owed());
+ const owed = Number(row['summary.balance_owed']);
if (amount > owed) {
// Pending payment amount exceeds balance of this
});
}
- printBills(rows: BillGridEntry[]) {
+ printBills(rows: any[]) {
if (rows.length === 0) { return; }
this.printer.print({
templateName: 'bills_current',
- contextData: {xacts: rows.map(r => r.xact)},
+ contextData: {xacts: rows},
printContext: 'default'
});
}
};
payments.forEach(payment => {
+
const entry =
- this.entries.filter(e => e.xact.id() === payment[0])[0];
+ this.gridDataSource.data.filter(e => e.xact.id() === payment[0])[0];
context.payments.push({
amount: payment[1],
- xact: entry.xact,
+ xact: entry,
title: entry.title,
- copy_barcode: entry.copy ? entry.copy.barcode() : ''
+ copy_barcode: entry.copy_barcode
});
});
selectRefunds() {
this.billGrid.context.rowSelector.clear();
- this.entries.forEach(entry => {
- if (entry.xact.summary().balance_owed() < 0) {
- this.billGrid.context.toggleSelectOneRow(entry.xact.id());
+ this.gridDataSource.data.forEach(row => {
+ if (row['summary.balance_owed'] < 0) {
+ this.billGrid.context.toggleSelectOneRow(row.id);
}
});
}
this.billingDialog.newXact = true;
this.billingDialog.open().subscribe(data => {
if (data) {
- this.load([data.xactId]);
+ this.billGrid.reload();
}
});
}
- addBillingForXact(rows: BillGridEntry[]) {
+ addBillingForXact(rows: any[]) {
if (rows.length === 0) { return; }
- const xactIds = rows.map(r => r.xact.id());
+ const xactIds = rows.map(r => r.id);
this.billingDialog.newXact = false;
const xactsChanged = [];
}))
.subscribe(null, null, () => {
if (xactsChanged.length > 0) {
- this.load(xactsChanged);
+ this.billGrid.reload();
}
});
}
- voidBillings(rows: BillGridEntry[]) {
+ voidBillings(rows: any[]) {
if (rows.length === 0) { return; }
- const xactIds = rows.map(r => r.xact.id());
+ const xactIds = rows.map(r => r.id);
const billIds = [];
let cents = 0;
this.sessionVoided = (this.sessionVoided * 100 + cents) / 100;
this.voidAmount = 0;
- this.load(xactIds);
+ this.billGrid.reload();
});
}
- adjustToZero(rows: BillGridEntry[]) {
+ adjustToZero(rows: any[]) {
if (rows.length === 0) { return; }
- const xactIds = rows.map(r => r.xact.id());
+ const xactIds = rows.map(r => r.id);
this.audio.play('warning.circ.adjust_to_zero_confirmation');
'open-ils.circ.money.billable_xact.adjust_to_zero',
this.auth.token(), xactIds
).subscribe(resp => {
- if (!this.reportError(resp)) { this.load(xactIds); }
+ if (!this.reportError(resp)) { this.billGrid.reload(); }
});
});
}
// This is functionally equivalent to selecting a neg. transaction
// then clicking Apply Payment -- this just adds a speed bump (ditto
// the XUL client).
- refund(rows: BillGridEntry[]) {
+ refund(rows: any[]) {
if (rows.length === 0) { return; }
- const xactIds = rows.map(r => r.xact.id());
+ const xactIds = rows.map(r => r.id);
this.refundDialog.open().subscribe(confirmed => {
if (!confirmed) { return; }
});
}
- showStatement(row: BillGridEntry) {
+ showStatement(row: any) {
this.router.navigate(['/staff/circ/patron',
- this.patronId, 'bills', row.xact.id(), 'statement']);
+ this.patronId, 'bills', row.id, 'statement']);
}
}
}
}
-
-__PACKAGE__->register_method(
- method => 'user_billing_xacts',
- api_name => 'open-ils.actor.user.transactions.for_billing',
- signature => {
- desc => q/Returns a stream of user billing data appropriate for
- display in the user bills UI. API is natively "authoritative"./,
- 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'}
- ],
- return => {
- desc => q/First response is the user money summary, following
- responses are fleshed billable transactions/
- }
- }
-);
-
-sub user_billing_xacts {
- 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;
-
- my $user = $e->retrieve_actor_user($user_id) or return $e->die_event;
-
- return $e->die_event unless
- $e->allowed('VIEW_USER_TRANSACTIONS', $user->home_ou);
-
- # 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({
- select => {mbts => ['id']},
- from => 'mbts',
- where => {
- usr => $user_id,
- balance_owed => {'<>' => 0},
- $xact_ids ? (id => $xact_ids) : ()
- },
- order_by => {mbts => {xact_start => 'asc'}}
- });
-
- for my $xact_id (map { $_->{id} } @$xact_ids) {
-
- my $xact = $e->retrieve_money_billable_transaction([
- $xact_id, {
- flesh => 5,
- flesh_fields => {
- mbt => [qw/summary circulation grocery/],
- circ => [qw/
- target_copy
- workstation
- checkin_workstation
- /],
- acp => [qw/
- call_number
- holds_count
- status
- circ_lib
- location
- floating
- age_protect
- parts
- /],
- acpm => [qw/part/],
- acn => [qw/record owning_lib prefix suffix/],
- bre => [qw/wide_display_entry/]
- },
- # Avoid adding the MARXML
- # Fleshed fields are implicitly included.
- select => {bre => ['id']}
- }
- ]);
-
- $client->respond($xact);
- }
-
- $e->rollback;
-
- return undef;
-}
-
1;