routerLink="/staff/admin/acq/cancel_reason"></eg-link-table-link>
<eg-link-table-link i18n-label label="Claiming"
routerLink="/staff/admin/acq/claiming"></eg-link-table-link>
- <eg-link-table-link i18n-label label="Currency Types"
- routerLink="/staff/admin/acq/currency_type"></eg-link-table-link>
+ <eg-link-table-link i18n-label label="Currencies and Exchange Rates"
+ routerLink="/staff/admin/acq/currency"></eg-link-table-link>
<eg-link-table-link i18n-label label="Distribution Formulas"
routerLink="/staff/admin/acq/distribution_formula"></eg-link-table-link>
<eg-link-table-link i18n-label label="EDI Accounts"
routerLink="/staff/admin/acq/edi_message"></eg-link-table-link>
<eg-link-table-link i18n-label label="EDI Attribute Sets"
routerLink="/staff/admin/acq/edi_attr_set"></eg-link-table-link>
- <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 Administration"
routerLink="/staff/admin/acq/funds"></eg-link-table-link>
<eg-link-table-link i18n-label label="Invoice Item Types"
--- /dev/null
+<eg-staff-banner bannerText="Currencies and Exchange Rates" i18n-bannerText>
+</eg-staff-banner>
+
+<eg-title i18n-prefix prefix="Currencies and Exchange Rates"></eg-title>
+
+<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)">
+ </eg-grid-toolbar-action>
+ <eg-grid-toolbar-action label="Delete Selected" i18n-label (onClick)="deleteIfPossible($event)"
+ [disableOnRows]="notOneSelectedRow">
+ </eg-grid-toolbar-action>
+
+ <eg-grid-column path="code"></eg-grid-column>
+ <eg-grid-column path="label"></eg-grid-column>
+ <ng-template #exchangeRatesTmpl let-currency="row">
+ <button class="btn btn-outline-dark" (click)="openExchangeRatesDialog(currency.code())" i18n>Manage Exchange Rates</button>
+ </ng-template>
+ <eg-grid-column i18n-label label="Exchange Rates" name="exchange_rates"
+ [sortable]="false" [filterable]="false" [cellTemplate]="exchangeRatesTmpl" [disableTooltip]="true"></eg-grid-column>
+
+</eg-grid>
+
+<eg-fm-record-editor #editDialog idlClass="{{idlClass}}"
+ [fieldOptions]="fieldOptions"
+ [fieldOrder]="fieldOrder"
+ [defaultNewRecord]="defaultNewRecord"
+ [preloadLinkedValues]="true"
+ [readonlyFieldsList]="calculateReadonlyFields(editDialog.mode)">
+</eg-fm-record-editor>
+
+<eg-exchange-rates-dialog #exchangeRatesDialog></eg-exchange-rates-dialog>
+
+<eg-confirm-dialog #confirmDel
+ dialogTitle="Delete?" i18n-dialogTitle
+ dialogBody="Delete currency?" i18n-dialogBody>
+</eg-confirm-dialog>
+<eg-alert-dialog #alertDialog
+ i18n-dialogBody
+ dialogBody="Currency Type cannot be deleted because it is associated with funds, providers, funding sources, debits, and/or exchange rates.">
+</eg-alert-dialog>
--- /dev/null
+import {Component, Input, ViewChild, OnInit} 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 {ExchangeRatesDialogComponent} from './exchange-rates-dialog.component';
+import {Observable, forkJoin, of} from 'rxjs';
+import {AlertDialogComponent} from '@eg/share/dialog/alert.component';
+import {ConfirmDialogComponent} from '@eg/share/dialog/confirm.component';
+
+@Component({
+ templateUrl: './currencies.component.html'
+})
+
+export class CurrenciesComponent extends AdminPageComponent implements OnInit {
+ idlClass = 'acqct';
+ classLabel: string;
+
+ @ViewChild('grid', { static: true }) grid: GridComponent;
+ @ViewChild('exchangeRatesDialog', { static: false }) exchangeRatesDialog: ExchangeRatesDialogComponent;
+ @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.notOneSelectedRow = (rows: IdlObject[]) => (rows.length !== 1);
+ this.cellTextGenerator = {
+ exchange_rates: row => ''
+ };
+ this.fieldOrder = 'code,name';
+ this.defaultNewRecord = this.idl.create('acqct');
+
+ 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
+ };
+ 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;
+ }
+
+ openExchangeRatesDialog(code: string) {
+ this.exchangeRatesDialog.currencyCode = code;
+ this.exchangeRatesDialog.open({size: 'lg'});
+ }
+
+ deleteIfPossible(rows: IdlObject[]) {
+ if (rows.length > 0) {
+ const code = rows[0].code();
+ let can = true;
+ forkJoin([
+ this.pcrud.search('acqexr', { from_currency: code }, { limit: 1 }, { atomic: true }),
+ this.pcrud.search('acqexr', { to_currency: code }, { limit: 1 }, { atomic: true }),
+ this.pcrud.search('acqf', { currency_type: code }, { limit: 1 }, { atomic: true }),
+ this.pcrud.search('acqpro', { currency_type: code }, { limit: 1 }, { atomic: true }),
+ this.pcrud.search('acqfdeb', { origin_currency_type: code }, { limit: 1 }, { atomic: true }),
+ this.pcrud.search('acqfs', { currency_type: code }, { 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();
+ }
+ }
+ );
+ }
+ }
+
+ calculateReadonlyFields(mode: string) {
+ return mode === 'update' ? 'code' : '';
+ }
+
+}
--- /dev/null
+import {NgModule} from '@angular/core';
+import {StaffCommonModule} from '@eg/staff/common.module';
+import {AdminCommonModule} from '@eg/staff/admin/common.module';
+import {CurrenciesRoutingModule} from './routing.module';
+import {CurrenciesComponent} from './currencies.component';
+import {ExchangeRatesDialogComponent} from './exchange-rates-dialog.component';
+
+@NgModule({
+ declarations: [
+ CurrenciesComponent,
+ ExchangeRatesDialogComponent
+ ],
+ imports: [
+ StaffCommonModule,
+ AdminCommonModule,
+ CurrenciesRoutingModule
+ ],
+ exports: [
+ ],
+ providers: [
+ ]
+})
+
+export class CurrenciesModule {
+}
--- /dev/null
+<eg-string #successString i18n-text text="Exchange Rates Update Succeeded"></eg-string>
+<eg-string #updateFailedString i18n-text text="Exchange Rates Update Failed"></eg-string>
+
+<ng-template #dialogContent>
+ <div class="modal-header bg-info" *ngIf="doneLoading">
+ <h3 *ngIf="canUpdate" class="modal-title" i18n>Manage Exchange Rates for {{currency.code()}}: {{currency.label()}}</h3>
+ <h3 *ngIf="!canUpdate" class="modal-title" i18n>View Exchange Rates for {{currency.code()}}: {{currency.label()}}</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">
+ <h4 i18n>Exchange rates to other currencies: 1 {{currency?.label()}} is equal to:</h4>
+ <div *ngIf="otherCurrencies?.length < 1" class="alert alert-warning" i18n>
+ No other currencies are currently defined, so cannot set any exchange rates.
+ </div>
+ <form #exrForm="ngForm" role="form" [hidden]="otherCurrencies?.length < 1" class="form-validated">
+ <div class="form-group row mt-2" *ngFor="let ratio of ratios">
+ <label for="exr-{{ratio.to_currency().code()}}" class="col-sm-4 col-form-label">
+ {{ratio.to_currency().code()}} ({{ratio.to_currency().label()}})
+ </label>
+ <div class="col-sm-3">
+ <input *ngIf="canUpdate" class="form-control" type="number" id="exr-{{ratio.to_currency().code()}}"
+ [disabled]="ratio.id() === -1"
+ [ngModel]="ratio.ratio()" name="ratio_{{ratio.to_currency().code()}}"
+ (ngModelChange)="ratio.ratio($event)">
+ <span class="form-control-plaintext" *ngIf="!canUpdate">
+ <ng-container *ngIf="ratio.ratio() !== undefined">{{ratio.ratio()}}</ng-container>
+ <ng-container *ngIf="ratio.ratio() === undefined" i18n>not set</ng-container>
+ </span>
+ </div>
+ <div class="col-sm-3">
+ <span *ngIf="ratio.id() === -1" class="alert-warning" i18n>(inverse; go to other currency to change)</span>
+ </div>
+ </div>
+ </form>
+ </div>
+ <div class="modal-footer">
+ <ng-container *ngIf="canUpdate">
+ <button type="button" class="btn btn-info" [disabled]="!(exrForm?.dirty)"
+ (click)="save()" i18n>Save</button>
+ </ng-container>
+ <button type="button" class="btn btn-warning" *ngIf="canUpdate"
+ (click)="close()" i18n>Cancel</button>
+ <button type="button" class="btn btn-warning" *ngIf="!canUpdate"
+ (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';
+
+@Component({
+ selector: 'eg-exchange-rates-dialog',
+ templateUrl: './exchange-rates-dialog.component.html'
+})
+
+export class ExchangeRatesDialogComponent
+ extends DialogComponent implements OnInit {
+
+ @Input() currencyCode: string;
+ currency: IdlObject;
+ otherCurrencies: IdlObject[];
+ existingRatios: {[toCurrency: string]: IdlObject} = {};
+ existingInverseRatios: {[fromCurrency: string]: IdlObject} = {};
+ ratios: IdlObject[];
+ idlDef: any;
+ fieldOrder: any;
+ canUpdate = false;
+ doneLoading = false;
+
+ @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 perm: PermService,
+ private toast: ToastService,
+ private modal: NgbModal
+ ) {
+ super(modal);
+ }
+
+ ngOnInit() {
+ this.currency = null;
+ this.onOpen$.subscribe(() => this._initRecord());
+ this.idlDef = this.idl.classes['acqct'];
+ this.perm.hasWorkPermAt(['ADMIN_CURRENCY_TYPE'], true).then((perm) => {
+ if (perm['ADMIN_CURRENCY_TYPE'].length > 0) {
+ this.canUpdate = true;
+ }
+ });
+ }
+
+ private _initRecord() {
+ this.doneLoading = false;
+ this.ratios = [];
+ this.otherCurrencies = [];
+ this.existingRatios = {};
+ this.existingInverseRatios = {};
+ this.pcrud.retrieve('acqct', this.currencyCode, {}
+ ).subscribe(res => this.currency = res);
+ this.pcrud.search('acqexr', { from_currency: this.currencyCode }, {
+ flesh: 1,
+ flesh_fields: {'acqexr': ['to_currency']},
+ }, {}).subscribe(
+ exr => this.existingRatios[exr.to_currency().code()] = exr,
+ err => {},
+ () => this.pcrud.search('acqexr', { to_currency: this.currencyCode }, {
+ flesh: 1,
+ flesh_fields: {'acqexr': ['from_currency']},
+ }, {}).subscribe(
+ exr => this.existingInverseRatios[exr.from_currency().code()] = exr,
+ err => {},
+ () => this.pcrud.search('acqct', { code: { '!=': this.currencyCode } },
+ { order_by: 'code ASC' }, { atomic: true })
+ .subscribe(
+ currs => this.otherCurrencies = currs,
+ err => {},
+ () => { this._mergeCurrenciesAndRates(); this.doneLoading = true; }
+ )
+ )
+ );
+ }
+
+ private _mergeCurrenciesAndRates() {
+ this.ratios = [];
+ this.otherCurrencies.forEach(curr => {
+ if (curr.code() in this.existingRatios) {
+ this.ratios.push(this.existingRatios[curr.code()]);
+ } else if (curr.code() in this.existingInverseRatios) {
+ const ratio = this.idl.clone(this.existingInverseRatios[curr.code()]);
+ // mark it as an inverse ratio that should not be directly edited
+ ratio.id(-1);
+ const toCur = ratio.to_currency();
+ ratio.to_currency(ratio.from_currency());
+ ratio.from_currency(toCur);
+ ratio.ratio(1.0 / ratio.ratio());
+ this.ratios.push(ratio);
+ } else {
+ const ratio = this.idl.create('acqexr');
+ ratio.from_currency(this.currencyCode);
+ ratio.to_currency(curr);
+ this.ratios.push(ratio);
+ }
+ });
+ this.ratios.sort((a, b) => {
+ return a.to_currency().code() < b.to_currency().code() ? -1 : 1;
+ });
+ }
+
+ save() {
+ const updateBatch: IdlObject[] = [];
+ this.ratios.forEach(ratio => {
+ if (ratio.id() === -1) {
+ // ignore inverse entries
+ } else if (ratio.id() === undefined && ratio.ratio() !== undefined && ratio.ratio() !== null) {
+ // completely new entry
+ ratio.isnew(true);
+ updateBatch.push(ratio);
+ } else if (ratio.id() !== undefined && ratio.ratio() !== undefined && ratio.ratio() !== null) {
+ // entry that might have been updated
+ ratio.ischanged(true);
+ updateBatch.push(ratio);
+ } else if (ratio.id() !== undefined && (ratio.ratio() === undefined || ratio.ratio() === null)) {
+ // existing entry to delete
+ ratio.isdeleted(true);
+ updateBatch.push(ratio);
+ }
+ });
+ this.pcrud.autoApply(updateBatch).toPromise().then(res => this.close(res));
+ }
+
+}
--- /dev/null
+import {NgModule} from '@angular/core';
+import {RouterModule, Routes} from '@angular/router';
+import {CurrenciesComponent} from './currencies.component';
+
+const routes: Routes = [{
+ path: '',
+ component: CurrenciesComponent
+}];
+
+@NgModule({
+ imports: [RouterModule.forChild(routes)],
+ exports: [RouterModule]
+})
+
+export class CurrenciesRoutingModule {}
path: 'claim_type',
redirectTo: 'claiming' // from legacy auto-generated admin page
}, {
+ path: 'currency',
+ loadChildren: () =>
+ import('./currency/currencies.module').then(m => m.CurrenciesModule)
+}, {
+ path: 'currency_type',
+ redirectTo: 'currency' // from auto-generated admin page
+}, {
+ path: 'exchange_rate',
+ redirectTo: 'currency' // from auto-generated admin page
+}, {
path: 'distribution_formula',
loadChildren: () =>
import('./distribution_formula/distribution-formulas.module').then(m => m.DistributionFormulasModule)