funds: implement rollover dialog
authorGalen Charlton <gmc@equinoxinitiative.org>
Mon, 29 Mar 2021 01:00:58 +0000 (21:00 -0400)
committerGalen Charlton <gmc@equinoxinitiative.org>
Mon, 29 Mar 2021 01:00:58 +0000 (21:00 -0400)
Signed-off-by: Galen Charlton <gmc@equinoxinitiative.org>
Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-rollover-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-rollover-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funds-manager.component.html
Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funds-manager.component.ts
Open-ILS/src/eg2/src/app/staff/admin/acq/funds/funds.module.ts

diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-rollover-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-rollover-dialog.component.html
new file mode 100644 (file)
index 0000000..1850ea1
--- /dev/null
@@ -0,0 +1,81 @@
+<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">&times;</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 &mdash; 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 &amp; 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>
diff --git a/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-rollover-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/admin/acq/funds/fund-rollover-dialog.component.ts
new file mode 100644 (file)
index 0000000..9d9590a
--- /dev/null
@@ -0,0 +1,139 @@
+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
+            }
+        )
+    }
+
+}
index 301272e..06af88a 100644 (file)
@@ -51,6 +51,9 @@
   <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-fm-record-editor>
 
 <eg-fund-details-dialog #fundDetailsDialog></eg-fund-details-dialog>
+<eg-fund-rollover-dialog #fundRolloverDialog></eg-fund-rollover-dialog>
index 07faf6f..d79c5da 100644 (file)
@@ -15,6 +15,7 @@ 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',
@@ -26,9 +27,11 @@ export class FundsManagerComponent extends AdminPageComponent implements OnInit
     classLabel: string;
 
     @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,
@@ -39,6 +42,8 @@ export class FundsManagerComponent extends AdminPageComponent implements OnInit
         auth: AuthService,
         pcrud: PcrudService,
         perm: PermService,
+        private perm2: PermService, // need copy because perm is private to base
+                                    // component
         toast: ToastService,
         private net: NetService
     ) {
@@ -50,6 +55,7 @@ export class FundsManagerComponent extends AdminPageComponent implements OnInit
         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);
@@ -115,6 +121,18 @@ export class FundsManagerComponent extends AdminPageComponent implements OnInit
         this.includeOrgDescendants = true;
     }
 
+    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();
@@ -129,4 +147,13 @@ export class FundsManagerComponent extends AdminPageComponent implements OnInit
     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()
+        );
+    }
 }
index 3c7d82b..d7f4d04 100644 (file)
@@ -9,6 +9,7 @@ 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: [
@@ -18,12 +19,13 @@ import {FundTransferDialogComponent} from './fund-transfer-dialog.component';
     FundingSourcesComponent,
     FundingSourceTransactionsDialogComponent,
     FundTagsComponent,
-    FundTransferDialogComponent
+    FundTransferDialogComponent,
+    FundRolloverDialogComponent
   ],
   imports: [
     StaffCommonModule,
     AdminCommonModule,
-    FundsRoutingModule
+    FundsRoutingModule,
   ],
   exports: [
   ],