-->
<eg-link-table-link i18n-label label="Exchange Rates"
routerLink="/staff/admin/acq/exchange_rate"></eg-link-table-link>
- <eg-link-table-link i18n-label label="Fund Tags"
- routerLink="/staff/admin/acq/fund_tag"></eg-link-table-link>
- <eg-link-table-link i18n-label label="Funding Sources"
- url="/eg/staff/admin/acq/funding_source/list"></eg-link-table-link>
- <!-- TODO
- routerLink="/staff/admin/acq/funding_source"></eg-link-table-link>
- -->
- <!-- TODO fund admin page w/ year filter and rollover -->
- <eg-link-table-link i18n-label label="Funds"
- url="/eg/staff/admin/acq/fund/list"></eg-link-table-link>
- <!-- routerLink="/staff/admin/acq/fund" -->
+ <eg-link-table-link i18n-label label="Funds, Funding Sources, and Fund Tags"
+ routerLink="/staff/admin/acq/funds"></eg-link-table-link>
<eg-link-table-link i18n-label label="Invoice Item Types"
routerLink="/staff/admin/acq/invoice_item_type"></eg-link-table-link>
<eg-link-table-link i18n-label label="Invoice Payment Method"
--- /dev/null
+<eg-string #successString i18n-text text="Fund Update Succeeded"></eg-string>
+<eg-string #updateFailedString i18n-text text="Fund Update Failed"></eg-string>
+
+<ng-template #summaryField let-field="field" let-value="value">
+ <div class="col-2">
+ <label style="font-weight: bold" for="fund-{{field}}">{{idlDef.field_map[field].label}}</label>
+ </div>
+ <div class="col-2">
+ <span id="fund-{{field}}">
+ {{value}}
+ </span>
+ </div>
+</ng-template>
+<ng-template #dialogContent>
+ <div class="modal-header bg-info">
+ <h3 class="modal-title" i18n>Fund Details - {{fund?.name()}} ({{fund?.code()}} ({{fund?.year()}}) ({{fund?.org().shortname()}}))</h3>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="close()">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body">
+ <div class="row mt-3">
+ <div class="col-lg-3">
+ <button class="btn btn-primary"
+ (click)="allocateToFund()" i18n>Create Allocation</button>
+ <button class="btn btn-primary ml-1"
+ (click)="doTransfer()" i18n>Transfer Money</button>
+ </div>
+ <div class="col-lg-9 text-right pb-1">
+ <button class="btn btn-secondary btn-xs" [disabled]="activeTab == defaultTabType"
+ (click)="setDefaultTab()" i18n>Set Default View</button>
+ </div>
+ </div>
+ <ul ngbNav #fundDetailsNav="ngbNav" [(activeId)]="activeTab" class="nav-tabs">
+ <li [ngbNavItem]="'summary'" *ngIf="fund">
+ <a ngbNavLink i18n>Summary</a>
+ <ng-template ngbNavContent>
+ <div class="mt-2">
+ <div class="row">
+ <button class="btn btn-info ml-3" (click)="openEditDialog()" i18n>Edit Fund</button>
+ </div>
+ <div class="row">
+ <ng-container *ngTemplateOutlet="summaryField;context:{value:fund.code(),field:'code'}"></ng-container>
+ <ng-container *ngTemplateOutlet="summaryField;context:{value:fund.name(),field:'name'}"></ng-container>
+ </div>
+ <div class="row">
+ <ng-container *ngTemplateOutlet="summaryField;context:{value:fund.id(),field:'id'}"></ng-container>
+ <ng-container *ngTemplateOutlet="summaryField;context:{value:fund.year(),field:'year'}"></ng-container>
+ </div>
+ <div class="row">
+ <ng-container *ngTemplateOutlet="summaryField;context:{value:fund.org().shortname(),field:'org'}"></ng-container>
+ <ng-container *ngTemplateOutlet="summaryField;context:{value:fund.currency_type().code(),field:'currency_type'}"></ng-container>
+ </div>
+ <div class="row">
+ <ng-container *ngTemplateOutlet="summaryField;context:{value:formatCurrency(fund.combined_balance()?.amount()),field:'combined_balance'}"></ng-container>
+ <ng-container *ngTemplateOutlet="summaryField;context:{value:formatCurrency(fund.allocation_total()?.amount()),field:'allocation_total'}"></ng-container>
+ </div>
+ <div class="row">
+ <ng-container *ngTemplateOutlet="summaryField;context:{value:formatCurrency(fund.spent_balance()?.amount()),field:'spent_balance'}"></ng-container>
+ <ng-container *ngTemplateOutlet="summaryField;context:{value:formatCurrency(fund.debit_total()?.amount()),field:'debit_total'}"></ng-container>
+ </div>
+ <div class="row">
+ <ng-container *ngTemplateOutlet="summaryField;context:{value:formatCurrency(fund.spent_total()?.amount()),field:'spent_total'}"></ng-container>
+ <ng-container *ngTemplateOutlet="summaryField;context:{value:formatCurrency(fund.encumbrance_total()?.amount()),field:'encumbrance_total'}"></ng-container>
+ </div>
+ </div>
+ </ng-template>
+ </li>
+ <li [ngbNavItem]="'allocations'" *ngIf="fund">
+ <a ngbNavLink i18n>Allocations</a>
+ <ng-template ngbNavContent>
+ <div class="mt-2">
+ <eg-grid #fundDetailsAllocationsGrid idlClass="acqfa" [dataSource]="acqfaDataSource"
+ [sortable]="true" persistKey="acq.fund.fund_allocation"
+ [cellTextGenerator]="cellTextGenerator"
+ [filterable]="true" [stickyHeader]="true">
+
+ <ng-template #sourceTmpl let-row="row">
+ <a href="/eg2/staff/admin/acq/funds/funding_source/{{row.funding_source().id()}}" target="_blank">
+ {{row.funding_source().code()}} ({{getOrgShortname(row.funding_source().owner())}})
+ </a>
+ </ng-template>
+ <eg-grid-column path="funding_source" [cellTemplate]="sourceTmpl"></eg-grid-column>
+ <eg-grid-column path="amount"></eg-grid-column>
+ <eg-grid-column path="create_time"></eg-grid-column>
+ <eg-grid-column path="allocator"></eg-grid-column>
+ <eg-grid-column path="note"></eg-grid-column>
+ <eg-grid-column path="id" [hidden]="true"></eg-grid-column>
+ <eg-grid-column path="fund" [hidden]="true" [filterable]="false"></eg-grid-column>
+ </eg-grid>
+ </div>
+ </ng-template>
+ </li>
+ <li [ngbNavItem]="'transfers'" *ngIf="fund">
+ <a ngbNavLink i18n>Transfers</a>
+ <ng-template ngbNavContent>
+ <div class="mt-2">
+ <eg-grid #fundDetailsTransfersGrid idlClass="acqftr" [dataSource]="acqftrDataSource"
+ [sortable]="true" persistKey="acq.fund.fund_transfer"
+ [cellTextGenerator]="cellTextGenerator"
+ [filterable]="true" [stickyHeader]="true">
+
+ <ng-template #sourcefundTmpl let-row="row">
+ <ng-container *ngIf="row.src_fund().id() === fundId">
+ {{row.src_fund().code()}} ({{row.src_fund().year()}}) ({{getOrgShortname(row.src_fund().org())}})
+ </ng-container>
+ <ng-container *ngIf="row.src_fund().id() !== fundId">
+ <a href="/eg2/staff/admin/acq/funds/fund/{{row.src_fund().id()}}" target="_blank">
+ {{row.src_fund().code()}} ({{row.src_fund().year()}}) ({{getOrgShortname(row.src_fund().org())}})
+ </a>
+ </ng-container>
+ </ng-template>
+ <eg-grid-column path="src_fund" [cellTemplate]="sourcefundTmpl"></eg-grid-column>
+ <eg-grid-column path="src_amount"></eg-grid-column>
+ <ng-template #destfundTmpl let-row="row">
+ <ng-container *ngIf="row.dest_fund().id() === fundId">
+ {{row.dest_fund().code()}} ({{row.dest_fund().year()}}) ({{getOrgShortname(row.dest_fund().org())}})
+ </ng-container>
+ <ng-container *ngIf="row.dest_fund().id() !== fundId">
+ <a href="/eg2/staff/admin/acq/funds/fund/{{row.dest_fund().id()}}" target="_blank">
+ {{row.dest_fund().code()}} ({{row.dest_fund().year()}}) ({{getOrgShortname(row.dest_fund().org())}})
+ </a>
+ </ng-container>
+ </ng-template>
+ <eg-grid-column path="dest_fund" [cellTemplate]="destfundTmpl"></eg-grid-column>
+ <eg-grid-column path="dest_amount"></eg-grid-column>
+ <eg-grid-column path="transfer_time"></eg-grid-column>
+ <eg-grid-column path="transfer_user"></eg-grid-column>
+ <eg-grid-column path="note"></eg-grid-column>
+ <ng-template #fsCredTmpl let-row="row">
+ <a href="/eg2/staff/admin/acq/funds/funding_source/{{row.funding_source_credit().funding_source().id()}}" target="_blank">
+ {{row.funding_source_credit().funding_source().code()}} ({{getOrgShortname(row.funding_source_credit().funding_source().owner())}})
+ </a>
+ </ng-template>
+ <eg-grid-column path="funding_source_credit" [cellTemplate]="fsCredTmpl"></eg-grid-column>
+ <eg-grid-column path="id" [hidden]="true"></eg-grid-column>
+ </eg-grid>
+ </div>
+ </ng-template>
+ </li>
+ <li [ngbNavItem]="'debits'" *ngIf="fund">
+ <a ngbNavLink i18n>Debits</a>
+ <ng-template ngbNavContent>
+ <div class="mt-2">
+ <eg-grid #fundDetailsDebitsGrid idlClass="acqfdeb" [dataSource]="acqfdebDataSource"
+ [cellTextGenerator]="cellTextGenerator"
+ [sortable]="true" persistKey="acq.fund.fund_debit"
+ [filterable]="true" [stickyHeader]="true">
+
+ <eg-grid-column path="amount"></eg-grid-column>
+ <eg-grid-column path="encumbrance"></eg-grid-column>
+ <eg-grid-column path="debit_type"></eg-grid-column>
+ <eg-grid-column path="origin_amount"></eg-grid-column>
+ <eg-grid-column path="origin_currency_type"></eg-grid-column>
+ <eg-grid-column path="create_time"></eg-grid-column>
+ <ng-template #liTmpl let-row="row">
+ <a href="/eg/staff/acq/legacy/po/view/{{row.po_id}}?focus_li={{row.li_id}}" target="_blank">
+ {{row.li_id}}
+ </a>
+ </ng-template>
+ <eg-grid-column path="li_id" i18n-label label="Line Item" [cellTemplate]="liTmpl" [filterable]="false" [sortable]="false"></eg-grid-column>
+ <ng-template #poTmpl let-row="row">
+ <a href="/eg/staff/acq/legacy/po/view/{{row.po_id}}" target="_blank">
+ {{row.po_name}}
+ </a>
+ </ng-template>
+ <eg-grid-column path="po_id" i18n-label label="Purchase Order" [cellTemplate]="poTmpl" [filterable]="false" [sortable]="false"></eg-grid-column>
+ <ng-template #invTmpl let-row="row">
+ <a href="/eg/staff/acq/legacy/invoice/view/{{row.invoice_id}}" target="_blank">
+ {{row.vendor_invoice_id}}
+ </a>
+ </ng-template>
+ <eg-grid-column path="invoice_id" i18n-label label="Invoice" [cellTemplate]="invTmpl" [filterable]="false" [sortable]="false"></eg-grid-column>
+ <eg-grid-column path="invoice_entry" [hidden]="true"></eg-grid-column>
+ <eg-grid-column path="id" [hidden]="true"></eg-grid-column>
+ <eg-grid-column path="fund" [hidden]="true" [filterable]="false"></eg-grid-column>
+ </eg-grid>
+ </div>
+ </ng-template>
+ </li>
+ <li [ngbNavItem]="'tags'" *ngIf="fund">
+ <a ngbNavLink i18n>Tags</a>
+ <ng-template ngbNavContent>
+ <div class="mt-2">
+ <eg-fund-tags [fundId]="fundId" [fundOwner]="fund.org()"></eg-fund-tags>
+ </div>
+ </ng-template>
+ </li>
+ </ul>
+ <div [ngbNavOutlet]="fundDetailsNav"></div>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-warning"
+ (click)="close()" i18n>Close</button>
+ </div>
+</ng-template>
+
+<eg-fm-record-editor #editDialog idlClass="acqf"
+ [fieldOptions]="fieldOptions"
+ [fieldOrder]="fieldOrder"
+ [defaultNewRecord]="defaultNewRecord"
+ [preloadLinkedValues]="true"
+ [readonlyFields]="readonlyFields">
+</eg-fm-record-editor>
+
+<eg-fm-record-editor #allocateToFundDialog idlClass="acqfa">
+</eg-fm-record-editor>
+
+<eg-fund-transfer-dialog #transferDialog></eg-fund-transfer-dialog>
--- /dev/null
+import {Component, Input, ViewChild, TemplateRef, OnInit} from '@angular/core';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {FormatService} from '@eg/core/format.service';
+import {EventService} from '@eg/core/event.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {StoreService} from '@eg/core/store.service';
+import {OrgService} from '@eg/core/org.service';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {GridDataSource, GridCellTextGenerator} from '@eg/share/grid/grid';
+import {Pager} from '@eg/share/util/pager';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {StringComponent} from '@eg/share/string/string.component';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {FundTagsComponent} from './fund-tags.component';
+import {FundTransferDialogComponent} from './fund-transfer-dialog.component';
+import {map, mergeMap} from 'rxjs/operators';
+import {Observable, forkJoin, of} from 'rxjs';
+
+@Component({
+ selector: 'eg-fund-details-dialog',
+ templateUrl: './fund-details-dialog.component.html'
+})
+
+export class FundDetailsDialogComponent
+ extends DialogComponent implements OnInit {
+
+ @Input() fundId: number;
+ fund: IdlObject;
+ idlDef: any;
+ fieldOrder: any;
+ acqfaDataSource: GridDataSource
+ acqftrDataSource: GridDataSource
+ acqfdebDataSource: GridDataSource
+
+ @ViewChild('editDialog', { static: true }) editDialog: FmRecordEditorComponent;
+ @ViewChild('transferDialog', { static: false }) transferDialog: FundTransferDialogComponent;
+ @ViewChild('allocateToFundDialog', { static: true }) allocateToFundDialog: FmRecordEditorComponent;
+ @ViewChild('successString', { static: true }) successString: StringComponent;
+ @ViewChild('updateFailedString', { static: false }) updateFailedString: StringComponent;
+
+ activeTab = 'summary';
+ defaultTabType = 'summary';
+ cellTextGenerator: GridCellTextGenerator;
+
+ constructor(
+ private idl: IdlService,
+ private evt: EventService,
+ private net: NetService,
+ private auth: AuthService,
+ private pcrud: PcrudService,
+ private store: StoreService,
+ private org: OrgService,
+ private format: FormatService,
+ private toast: ToastService,
+ private modal: NgbModal
+ ) {
+ super(modal);
+ }
+
+ ngOnInit() {
+
+ this.defaultTabType =
+ this.store.getLocalItem('eg.acq.fund_details.default_tab') || 'details';
+ this.activeTab = this.defaultTabType;
+
+ this.fund = null;
+ this.onOpen$.subscribe(() => this._initRecord());
+ this.idlDef = this.idl.classes['acqf']
+ this.fieldOrder = 'name,code,year,org,active,currency_type,balance_stop_percentage,balance_warning_percentage,propagate,rollover';
+
+ this.cellTextGenerator = {
+ src_fund: row => row().code(),
+ dest_fund: row => row().code(),
+ funding_source: row => row().code(),
+ li_id: row => row.li_id,
+ po_id: row => row.po_name,
+ invoice_id: row => row.vendor_invoice_id
+ };
+ }
+
+ private _initRecord() {
+ this.fund = null;
+ this.acqfaDataSource = this._getDataSource('acqfa', 'create_time ASC');
+ this.acqftrDataSource = this._getDataSource('acqftr', 'transfer_time ASC');
+ this.acqfdebDataSource = this._getDataSource('acqfdeb', 'create_time ASC');
+ this.pcrud.retrieve('acqf', this.fundId, {
+ flesh: 1,
+ flesh_fields: {
+ acqf: [
+ 'spent_balance',
+ 'combined_balance',
+ 'spent_total',
+ 'encumbrance_total',
+ 'debit_total',
+ 'allocation_total',
+ 'org',
+ 'currency_type'
+ ]
+ }
+ }).subscribe(res => this.fund = res);
+ }
+
+ _getDataSource(idlClass: string, sortField: string): GridDataSource {
+ const gridSource = new GridDataSource();
+
+ gridSource.getRows = (pager: Pager, sort: any[]) => {
+ const orderBy: any = {};
+ if (sort.length) {
+ // Sort specified from grid
+ orderBy[idlClass] = sort[0].name + ' ' + sort[0].dir;
+ } else if (sortField) {
+ // Default sort field
+ orderBy[idlClass] = sortField;
+ }
+
+ const searchOps = {
+ offset: pager.offset,
+ limit: pager.limit,
+ order_by: orderBy,
+ };
+ const reqOps = {
+ fleshSelectors: true,
+ };
+
+ const search: any = new Array();
+ if (idlClass === 'acqfa') {
+ search.push({ fund: this.fundId });
+ } else if (idlClass === 'acqftr') {
+ search.push({
+ '-or': [
+ { src_fund: this.fundId },
+ { dest_fund: this.fundId }
+ ]
+ });
+ searchOps['flesh'] = 2;
+ searchOps['flesh_fields'] = {
+ 'acqftr': ['funding_source_credit'],
+ 'acqfscred': ['funding_source']
+ }
+ } else if (idlClass === 'acqfdeb') {
+ search.push({ fund: this.fundId });
+ searchOps['flesh'] = 2;
+ searchOps['flesh_fields'] = {
+ 'acqfdeb': ['invoice_entry'],
+ 'acqie': ['invoice','purchase_order','lineitem']
+ }
+ }
+
+ Object.keys(gridSource.filters).forEach(key => {
+ Object.keys(gridSource.filters[key]).forEach(key2 => {
+ search.push(gridSource.filters[key][key2]);
+ });
+ });
+
+ return this.pcrud.search(idlClass, search, searchOps, reqOps)
+ .pipe(mergeMap((row) => this.doExtraFleshing(row)));
+ };
+
+ return gridSource;
+ }
+
+ doExtraFleshing(row: IdlObject): Observable<IdlObject> {
+ if (row.classname === 'acqfdeb') {
+ row['vendor_invoice_id'] = null;
+ row['invoice_id'] = null;
+ row['po_id'] = null;
+ row['po_name'] = null;
+ row['li_id'] = null;
+ if (row.invoice_entry()) {
+ if (row.invoice_entry().invoice()) {
+ row['invoice_id'] = row.invoice_entry().invoice().id();
+ row['vendor_invoice_id'] = row.invoice_entry().invoice().inv_ident();
+ }
+ if (row.invoice_entry().purchase_order()) {
+ row['po_id'] = row.invoice_entry().purchase_order().id();
+ row['po_name'] = row.invoice_entry().purchase_order().name();
+ }
+ if (row.invoice_entry().lineitem()) {
+ row['li_id'] = row.invoice_entry().lineitem().id();
+ }
+ }
+ }
+ return of(row);
+ }
+ formatCurrency(value: any) {
+ return this.format.transform({
+ value: value,
+ datatype: 'money'
+ });
+ }
+
+ openEditDialog() {
+ this.editDialog.recordId = this.fundId
+ this.editDialog.mode = 'update';
+ this.editDialog.open({size: 'lg'}).subscribe(
+ result => {
+ this.successString.current()
+ .then(str => this.toast.success(str));
+ this._initRecord();
+ },
+ error => {
+ this.updateFailedString.current()
+ .then(str => this.toast.danger(str));
+ }
+ );
+ }
+
+ allocateToFund() {
+ const allocation = this.idl.create('acqfa');
+ allocation.fund(this.fundId);
+ allocation.allocator(this.auth.user().id());
+ this.allocateToFundDialog.defaultNewRecord = allocation;
+ this.allocateToFundDialog.mode = 'create';
+
+ this.allocateToFundDialog.hiddenFieldsList = ['id', 'fund', 'allocator', 'create_time'];
+ this.allocateToFundDialog.fieldOrder = 'funding_source,amount,note';
+ this.allocateToFundDialog.open().subscribe(
+ result => {
+ this.successString.current()
+ .then(str => this.toast.success(str));
+ this._initRecord();
+ },
+ error => {
+ this.updateFailedString.current()
+ .then(str => this.toast.danger(str));
+ }
+ );
+ }
+
+ doTransfer() {
+ this.transferDialog.sourceFund = this.fund;
+ this.transferDialog.open().subscribe(
+ ok => this._initRecord()
+ )
+ }
+
+ setDefaultTab() {
+ this.defaultTabType = this.activeTab;
+ this.store.setLocalItem('eg.acq.fund_details.default_tab', this.activeTab);
+ }
+
+ getOrgShortname(ou: any) {
+ if (typeof ou === 'object') {
+ return ou.shortname();
+ } else {
+ return this.org.get(ou).shortname();
+ }
+ }
+}
--- /dev/null
+<eg-string #successString i18n-text text="Rollover Succeeded"></eg-string>
+<eg-string #updateFailedString i18n-text text="Rollover Failed"></eg-string>
+
+<ng-template #dialogContent>
+ <div class="modal-header bg-info" *ngIf="doneLoading">
+ <h3 class="modal-title" i18n>Fund Propagation and Rollover</h3>
+ <button type="button" class="close"
+ [disabled]="isProcessing"
+ i18n-aria-label aria-label="Close" (click)="close()">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body" [hidden]="!doneLoading">
+ <form #rolloverForm="ngForm" role="form" class="form-validated">
+ <div class="row col" i18n>Context Org Unit: {{contextOrg?.shortname()}}</div>
+ <div class="row col">
+ <label for="year" class="my-1 mr-1" i18n>Year</label>
+ <eg-combobox [entries]="years" *ngIf="years"
+ [required]="true" [selectedId]="year"
+ (onChange)="year = $event ? $event.id : null">
+ </eg-combobox>
+ </div>
+ <div class="form-check">
+ <input type="checkbox" name="includeDescendants" id="includeDescendants"
+ class="form-check-input"
+ [(ngModel)]="includeDescendants">
+ <label for="includeDescendants" class="form-check-label" i18n>Include funds from descendant Org Units</label>
+ </div>
+ <div class="form-check">
+ <input type="checkbox" name="propgateFunds" id="propgateFunds"
+ class="form-check-input"
+ [(ngModel)]="propgateFunds">
+ <label for="propgateFunds" class="form-check-label" i18n>Propagate Funds</label>
+ <eg-help-popover helpText="Propagate Funds creates new funds for the next fiscal year. Propagating funds will not affect the money or encumbrances in the funds. Only funds that have the Propagate setting enabled will be affected." i18n-helpText></eg-help-popover>
+ </div>
+ <div class="form-check">
+ <input type="checkbox" name="doCloseout" id="doCloseout"
+ class="form-check-input"
+ [(ngModel)]="doCloseout">
+ <label for="doCloseout" class="form-check-label" i18n>Perform Fiscal Year Close-Out</label>
+ <eg-help-popover helpText="Perform Fiscal Year Close-Out moves encumbrances to the corresponding fund for the next fiscal year and deactivates funds for the selected fiscal year. If funds have the Rollover setting enabled, all unspect money will also be moved to the corresponding fund for the next fiscal year." i18n-helpText></eg-help-popover>
+ <span class="alert-warning" *ngIf="doCloseout && !dryRun">Will do a Close-Out for real. If you need to double-check first, check the "Dry Run" checkbox.</span>
+ </div>
+ <div class="offset-sm-1 form-check">
+ <input type="checkbox" name="limitToEncumbrances" id="limitToEncumbrances"
+ class="form-check-input"
+ [(ngModel)]="limitToEncumbrances">
+ <label for="limitToEncumbrances" class="form-check-label" i18n>Limit Fiscal Year Close-Out to Encumbrances</label>
+ <eg-help-popover helpText="This option will limit the Perform Fiscal Year Close-Out procedure to only move encumbrances to the corresponding fund for the next fiscal year. Any unspent money in the funds will not roll over." i18n-helpText></eg-help-popover>
+ </div>
+ <div class="form-check">
+ <input type="checkbox" name="dryRun" id="dryRun"
+ class="form-check-input"
+ [(ngModel)]="dryRun">
+ <label for="dryRun" class="form-check-label" i18n>Dry Run — no data will be changed</label>
+ <eg-help-popover helpText="Select Dry Run to see a preview of the changes that would occur based on the selected actions. Data will not be changed when Dry Run is selected." i18n-helpText></eg-help-popover>
+ </div>
+ </form>
+ <div class="row" [hidden]="!isProcessing">
+ <div class="col-lg-10 offset-lg-1">
+ <eg-progress-inline #rolloverProgress></eg-progress-inline>
+ </div>
+ </div>
+ <div [hidden]="!showResults" class="row col mt-2">
+ <h4 i18n>Fund Propagation & Rollover Summary for Fiscal Year {{year + 1}}</h4>
+ <ul>
+ <li *ngIf="dryRun" i18n>DRY RUN: these changes have not been committed yet.</li>
+ <li i18n>{{count}} funds propagated for fiscal year {{year + 1}} for the selected locations</li>
+ <li i18n>${{amount_rolled}} unspent money rolled over to fiscal year {{year + 1}} for the selected locations</li>
+ </ul>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-info"
+ [disabled]="isProcessing"
+ (click)="rollover()" i18n>Process</button>
+ <button type="button" class="btn btn-warning"
+ [disabled]="isProcessing"
+ (click)="close()" i18n>Close</button>
+ </div>
+</ng-template>
--- /dev/null
+import {Component, Input, ViewChild, TemplateRef, OnInit} from '@angular/core';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgForm} from '@angular/forms';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {Pager} from '@eg/share/util/pager';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {StringComponent} from '@eg/share/string/string.component';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {PermService} from '@eg/core/perm.service';
+import {OrgService} from '@eg/core/org.service';
+import {Observable} from 'rxjs';
+import {map} from 'rxjs/operators';
+import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+@Component({
+ selector: 'eg-fund-rollover-dialog',
+ templateUrl: './fund-rollover-dialog.component.html'
+})
+
+export class FundRolloverDialogComponent
+ extends DialogComponent implements OnInit {
+
+ doneLoading: boolean = false;
+
+ @Input() contextOrgId: number;
+
+ @ViewChild('successString', { static: true }) successString: StringComponent;
+ @ViewChild('updateFailedString', { static: false }) updateFailedString: StringComponent;
+ @ViewChild('rolloverProgress', { static: true })
+ private rolloverProgress: ProgressInlineComponent;
+
+ includeDescendants: boolean = false;
+ propagateFunds: boolean = false;
+ doCloseout: boolean = false;
+ limitToEncumbrances: boolean = false;
+ dryRun: boolean = true;
+ contextOrg: IdlObject;
+ isProcessing: boolean = false;
+ showResults: boolean = false;
+ years: ComboboxEntry[];
+ year: number;
+
+ count: number;
+ amount_rolled: number;
+
+ constructor(
+ private idl: IdlService,
+ private evt: EventService,
+ private net: NetService,
+ private auth: AuthService,
+ private pcrud: PcrudService,
+ private perm: PermService,
+ private toast: ToastService,
+ private org: OrgService,
+ private modal: NgbModal
+ ) {
+ super(modal);
+ }
+
+ ngOnInit() {
+ this.onOpen$.subscribe(() => this._initDialog());
+ this.doneLoading = true;
+ }
+
+ private _initDialog() {
+ this.contextOrg = this.org.get(this.contextOrgId);
+ this.includeDescendants = false;
+ this.propagateFunds = false;
+ this.doCloseout = false;
+ this.limitToEncumbrances = false;
+ this.showResults = false;
+ this.dryRun = true;
+ this.years = null;
+ this.year = null;
+ let maxYear = 0;
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.fund.org.years.retrieve',
+ this.auth.token(),
+ {},
+ { limit_perm: 'VIEW_FUND' }
+ ).subscribe(
+ years => {
+ this.years = years.map(y => {
+ if (maxYear < y) { maxYear = y; }
+ return { id: y, label: y };
+ });
+ this.year = maxYear;
+ }
+ )
+ }
+
+ rollover() {
+ this.isProcessing = true;
+
+ const rolloverResponses: any = [];
+
+ let method = 'open-ils.acq.fiscal_rollover';
+ if (this.doCloseout) {
+ method += '.combined';
+ } else {
+ method += '.propagate';
+ }
+ if (this.dryRun) { method += '.dry_run'; }
+
+ this.count = 0;
+ this.amount_rolled = 0;
+
+ this.net.request(
+ 'open-ils.acq',
+ method,
+ this.auth.token(),
+ this.year,
+ this.contextOrgId,
+ this.includeDescendants,
+ { encumb_only : this.limitToEncumbrances }
+ ).subscribe(
+ r => {
+ rolloverResponses.push(r.fund);
+ this.count++;
+ this.amount_rolled += Number(r.rollover_amount);
+ },
+ err => {},
+ () => {
+ this.isProcessing = false;
+ this.showResults = true;
+ // note that we're intentionally not closing the dialog
+ // so that user can view the results
+ }
+ )
+ }
+
+}
--- /dev/null
+<ng-template #addSuccessStrTmpl i18n>Added tag</ng-template>
+<eg-string #addSuccessString [template]="removeSuccessStrTmpl"></eg-string>
+<ng-template #addErrorStrTmpl i18n>Failed to add tag</ng-template>
+<eg-string #addErrorString [template]="removeErrorStrTmpl"></eg-string>
+<ng-template #removeSuccessStrTmpl i18n>Removed tag</ng-template>
+<eg-string #removeSuccessString [template]="removeSuccessStrTmpl"></eg-string>
+<ng-template #removeErrorStrTmpl i18n>Failed to remove tag</ng-template>
+<eg-string #removeErrorString [template]="removeErrorStrTmpl"></eg-string>
+
+<div class="row">
+ <div class="col-sm-2" *ngFor="let ftm of tagMaps">
+ <button class="btn btn-sm material-icon-button" type="button"
+ (click)="removeTagMap(ftm)"
+ i18n-title title="Remove Tag"><span class="sr-only">Remove Tag</span>
+ <span class="material-icons" aria-hidden="true">delete</span>
+ </button>
+ {{ftm.tag().name()}} ({{ftm.tag().owner().shortname()}})
+ </div>
+</div>
+<div class="row mt-3">
+ <div class="col-sm-2">
+ <eg-combobox #tagSelector [asyncSupportsEmptyTermClick]="true"
+ [(ngModel)]="newTag" [asyncDataSource]="tagSelectorDataSource"
+ i18n-placeholder placeholder="Select tag"></eg-combobox>
+ </div>
+ <div class="col-sm-1">
+ <button class="btn btn-success" [disabled]="!newTag || checkNewTagAlreadyMapped()"
+ (click)="addTagMap()" i18n>Add Tag
+ </button>
+ </div>
+ <div class="col-sm-2" *ngIf="newTag && checkNewTagAlreadyMapped()">
+ <span class="alert-warning" i18n>(tag is already assigned to this fund)</span>
+ </div>
+</div>
--- /dev/null
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {ComboboxComponent} from '@eg/share/combobox/combobox.component';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {Observable} from 'rxjs';
+import {map} from 'rxjs/operators';
+
+@Component({
+ selector: 'eg-fund-tags',
+ templateUrl: './fund-tags.component.html'
+})
+export class FundTagsComponent implements OnInit {
+
+ @Input() fundId: number;
+ @Input() fundOwner: number;
+
+ @ViewChild('addSuccessString', { static: true }) addSuccessString: StringComponent;
+ @ViewChild('addErrorString', { static: true }) addErrorString: StringComponent;
+ @ViewChild('removeSuccessString', { static: true }) removeSuccessString: StringComponent;
+ @ViewChild('removeErrorString', { static: true }) removeErrorString: StringComponent;
+ @ViewChild('tagSelector', { static: false }) tagSelector: ComboboxComponent;
+
+ tagMaps: IdlObject[];
+ newTag: ComboboxEntry = null;
+ tagSelectorDataSource: (term: string) => Observable<ComboboxEntry>
+
+ constructor(
+ private idl: IdlService,
+ private evt: EventService,
+ private net: NetService,
+ private auth: AuthService,
+ private pcrud: PcrudService,
+ private org: OrgService,
+ private toast: ToastService
+ ) {}
+
+ ngOnInit() {
+ this._loadTagMaps();
+ this.tagSelectorDataSource = term => {
+ const field = 'name';
+ const args = {};
+ const extra_args = { order_by : {} };
+ args[field] = {'ilike': `%${term}%`}; // could -or search on label
+ args['owner'] = this.org.ancestors(this.fundOwner, true)
+ extra_args['order_by']['acqft'] = field;
+ extra_args['limit'] = 100;
+ extra_args['flesh'] = 2;
+ const flesh_fields: Object = {};
+ flesh_fields['acqft'] = ['owner'];
+ extra_args['flesh_fields'] = flesh_fields;
+ return this.pcrud.search('acqft', args, extra_args).pipe(map(data => {
+ return {
+ id: data.id(),
+ label: data.name() + ' (' + data.owner().shortname() + ')',
+ fm: data
+ };
+ }));
+ };
+ }
+
+ _loadTagMaps() {
+ this.tagMaps = [];
+ this.pcrud.search('acqftm', { fund: this.fundId }, {
+ flesh: 2,
+ flesh_fields: {
+ acqftm: ['tag'],
+ acqft: ['owner']
+ }
+ }).subscribe(
+ res => this.tagMaps.push(res),
+ err => {},
+ () => this.tagMaps.sort((a, b) => {
+ return a.tag().name() < b.tag().name() ? -1 : 1
+ })
+ );
+ }
+
+ checkNewTagAlreadyMapped(): boolean {
+ if ( this.newTag == null) { return false; }
+ const matches: IdlObject[] = this.tagMaps.filter(tm => tm.tag().id() === this.newTag.id);
+ return matches.length > 0 ? true : false;
+ }
+
+ addTagMap() {
+ const ftm = this.idl.create('acqftm');
+ ftm.tag(this.newTag.id);
+ ftm.fund(this.fundId);
+ this.pcrud.create(ftm).subscribe(
+ ok => {
+ this.addSuccessString.current()
+ .then(str => this.toast.success(str));
+ },
+ err => {
+ this.addErrorString.current()
+ .then(str => this.toast.danger(str));
+ },
+ () => {
+ this.newTag = null;
+ this.tagSelector.selectedId = null;
+ this._loadTagMaps();
+ }
+ );
+ }
+ removeTagMap(ftm: IdlObject) {
+ this.pcrud.remove(ftm).subscribe(
+ ok => {
+ this.removeSuccessString.current()
+ .then(str => this.toast.success(str));
+ },
+ err => {
+ this.removeErrorString.current()
+ .then(str => this.toast.danger(str));
+ },
+ () => this._loadTagMaps()
+ )
+ }
+}
--- /dev/null
+<eg-string #successString i18n-text text="Fund Transfer Succeeded"></eg-string>
+<eg-string #updateFailedString i18n-text text="Fund Transfer Failed"></eg-string>
+
+<ng-template #dialogContent>
+ <div class="modal-header bg-info" *ngIf="doneLoading">
+ <h3 class="modal-title" i18n>Transfer from Fund {{sourceFund?.name()}} ({{sourceFund?.code()}} ({{sourceFund?.year()}}) ({{sourceFund?.org().shortname()}}))</h3>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="close()">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body" [hidden]="!doneLoading">
+ <form #xfrForm="ngForm" role="form" class="form-validated">
+ <div class="form-group row mt-2">
+ <label for="dest-fund" class="col-sm-4 col-form-label" i18n>Destination Fund</label>
+ <div class="col-sm-8">
+ <eg-combobox #fundSelector [asyncSupportsEmptyTermClick]="true"
+ name="dest_fund" id="dest-fund"
+ [(ngModel)]="destFund" [asyncDataSource]="fundDataSource"
+ i18n-placeholder placeholder="Select fund..."></eg-combobox>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label for="source_amount" class="col-sm-4 col-form-label" i18n>Source Amount</label>
+ <div class="col-sm-8">
+ <input class="form-control" type="number" name="source_amount" id="source_amount" [(ngModel)]="sourceAmount">
+ </div>
+ </div>
+ <div class="form-group row">
+ <div class="col-sm-4"></div>
+ <div class="col-sm-8" i18n>
+ <i>Amount to transfer from {{sourceFund?.name()}} ({{sourceFund?.code()}} ({{sourceFund?.year()}}) ({{sourceFund?.org().shortname()}}))</i>
+ </div>
+ </div>
+ <div class="form-group row">
+ <label for="note" class="col-sm-4 col-form-label" i18n>Note</label>
+ <div class="col-sm-8">
+ <input class="form-control" type="text" name="note" id="note" [(ngModel)]="note">
+ </div>
+ </div>
+ </form>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-info"
+ [disabled]="!destFund || (sourceAmount <= 0)"
+ (click)="transfer()" i18n>Transfer</button>
+ <button type="button" class="btn btn-warning"
+ (click)="close()" i18n>Close</button>
+ </div>
+</ng-template>
--- /dev/null
+import {Component, Input, ViewChild, TemplateRef, OnInit} from '@angular/core';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {NgForm} from '@angular/forms';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {EventService} from '@eg/core/event.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {GridDataSource} from '@eg/share/grid/grid';
+import {Pager} from '@eg/share/util/pager';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {StringComponent} from '@eg/share/string/string.component';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {PermService} from '@eg/core/perm.service';
+import {ComboboxComponent} from '@eg/share/combobox/combobox.component';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+import {Observable} from 'rxjs';
+import {map} from 'rxjs/operators';
+
+@Component({
+ selector: 'eg-fund-transfer-dialog',
+ templateUrl: './fund-transfer-dialog.component.html'
+})
+
+export class FundTransferDialogComponent
+ extends DialogComponent implements OnInit {
+
+ @Input() sourceFund: IdlObject;
+ doneLoading: boolean = false;
+
+ @ViewChild('successString', { static: true }) successString: StringComponent;
+ @ViewChild('updateFailedString', { static: false }) updateFailedString: StringComponent;
+ @ViewChild('fundSelector', { static: false }) tagSelector: ComboboxComponent;
+
+ fundDataSource: (term: string) => Observable<ComboboxEntry>;
+ destFund: ComboboxEntry = null;
+ sourceAmount: number = 0.0;
+ note = null;
+
+ constructor(
+ private idl: IdlService,
+ private evt: EventService,
+ private net: NetService,
+ private auth: AuthService,
+ private pcrud: PcrudService,
+ private perm: PermService,
+ private toast: ToastService,
+ private modal: NgbModal
+ ) {
+ super(modal);
+ }
+
+ ngOnInit() {
+ this.destFund = null;
+ this.onOpen$.subscribe(() => this._initRecord());
+ this.fundDataSource = term => {
+ const field = 'code';
+ const args = {};
+ const extra_args = { order_by : {} };
+ args[field] = {'ilike': `%${term}%`}; // could -or search on label
+ args['active'] = 't';
+ extra_args['order_by']['acqf'] = field;
+ extra_args['limit'] = 100;
+ extra_args['flesh'] = 1;
+ const flesh_fields: Object = {};
+ flesh_fields['acqf'] = ['org'];
+ extra_args['flesh_fields'] = flesh_fields;
+ return this.pcrud.search('acqf', args, extra_args).pipe(map(data => {
+ return {
+ id: data.id(),
+ label: data.code()
+ + ' (' + data.year() + ')'
+ + ' (' + data.org().shortname() + ')',
+ fm: data
+ };
+ }));
+ };
+ }
+
+ private _initRecord() {
+ this.doneLoading = false;
+ this.destFund = null;
+ this.sourceAmount = 0;
+ this.note = null;
+ this.doneLoading = true;
+ }
+
+ transfer() {
+ this.net.request(
+ 'open-ils.acq',
+ 'open-ils.acq.funds.transfer_money',
+ this.auth.token(),
+ this.sourceFund.id(),
+ this.sourceAmount,
+ this.destFund.id,
+ null,
+ this.note
+ ).subscribe(
+ res => {
+ this.successString.current()
+ .then(str => this.toast.success(str));
+ this.close(true);
+ },
+ res => {
+ this.updateFailedString.current()
+ .then(str => this.toast.danger(str));
+ this.close(false);
+ }
+ )
+ }
+
+}
--- /dev/null
+<eg-string #successString i18n-text text="Funding Source Update Succeeded"></eg-string>
+<eg-string #updateFailedString i18n-text text="Funding Source Update Failed"></eg-string>
+
+<ng-template #summaryField let-field="field" let-value="value">
+ <div class="col-2">
+ <label style="font-weight: bold" for="fund-{{field}}">{{idlDef.field_map[field].label}}</label>
+ </div>
+ <div class="col-2">
+ <span id="fund-{{field}}">
+ {{value}}
+ </span>
+ </div>
+</ng-template>
+<ng-template #dialogContent>
+ <div class="modal-header bg-info">
+ <h3 class="modal-title" i18n>Funding Source: {{fundingSource?.name()}}</h3>
+ <button type="button" class="close"
+ i18n-aria-label aria-label="Close" (click)="close()">
+ <span aria-hidden="true">×</span>
+ </button>
+ </div>
+ <div class="modal-body" *ngIf="fundingSource">
+ <ul ngbNav #fundingSourceTransactionsNav="ngbNav" [(activeId)]="activeTab" class="nav-tabs">
+ <li [ngbNavItem]="'credits'">
+ <a ngbNavLink i18n>Credits</a>
+ <ng-template ngbNavContent>
+ <div class="mt-2">
+ <eg-grid #creditsGrid idlClass="acqfscred" [dataSource]="acqfscredDataSource"
+ [sortable]="true" persistKey="acq.funding_source.credit"
+ [filterable]="true" [stickyHeader]="true">
+
+ <eg-grid-toolbar-button label="Apply Credits" i18n-label (onClick)="createCredit(creditsGrid)">
+ </eg-grid-toolbar-button>
+ <eg-grid-column path="amount"></eg-grid-column>
+ <eg-grid-column path="effective_date"></eg-grid-column>
+ <eg-grid-column path="deadline_date"></eg-grid-column>
+ <eg-grid-column path="note"></eg-grid-column>
+ <eg-grid-column path="id" [hidden]="true"></eg-grid-column>
+ <eg-grid-column path="funding_source" [hidden]="true" [filterable]="false"></eg-grid-column>
+ </eg-grid>
+ </div>
+ </ng-template>
+ </li>
+ <li [ngbNavItem]="'allocations'">
+ <a ngbNavLink i18n>Allocations</a>
+ <ng-template ngbNavContent>
+ <div class="mt-2">
+ <eg-grid #allocationsGrid idlClass="acqfa" [dataSource]="acqfaDataSource"
+ [sortable]="true" persistKey="acq.funding_source.fund_allocation"
+ [filterable]="true" [stickyHeader]="true" [cellTextGenerator]="cellTextGenerator">
+
+ <eg-grid-toolbar-button label="Allocate to Fund" i18n-label (onClick)="allocateToFund(allocationsGrid)">
+ </eg-grid-toolbar-button>
+ <ng-template #fundTmpl let-row="row">
+ <a href="/eg2/staff/admin/acq/funds/fund/{{row.fund().id()}}" target="_blank">
+ {{row.fund().code()}} ({{row.fund().year()}}) ({{getOrgShortname(row.fund().org())}})
+ </a>
+ </ng-template>
+ <eg-grid-column path="fund" [cellTemplate]="fundTmpl"></eg-grid-column>
+ <eg-grid-column path="amount"></eg-grid-column>
+ <eg-grid-column path="create_time"></eg-grid-column>
+ <eg-grid-column path="allocator"></eg-grid-column>
+ <eg-grid-column path="note"></eg-grid-column>
+ <eg-grid-column path="id" [hidden]="true"></eg-grid-column>
+ <eg-grid-column path="funding_source" [hidden]="true" [filterable]="false"></eg-grid-column>
+ </eg-grid>
+ </div>
+ </ng-template>
+ </li>
+ </ul>
+ <div [ngbNavOutlet]="fundingSourceTransactionsNav"></div>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-warning"
+ (click)="close()" i18n>Close</button>
+ </div>
+</ng-template>
+
+<eg-fm-record-editor #applyCreditDialog idlClass="acqfscred">
+</eg-fm-record-editor>
+<eg-fm-record-editor #allocateToFundDialog idlClass="acqfa">
+</eg-fm-record-editor>
--- /dev/null
+import {Component, Input, ViewChild, TemplateRef, OnInit} from '@angular/core';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {FormatService} from '@eg/core/format.service';
+import {EventService} from '@eg/core/event.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {GridDataSource, GridCellTextGenerator} from '@eg/share/grid/grid';
+import {Pager} from '@eg/share/util/pager';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {StringComponent} from '@eg/share/string/string.component';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {OrgService} from '@eg/core/org.service';
+
+@Component({
+ selector: 'eg-funding-source-transactions-dialog',
+ templateUrl: './funding-source-transactions-dialog.component.html'
+})
+
+export class FundingSourceTransactionsDialogComponent
+ extends DialogComponent implements OnInit {
+
+ @Input() fundingSourceId: number;
+ @Input() activeTab = 'credits';
+ fundingSource: IdlObject;
+ idlDef: any;
+ fieldOrder: any;
+ acqfaDataSource: GridDataSource
+ acqfscredDataSource: GridDataSource
+ cellTextGenerator: GridCellTextGenerator;
+
+ @ViewChild('applyCreditDialog', { static: true }) applyCreditDialog: FmRecordEditorComponent;
+ @ViewChild('allocateToFundDialog', { static: true }) allocateToFundDialog: FmRecordEditorComponent;
+ @ViewChild('successString', { static: true }) successString: StringComponent;
+ @ViewChild('updateFailedString', { static: false }) updateFailedString: StringComponent;
+
+ constructor(
+ private idl: IdlService,
+ private evt: EventService,
+ private net: NetService,
+ private auth: AuthService,
+ private pcrud: PcrudService,
+ private org: OrgService,
+ private format: FormatService,
+ private toast: ToastService,
+ private modal: NgbModal
+ ) {
+ super(modal);
+ }
+
+ ngOnInit() {
+ this.cellTextGenerator = {
+ fund: row => {
+ return row.code() + ' (' + row.year() + ') (' +
+ this.getOrgShortname(row.org()) + ')';
+ }
+ };
+ this.fundingSource = null;
+ this.onOpen$.subscribe(() => this._initRecord());
+ this.idlDef = this.idl.classes['acqfs']
+ this.fieldOrder = 'name,code,year,org,active,currency_type,balance_stop_percentage,balance_warning_percentage,propagate,rollover';
+ }
+
+ private _initRecord() {
+ this.fundingSource = null;
+ this.acqfaDataSource = this._getDataSource('acqfa', 'create_time DESC');
+ this.acqfscredDataSource = this._getDataSource('acqfscred', 'effective_date DESC');
+ this.pcrud.retrieve('acqfs', this.fundingSourceId, {}
+ ).subscribe(res => this.fundingSource = res);
+ }
+
+ _getDataSource(idlClass: string, sortField: string): GridDataSource {
+ const gridSource = new GridDataSource();
+
+ gridSource.getRows = (pager: Pager, sort: any[]) => {
+ const orderBy: any = {};
+ if (sort.length) {
+ // Sort specified from grid
+ orderBy[idlClass] = sort[0].name + ' ' + sort[0].dir;
+ } else if (sortField) {
+ // Default sort field
+ orderBy[idlClass] = sortField;
+ }
+
+ const searchOps = {
+ offset: pager.offset,
+ limit: pager.limit,
+ order_by: orderBy,
+ };
+ const reqOps = {
+ fleshSelectors: true,
+ };
+
+ const search: any = new Array();
+ search.push({ funding_source: this.fundingSourceId });
+
+ Object.keys(gridSource.filters).forEach(key => {
+ Object.keys(gridSource.filters[key]).forEach(key2 => {
+ search.push(gridSource.filters[key][key2]);
+ });
+ });
+
+ return this.pcrud.search(
+ idlClass, search, searchOps, reqOps);
+ };
+
+ return gridSource;
+ }
+
+ formatCurrency(value: any) {
+ return this.format.transform({
+ value: value,
+ datatype: 'money'
+ });
+ }
+
+ createCredit(grid: GridComponent) {
+ const credit = this.idl.create('acqfscred');
+ credit.funding_source(this.fundingSourceId);
+ this.applyCreditDialog.defaultNewRecord = credit;
+ this.applyCreditDialog.mode = 'create';
+ this.applyCreditDialog.hiddenFieldsList = ['id', 'funding_source'];
+ this.applyCreditDialog.fieldOrder = 'amount,note,effective_date,deadline_date';
+ this.applyCreditDialog.open().subscribe(
+ result => {
+ this.successString.current()
+ .then(str => this.toast.success(str));
+ grid.reload();
+ },
+ error => {
+ this.updateFailedString.current()
+ .then(str => this.toast.danger(str));
+ }
+ );
+ }
+
+ allocateToFund(grid: GridComponent) {
+ const allocation = this.idl.create('acqfa');
+ allocation.funding_source(this.fundingSourceId);
+ allocation.allocator(this.auth.user().id());
+ this.allocateToFundDialog.defaultNewRecord = allocation;
+ this.allocateToFundDialog.mode = 'create';
+
+ this.allocateToFundDialog.hiddenFieldsList = ['id', 'funding_source', 'allocator', 'create_time'];
+ this.allocateToFundDialog.fieldOrder = 'fund,amount,note';
+ this.allocateToFundDialog.open().subscribe(
+ result => {
+ this.successString.current()
+ .then(str => this.toast.success(str));
+ grid.reload();
+ },
+ error => {
+ this.updateFailedString.current()
+ .then(str => this.toast.danger(str));
+ }
+ );
+ }
+
+ getOrgShortname(ou: any) {
+ if (typeof ou === 'object') {
+ return ou.shortname();
+ } else {
+ return this.org.get(ou).shortname();
+ }
+ }
+}
--- /dev/null
+<ng-template #successStrTmpl i18n>{{idlClassDef.label}} Update Succeeded</ng-template>
+<eg-string #successString [template]="successStrTmpl"></eg-string>
+
+<ng-template #updateFailedStrTmpl i18n>Update of {{idlClassDef.label}} failed</ng-template>
+<eg-string #updateFailedString [template]="updateFailedStrTmpl"></eg-string>
+
+<ng-template #deleteFailedStrTmpl i18n>Delete of {{idlClassDef.label}} failed or was not allowed</ng-template>
+<eg-string #deleteFailedString [template]="deleteFailedStrTmpl"></eg-string>
+
+<ng-template #deleteSuccessStrTmpl i18n>{{idlClassDef.label}} Successfully Deleted</ng-template>
+<eg-string #deleteSuccessString [template]="deleteSuccessStrTmpl"></eg-string>
+
+<ng-template #createStrTmpl i18n>{{idlClassDef.label}} Successfully Created</ng-template>
+<eg-string #createString [template]="createStrTmpl"></eg-string>
+
+<ng-template #createErrStrTmpl i18n>Failed to create new {{idlClassDef.label}}</ng-template>
+<eg-string #createErrString [template]="createErrStrTmpl"></eg-string>
+
+<ng-container *ngIf="orgField">
+ <div class="row">
+ <div class="col-lg-6">
+ <ng-container *ngIf="orgField">
+ <eg-org-family-select
+ [limitPerms]="viewPerms"
+ [selectedOrgId]="contextOrg.id()"
+ [(ngModel)]="searchOrgs"
+ (ngModelChange)="grid.reload()">
+ </eg-org-family-select>
+ </ng-container>
+ </div>
+ </div>
+ <hr/>
+</ng-container>
+
+<!-- idlObject and fieldName applied programmatically -->
+<eg-translate #translator></eg-translate>
+
+<ng-container *ngIf="helpTemplate">
+ <ng-container *ngTemplateOutlet="helpTemplate"></ng-container>
+</ng-container>
+
+<ng-template #configFieldLink let-row="row" let-col="col">
+ <a i18n-title title="Link To {{col.label}}"
+ [attr.href]="configFieldLinkUrl(row, col)">{{configLinkLabel(row, col)}}</a>
+</ng-template>
+
+<eg-grid #grid idlClass="{{idlClass}}" [dataSource]="dataSource" hideFields="{{hideGridFields}}"
+ [sortable]="true" persistKey="{{persistKey}}" autoGeneratedColumnOrder="{{fieldOrder}}"
+ [filterable]="true" [stickyHeader]="true"
+ [cellTextGenerator]="cellTextGenerator">
+ <eg-grid-toolbar-button [disabled]="!canCreate"
+ label="New {{idlClassDef.label}}" i18n-label (onClick)="createNew()">
+ </eg-grid-toolbar-button>
+ <eg-grid-toolbar-action label="Edit Selected" i18n-label (onClick)="editSelected($event)"
+ [disableOnRows]="notOneSelectedRow">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Delete Selected" i18n-label (onClick)="deleteSelected($event)"
+ [disableOnRows]="notOneSelectedRow">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Apply Credit" i18n-label (onClick)="createCredit($event)"
+ [disableOnRows]="notOneSelectedRow">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Allocate to Fund" i18n-label (onClick)="allocateToFund($event)"
+ [disableOnRows]="notOneSelectedRow">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="View Credits" i18n-label (onClick)="openTransactionsDialog($event, 'credits')"
+ [disableOnRows]="notOneSelectedRow">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="View Allocations" i18n-label (onClick)="openTransactionsDialog($event, 'allocations')"
+ [disableOnRows]="notOneSelectedRow">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-column path="name"></eg-grid-column>
+ <eg-grid-column path="owner"></eg-grid-column>
+ <eg-grid-column path="code"></eg-grid-column>
+ <eg-grid-column path="currency_type"></eg-grid-column>
+ <eg-grid-column path="balance" datatype="money" [filterable]="false" [sortable]="false"
+ i18n-label label="Balance"></eg-grid-column>
+ <eg-grid-column path="total_credits" datatype="money" [filterable]="false" [sortable]="false"
+ i18n-label label="Total Credits"></eg-grid-column>
+ <eg-grid-column path="total_allocations" datatype="money" [filterable]="false" [sortable]="false"
+ i18n-label label="Total Allocations"></eg-grid-column>
+ <eg-grid-column path="id" [hidden]="true"></eg-grid-column>
+
+
+</eg-grid>
+
+<eg-fm-record-editor #editDialog idlClass="{{idlClass}}"
+ [fieldOptions]="fieldOptions"
+ [fieldOrder]="fieldOrder"
+ [defaultNewRecord]="defaultNewRecord"
+ [preloadLinkedValues]="true"
+ [readonlyFields]="readonlyFields">
+</eg-fm-record-editor>
+
+<eg-funding-source-transactions-dialog #fundingSourceTransactionsDialog></eg-funding-source-transactions-dialog>
+
+<eg-confirm-dialog #confirmDel
+ dialogTitle="Delete?" i18n-dialogTitle
+ dialogBody="Delete funding source?" i18n-dialogBody>
+</eg-confirm-dialog>
+<eg-alert-dialog #alertDialog
+ i18n-dialogBody
+ dialogBody="Funding source cannot deleted as it is in use">
+</eg-alert-dialog>
+
+
+<eg-fm-record-editor #applyCreditDialog idlClass="acqfscred">
+</eg-fm-record-editor>
+<eg-fm-record-editor #allocateToFundDialog idlClass="acqfa">
+</eg-fm-record-editor>
--- /dev/null
+import {Component, Input, ViewChild, OnInit, AfterViewInit} from '@angular/core';
+import {Location} from '@angular/common';
+import {FormatService} from '@eg/core/format.service';
+import {GridDataSource, GridCellTextGenerator} from '@eg/share/grid/grid';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {AdminPageComponent} from '@eg/staff/share/admin-page/admin-page.component';
+import {Pager} from '@eg/share/util/pager';
+import {ActivatedRoute} from '@angular/router';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {FmRecordEditorComponent} from '@eg/share/fm-editor/fm-editor.component';
+import {OrgService} from '@eg/core/org.service';
+import {PermService} from '@eg/core/perm.service';
+import {AuthService} from '@eg/core/auth.service';
+import {NetService} from '@eg/core/net.service';
+import {map, mergeMap} from 'rxjs/operators';
+import {StringComponent} from '@eg/share/string/string.component';
+import {Observable, forkJoin, of} from 'rxjs';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+import {FundingSourceTransactionsDialogComponent} from './funding-source-transactions-dialog.component';
+
+@Component({
+ selector: 'eg-funding-sources',
+ templateUrl: './funding-sources.component.html'
+})
+
+export class FundingSourcesComponent extends AdminPageComponent implements OnInit, AfterViewInit {
+ idlClass = 'acqfs';
+ classLabel: string;
+
+ @Input() startId: number;
+
+ @ViewChild('grid', { static: true }) grid: GridComponent;
+ @ViewChild('fundingSourceTransactionsDialog', { static: false }) fundingSourceTransactionsDialog: FundingSourceTransactionsDialogComponent;
+ @ViewChild('applyCreditDialog', { static: true }) applyCreditDialog: FmRecordEditorComponent;
+ @ViewChild('allocateToFundDialog', { static: true }) allocateToFundDialog: FmRecordEditorComponent;
+ @ViewChild('alertDialog', {static: false}) private alertDialog: AlertDialogComponent;
+ @ViewChild('confirmDel', { static: true }) confirmDel: ConfirmDialogComponent;
+
+ cellTextGenerator: GridCellTextGenerator;
+ notOneSelectedRow: (rows: IdlObject[]) => boolean;
+
+ constructor(
+ route: ActivatedRoute,
+ ngLocation: Location,
+ format: FormatService,
+ idl: IdlService,
+ org: OrgService,
+ auth: AuthService,
+ pcrud: PcrudService,
+ perm: PermService,
+ toast: ToastService,
+ private net: NetService
+ ) {
+ super(route, ngLocation, format, idl, org, auth, pcrud, perm, toast);
+ this.dataSource = new GridDataSource();
+ }
+
+ ngOnInit() {
+ this.cellTextGenerator = {
+ name: row => row.name()
+ };
+ this.notOneSelectedRow = (rows: IdlObject[]) => (rows.length !== 1);
+ this.fieldOrder = 'name,code,year,org,active,currency_type,balance_stop_percentage,balance_warning_percentage,propagate,rollover';
+ this.defaultNewRecord = this.idl.create('acqfs');
+ this.defaultNewRecord.owner(this.auth.user().ws_ou());
+
+ this.dataSource.getRows = (pager: Pager, sort: any[]) => {
+ const orderBy: any = {};
+ if (sort.length) {
+ // Sort specified from grid
+ orderBy[this.idlClass] = sort[0].name + ' ' + sort[0].dir;
+ } else if (this.sortField) {
+ // Default sort field
+ orderBy[this.idlClass] = this.sortField;
+ }
+
+ const searchOps = {
+ offset: pager.offset,
+ limit: pager.limit,
+ order_by: orderBy,
+ flesh: 1,
+ flesh_fields: {
+ acqfs: ['credits','allocations']
+ }
+ };
+ const reqOps = {
+ fleshSelectors: true,
+ };
+
+ if (!this.contextOrg && !Object.keys(this.dataSource.filters).length) {
+ // No org filter -- fetch all rows
+ return this.pcrud.retrieveAll(
+ this.idlClass, searchOps, reqOps)
+ .pipe(mergeMap((row) => this.calculateSummary(row)));
+ }
+
+ const search: any = new Array();
+ const orgFilter: any = {};
+
+ if (this.orgField && (this.searchOrgs || this.contextOrg)) {
+ orgFilter[this.orgField] =
+ this.searchOrgs.orgIds || [this.contextOrg.id()];
+ search.push(orgFilter);
+ }
+
+ Object.keys(this.dataSource.filters).forEach(key => {
+ Object.keys(this.dataSource.filters[key]).forEach(key2 => {
+ search.push(this.dataSource.filters[key][key2]);
+ });
+ });
+
+ return this.pcrud.search(
+ this.idlClass, search, searchOps, reqOps)
+ .pipe(mergeMap((row) => this.calculateSummary(row)));
+ };
+
+ super.ngOnInit();
+
+ this.classLabel = this.idlClassDef.label;
+ this.includeOrgDescendants = true;
+ }
+
+ ngAfterViewInit() {
+ if (this.startId) {
+ this.pcrud.retrieve('acqfs', this.startId).subscribe(
+ acqfs => this.openTransactionsDialog([acqfs], 'allocations'),
+ err => {},
+ () => this.startId = null
+ );
+ }
+ }
+
+ calculateSummary(row: IdlObject): Observable<IdlObject> {
+ row['balance'] = 0;
+ row['total_credits'] = 0;
+ row['total_allocations'] = 0;
+
+ row.credits().forEach((c) => row['total_credits'] += Number(c.amount()));
+ row.allocations().forEach((a) => row['total_allocations'] += Number(a.amount()));
+ row['balance'] = row['total_credits'] - row['total_allocations'];
+ return of(row);
+ }
+
+ deleteSelected(rows: IdlObject[]) {
+ if (rows.length > 0) {
+ const id = rows[0].id();
+ let can: boolean = true;
+ forkJoin([
+ this.pcrud.search('acqfa', { funding_source: id }, { limit: 1 }, { atomic: true }),
+ this.pcrud.search('acqfscred', { funding_source: id }, { limit: 1 }, { atomic: true }),
+ ]).subscribe(
+ results => {
+ results.forEach((res) => {
+ if (res.length > 0) {
+ can = false;
+ }
+ });
+ },
+ err => {},
+ () => {
+ if (can) {
+ this.confirmDel.open().subscribe(confirmed => {
+ if (!confirmed) { return; }
+ super.deleteSelected([ rows[0] ]);
+ });
+ } else {
+ this.alertDialog.open();
+ }
+ }
+ );
+ }
+ }
+
+ openTransactionsDialog(rows: IdlObject[], tab: string) {
+ if (rows.length !== 1) { return; }
+ this.fundingSourceTransactionsDialog.fundingSourceId = rows[0].id();
+ this.fundingSourceTransactionsDialog.activeTab = tab;
+ this.fundingSourceTransactionsDialog.open({size: 'xl'}).subscribe(
+ res => {},
+ err => {},
+ () => this.grid.reload()
+ );
+ }
+
+ createCredit(rows: IdlObject[]) {
+ if (rows.length !== 1) { return; }
+ const fundingSourceId = rows[0].id();
+ const credit = this.idl.create('acqfscred');
+ credit.funding_source(fundingSourceId);
+ this.applyCreditDialog.defaultNewRecord = credit;
+ this.applyCreditDialog.mode = 'create';
+ this.applyCreditDialog.hiddenFieldsList = ['id', 'funding_source'];
+ this.applyCreditDialog.fieldOrder = 'amount,note,effective_date,deadline_date';
+ this.applyCreditDialog.open().subscribe(
+ result => {
+ this.successString.current()
+ .then(str => this.toast.success(str));
+ this.grid.reload();
+ },
+ error => {
+ this.updateFailedString.current()
+ .then(str => this.toast.danger(str));
+ }
+ );
+ }
+
+ allocateToFund(rows: IdlObject[]) {
+ if (rows.length !== 1) { return; }
+ const fundingSourceId = rows[0].id();
+ const allocation = this.idl.create('acqfa');
+ allocation.funding_source(fundingSourceId);
+ allocation.allocator(this.auth.user().id());
+ this.allocateToFundDialog.defaultNewRecord = allocation;
+ this.allocateToFundDialog.mode = 'create';
+
+ this.allocateToFundDialog.hiddenFieldsList = ['id', 'funding_source', 'allocator', 'create_time'];
+ this.allocateToFundDialog.fieldOrder = 'fund,amount,note';
+ this.allocateToFundDialog.open().subscribe(
+ result => {
+ this.successString.current()
+ .then(str => this.toast.success(str));
+ this.grid.reload();
+ },
+ error => {
+ this.updateFailedString.current()
+ .then(str => this.toast.danger(str));
+ }
+ );
+ }
+}
--- /dev/null
+<ng-template #successStrTmpl i18n>{{idlClassDef.label}} Update Succeeded</ng-template>
+<eg-string #successString [template]="successStrTmpl"></eg-string>
+
+<ng-template #updateFailedStrTmpl i18n>Update of {{idlClassDef.label}} failed</ng-template>
+<eg-string #updateFailedString [template]="updateFailedStrTmpl"></eg-string>
+
+<ng-template #deleteFailedStrTmpl i18n>Delete of {{idlClassDef.label}} failed or was not allowed</ng-template>
+<eg-string #deleteFailedString [template]="deleteFailedStrTmpl"></eg-string>
+
+<ng-template #deleteSuccessStrTmpl i18n>{{idlClassDef.label}} Successfully Deleted</ng-template>
+<eg-string #deleteSuccessString [template]="deleteSuccessStrTmpl"></eg-string>
+
+<ng-template #createStrTmpl i18n>{{idlClassDef.label}} Successfully Created</ng-template>
+<eg-string #createString [template]="createStrTmpl"></eg-string>
+
+<ng-template #createErrStrTmpl i18n>Failed to create new {{idlClassDef.label}}</ng-template>
+<eg-string #createErrString [template]="createErrStrTmpl"></eg-string>
+
+<ng-container *ngIf="orgField">
+ <div class="row">
+ <div class="col-lg-6">
+ <ng-container *ngIf="orgField">
+ <eg-org-family-select
+ [limitPerms]="viewPerms"
+ [selectedOrgId]="contextOrg.id()"
+ [(ngModel)]="searchOrgs"
+ (ngModelChange)="grid.reload()">
+ </eg-org-family-select>
+ </ng-container>
+ </div>
+ </div>
+ <hr/>
+</ng-container>
+
+<!-- idlObject and fieldName applied programmatically -->
+<eg-translate #translator></eg-translate>
+
+<ng-container *ngIf="helpTemplate">
+ <ng-container *ngTemplateOutlet="helpTemplate"></ng-container>
+</ng-container>
+
+<ng-template #configFieldLink let-row="row" let-col="col">
+ <a i18n-title title="Link To {{col.label}}"
+ [attr.href]="configFieldLinkUrl(row, col)">{{configLinkLabel(row, col)}}</a>
+</ng-template>
+
+<eg-grid #grid idlClass="{{idlClass}}" [dataSource]="dataSource" hideFields="{{hideGridFields}}"
+ [sortable]="true" persistKey="{{persistKey}}" autoGeneratedColumnOrder="{{fieldOrder}}"
+ [filterable]="true" [stickyHeader]="true"
+ [cellTextGenerator]="cellTextGenerator">
+ <eg-grid-toolbar-button [disabled]="!canCreate"
+ label="New {{idlClassDef.label}}" i18n-label (onClick)="createNew()">
+ </eg-grid-toolbar-button>
+ <eg-grid-toolbar-button [disabled]="!canRollover"
+ label="Fiscal Propagation and Rollover" i18n-label (onClick)="doRollover()">
+ </eg-grid-toolbar-button>
+ <eg-grid-toolbar-action label="View Selected" i18n-label
+ (onClick)="openFundDetailsDialog($event)">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Edit Selected" i18n-label (onClick)="editSelected($event)">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Delete Selected" i18n-label (onClick)="deleteSelected($event)">
+ </eg-grid-toolbar-action>
+
+ <ng-template #nameTmpl let-row="row">
+ <a href="" (click)="openFundDetailsDialog([row]); false">
+ {{row.name()}}
+ </a>
+ </ng-template>
+ <eg-grid-column path="name" [cellTemplate]="nameTmpl"></eg-grid-column>
+ <eg-grid-column path="code"></eg-grid-column>
+ <eg-grid-column path="year" initialFilterOperator="=" [initialFilterValue]="getDefaultYear()"></eg-grid-column>
+ <eg-grid-column path="org"></eg-grid-column>
+ <eg-grid-column path="currency_type"></eg-grid-column>
+ <eg-grid-column path="active"></eg-grid-column>
+ <eg-grid-column path="balance_stop_percent"></eg-grid-column>
+ <eg-grid-column path="balance_warning_percent"></eg-grid-column>
+ <eg-grid-column path="propagate"></eg-grid-column>
+ <eg-grid-column path="rollover"></eg-grid-column>
+ <eg-grid-column path="combined_balance.amount"
+ [sortable]="false" [filterable]="false"
+ label="Combined Balance" i18n-label></eg-grid-column>
+ <eg-grid-column path="spent_balance.amount"
+ [sortable]="false" [filterable]="false" [hidden]="true"
+ label="Spent Balance" i18n-label></eg-grid-column>
+ <eg-grid-column path="spent_total.amount"
+ [sortable]="false" [filterable]="false" [hidden]="true"
+ label="Total Spent" i18n-label></eg-grid-column>
+ <eg-grid-column path="encumbrance_total.amount"
+ [sortable]="false" [filterable]="false" [hidden]="true"
+ label="Total Encumbered" i18n-label></eg-grid-column>
+ <eg-grid-column path="debit_total.amount"
+ [sortable]="false" [filterable]="false" [hidden]="true"
+ label="Total Debits" i18n-label></eg-grid-column>
+ <eg-grid-column path="allocation_total.amount"
+ [sortable]="false" [filterable]="false" [hidden]="true"
+ label="Total Allocated" i18n-label></eg-grid-column>
+ <eg-grid-column path="id" [hidden]="true"></eg-grid-column>
+
+
+</eg-grid>
+
+<eg-fm-record-editor #editDialog idlClass="{{idlClass}}"
+ [fieldOptions]="fieldOptions"
+ [fieldOrder]="fieldOrder"
+ [defaultNewRecord]="defaultNewRecord"
+ [preloadLinkedValues]="true"
+ [readonlyFields]="readonlyFields">
+</eg-fm-record-editor>
+
+<eg-fund-details-dialog #fundDetailsDialog></eg-fund-details-dialog>
+<eg-fund-rollover-dialog #fundRolloverDialog></eg-fund-rollover-dialog>
--- /dev/null
+import {Component, Input, ViewChild, OnInit, AfterViewInit} from '@angular/core';
+import {Location} from '@angular/common';
+import {FormatService} from '@eg/core/format.service';
+import {GridDataSource, GridCellTextGenerator} from '@eg/share/grid/grid';
+import {GridComponent} from '@eg/share/grid/grid.component';
+import {AdminPageComponent} from '@eg/staff/share/admin-page/admin-page.component';
+import {Pager} from '@eg/share/util/pager';
+import {ActivatedRoute} from '@angular/router';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {ToastService} from '@eg/share/toast/toast.service';
+import {PcrudService} from '@eg/core/pcrud.service';
+import {OrgService} from '@eg/core/org.service';
+import {PermService} from '@eg/core/perm.service';
+import {AuthService} from '@eg/core/auth.service';
+import {NetService} from '@eg/core/net.service';
+import {StringComponent} from '@eg/share/string/string.component';
+import {FundDetailsDialogComponent} from './fund-details-dialog.component';
+import {FundRolloverDialogComponent} from './fund-rollover-dialog.component';
+
+@Component({
+ selector: 'eg-funds-manager',
+ templateUrl: './funds-manager.component.html'
+})
+
+export class FundsManagerComponent extends AdminPageComponent implements OnInit, AfterViewInit {
+ idlClass = 'acqf';
+ classLabel: string;
+
+ @Input() startId: number;
+
+ @ViewChild('fundDetailsDialog', { static: false }) fundDetailsDialog: FundDetailsDialogComponent;
+ @ViewChild('fundRolloverDialog', { static: false }) fundRolloverDialog: FundRolloverDialogComponent;
+ @ViewChild('grid', { static: true }) grid: GridComponent;
+
+ cellTextGenerator: GridCellTextGenerator;
+ canRollover: boolean = false;
+
+ constructor(
+ route: ActivatedRoute,
+ ngLocation: Location,
+ format: FormatService,
+ idl: IdlService,
+ org: OrgService,
+ auth: AuthService,
+ pcrud: PcrudService,
+ perm: PermService,
+ private perm2: PermService, // need copy because perm is private to base
+ // component
+ toast: ToastService,
+ private net: NetService
+ ) {
+ super(route, ngLocation, format, idl, org, auth, pcrud, perm, toast);
+ this.dataSource = new GridDataSource();
+ }
+
+ ngOnInit() {
+ this.cellTextGenerator = {
+ name: row => row.name()
+ };
+ this.checkRolloverPerms();
+ this.fieldOrder = 'name,code,year,org,active,currency_type,balance_stop_percentage,balance_warning_percentage,propagate,rollover';
+ this.defaultNewRecord = this.idl.create('acqf');
+ this.defaultNewRecord.active(true);
+ this.defaultNewRecord.org(this.auth.user().ws_ou());
+
+ this.dataSource.getRows = (pager: Pager, sort: any[]) => {
+ const orderBy: any = {};
+ if (sort.length) {
+ // Sort specified from grid
+ orderBy[this.idlClass] = sort[0].name + ' ' + sort[0].dir;
+ } else if (this.sortField) {
+ // Default sort field
+ orderBy[this.idlClass] = this.sortField;
+ }
+
+ const searchOps = {
+ offset: pager.offset,
+ limit: pager.limit,
+ order_by: orderBy,
+ flesh_fields: {
+ acqf: [
+ 'spent_balance',
+ 'combined_balance',
+ 'spent_total',
+ 'encumbrance_total',
+ 'debit_total',
+ 'allocation_total'
+ ]
+ }
+ };
+ const reqOps = {
+ fleshSelectors: true,
+ };
+
+ if (!this.contextOrg && !Object.keys(this.dataSource.filters).length) {
+ // No org filter -- fetch all rows
+ return this.pcrud.retrieveAll(
+ this.idlClass, searchOps, reqOps);
+ }
+
+ const search: any = new Array();
+ const orgFilter: any = {};
+
+ if (this.orgField && (this.searchOrgs || this.contextOrg)) {
+ orgFilter[this.orgField] =
+ this.searchOrgs.orgIds || [this.contextOrg.id()];
+ search.push(orgFilter);
+ }
+
+ Object.keys(this.dataSource.filters).forEach(key => {
+ Object.keys(this.dataSource.filters[key]).forEach(key2 => {
+ search.push(this.dataSource.filters[key][key2]);
+ });
+ });
+
+ return this.pcrud.search(
+ this.idlClass, search, searchOps, reqOps);
+ };
+
+ super.ngOnInit();
+
+ this.classLabel = this.idlClassDef.label;
+ this.includeOrgDescendants = true;
+ }
+
+ ngAfterViewInit() {
+ if (this.startId) {
+ this.pcrud.retrieve('acqf', this.startId).subscribe(
+ acqf => this.openFundDetailsDialog([acqf]),
+ err => {},
+ () => this.startId = null
+ );
+ }
+ }
+
+ checkRolloverPerms() {
+ this.canRollover = false;
+
+ this.perm2.hasWorkPermAt(['ADMIN_FUND'], true).then(permMap => {
+ Object.keys(permMap).forEach(key => {
+ if (permMap[key].length > 0) {
+ this.canRollover = true;
+ }
+ });
+ });
+ }
+
+ openFundDetailsDialog(rows: IdlObject[]) {
+ if (rows.length > 0) {
+ this.fundDetailsDialog.fundId = rows[0].id();
+ this.fundDetailsDialog.open({size: 'xl'}).subscribe(
+ result => this.grid.reload(),
+ error => this.grid.reload(),
+ () => this.grid.reload()
+ );
+ }
+ }
+
+ getDefaultYear(): string {
+ return new Date().getFullYear().toString();
+ }
+
+ doRollover() {
+ this.fundRolloverDialog.contextOrgId = this.searchOrgs.primaryOrgId;
+ this.fundRolloverDialog.open({size: 'lg'}).subscribe(
+ ok => {},
+ err => {},
+ () => this.grid.reload()
+ );
+ }
+}
--- /dev/null
+<eg-staff-banner bannerText="Funds" i18n-bannerText>
+</eg-staff-banner>
+
+<eg-title i18n-prefix prefix="Funds"></eg-title>
+
+<ul ngbNav #claimingAdminNav="ngbNav" class="nav-tabs" [(activeId)]="activeTab"
+ (navChange)="onNavChange($event)">
+ <li [ngbNavItem]="'fund'">
+ <a ngbNavLink i18n>Funds</a>
+ <ng-template ngbNavContent>
+ <div class="mt-2">
+ <eg-funds-manager [startId]="fundId"></eg-funds-manager>
+ </div>
+ </ng-template>
+ </li>
+ <li [ngbNavItem]="'funding_source'">
+ <a ngbNavLink i18n>Funding Sources</a>
+ <ng-template ngbNavContent>
+ <div class="mt-2">
+ <eg-funding-sources [startId]="fundingSourceId"></eg-funding-sources>
+ </div>
+ </ng-template>
+ </li>
+ <li [ngbNavItem]="'fund_tag'">
+ <a ngbNavLink i18n>Fund Tags</a>
+ <ng-template ngbNavContent>
+ <div class="mt-2">
+ <eg-admin-page idlClass="acqft"></eg-admin-page>
+ </div>
+ </ng-template>
+ </li>
+</ul>
+<div [ngbNavOutlet]="claimingAdminNav"></div>
--- /dev/null
+import {Component, OnInit, Input, ViewChild} from '@angular/core';
+import {Router, ActivatedRoute, ParamMap} from '@angular/router';
+import {Location} from '@angular/common';
+import {NgbNav, NgbNavChangeEvent} from '@ng-bootstrap/ng-bootstrap';
+
+@Component({
+ templateUrl: './funds.component.html'
+})
+export class FundsComponent implements OnInit {
+
+ activeTab: string;
+ fundId: number;
+ fundingSourceId: number;
+
+ constructor(
+ private location: Location,
+ private router: Router,
+ private route: ActivatedRoute
+ ) {}
+
+ ngOnInit() {
+ this.route.paramMap.subscribe((params: ParamMap) => {
+ const tab = params.get('tab');
+ const id = +params.get('id');
+ if (!id || !tab) { return; }
+ if (tab === 'fund' || tab === 'funding_source') {
+ this.activeTab = tab;
+ if (tab === 'fund') {
+ this.fundId = id;
+ } else {
+ this.fundingSourceId = id;
+ }
+ } else {
+ return;
+ }
+ });
+ }
+
+ // Changing a tab in the UI means clearing the route (e.g.,
+ // if we originally navigated vi funds/fund/:id or
+ // funds/funding_source/:id
+ // This is pretty much only cosmetic
+ onNavChange(evt: NgbNavChangeEvent) {
+ const url = this.router.createUrlTree(['/staff/admin/acq/funds']).toString();
+ this.location.go(url); // go without reloading
+ }
+}
--- /dev/null
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {AdminCommonModule} from '@eg/staff/admin/common.module';
+import {FundsComponent} from './funds.component';
+import {FundsRoutingModule} from './routing.module';
+import {FundsManagerComponent} from './funds-manager.component';
+import {FundDetailsDialogComponent} from './fund-details-dialog.component';
+import {FundingSourcesComponent} from './funding-sources.component';
+import {FundingSourceTransactionsDialogComponent} from './funding-source-transactions-dialog.component';
+import {FundTagsComponent} from './fund-tags.component';
+import {FundTransferDialogComponent} from './fund-transfer-dialog.component';
+import {FundRolloverDialogComponent} from './fund-rollover-dialog.component';
+
+@NgModule({
+ declarations: [
+ FundsComponent,
+ FundsManagerComponent,
+ FundDetailsDialogComponent,
+ FundingSourcesComponent,
+ FundingSourceTransactionsDialogComponent,
+ FundTagsComponent,
+ FundTransferDialogComponent,
+ FundRolloverDialogComponent
+ ],
+ imports: [
+ StaffCommonModule,
+ AdminCommonModule,
+ FundsRoutingModule,
+ ],
+ exports: [
+ ],
+ providers: [
+ ]
+})
+
+export class FundsModule {
+}
--- /dev/null
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {FundsComponent} from './funds.component';
+
+const routes: Routes = [{
+ path: '',
+ component: FundsComponent
+ }, {
+ path: ':tab/:id',
+ component: FundsComponent
+}];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+
+export class FundsRoutingModule {}
path: 'claim_type',
redirectTo: 'claiming' // from legacy auto-generated admin page
}, {
+ path: 'funds',
+ loadChildren: () =>
+ import('./funds/funds.module').then(m => m.FundsModule)
+}, {
+ path: 'fund',
+ redirectTo: 'funds' // from auto-generated admin page
+}, {
+ path: 'fund_allocation',
+ redirectTo: 'funds' // from auto-generated admin page
+}, {
+ path: 'fund_allocation_percent',
+ redirectTo: 'funds' // from auto-generated admin page
+}, {
+ path: 'fund_debit',
+ redirectTo: 'funds' // from auto-generated admin page
+}, {
+ path: 'funding_source',
+ redirectTo: 'funds' // from auto-generated admin page
+}, {
+ path: 'funding_source_credit',
+ redirectTo: 'funds' // from auto-generated admin page
+}, {
+ path: 'fund_tag',
+ redirectTo: 'funds' // from auto-generated admin page
+}, {
+ path: 'fund_tag_map',
+ redirectTo: 'funds' // from auto-generated admin page
+}, {
+ path: 'fund_transfer',
+ redirectTo: 'funds' // from auto-generated admin page
+}, {
path: ':table',
component: BasicAdminPageComponent,
// All ACQ admin pages cover data in the acq.* schema. No need to