LP#1942220: add 'Disencumber' action for PO items
authorGalen Charlton <gmc@equinoxOLI.org>
Tue, 14 Dec 2021 02:01:26 +0000 (21:01 -0500)
committerGalen Charlton <gmc@equinoxOLI.org>
Tue, 14 Dec 2021 02:01:26 +0000 (21:01 -0500)
This adds a button to, upon user confirmation, change the
fund debit for a PO item to a zero-value encumbrance.

This button is available for a charge only if:

- the PO item is attached to a fund debit that has
  no invoice entries or items attached
- the PO is activated but not cancelled
- the fund debit is not an expenditure
- the debit amount is not already zero

The purpose of this button is to clean up encumbrances for
miscellaneous charges on invoiced POs that have not been
linked to invoice items.

Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>
Open-ILS/src/eg2/src/app/staff/acq/po/charges.component.html
Open-ILS/src/eg2/src/app/staff/acq/po/charges.component.ts
Open-ILS/src/eg2/src/app/staff/acq/po/disencumber-charge-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/po/disencumber-charge-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/acq/po/po.module.ts
Open-ILS/src/eg2/src/app/staff/acq/po/po.service.ts

index 4e64164..6e4d850 100644 (file)
@@ -3,6 +3,8 @@
   <button class="btn btn-info btn-sm" (click)="newCharge()" *ngIf="canModify">New Charge</button>
 </h4>
 
+<eg-acq-disencumber-charge-dialog #disencumberChargeDialog></eg-acq-disencumber-charge-dialog>
+
 <ng-container *ngIf="showBody">
   <div class="row d-flex">
     <div class="flex-2 p-2 font-weight-bold">Charge Type</div>
         [idlQuerySort]="{acqf: 'year DESC, code'}"
         [idlQueryAnd]="{active: 't'}">
       </eg-combobox>
+      <span *ngIf="charge.fund_debit() && charge.fund_debit().fund() !== charge.fund()">
+        <br>
+        <i>Fund actually debited is 
+        <eg-combobox idlClass="acqf" [selectedId]="charge.fund_debit().fund()"
+          [readOnly]="true"></eg-combobox></i>
+      </span>
     </div>
     <div class="flex-2 p-2">
       <span *ngIf="!charge.isnew() && !charge.ischanged()">{{charge.title()}}</span>
       <input *ngIf="charge.isnew() || charge.ischanged()" type="number" min="0" class="form-control" 
         i18n-placeholder placeholder="Esimated Cost..." [required]="true"
         [ngModel]="charge.estimated_cost()" (ngModelChange)="charge.estimated_cost($event)"/>
+      <span *ngIf="charge.fund_debit()">
+        <br>
+        <span *ngIf="charge.fund_debit().encumbrance() === 't'" i18n>
+          <i>Amount encumbered is {{charge.fund_debit().amount() | currency}}</i>
+        </span>
+        <span *ngIf="charge.fund_debit().encumbrance() === 'f'" i18n>
+          <i>Amount expended is {{charge.fund_debit().amount() | currency}}</i>
+        </span>
+      </span>
     </div>
     <div class="flex-2 p-1">
       <button *ngIf="canModify" [disabled]="!(charge.isnew() || charge.ischanged())" class="btn btn-success btn-sm" 
         (click)="saveCharge(charge)" i18n>Save</button>
       <button *ngIf="canModify" [disabled]="charge.isnew()" class="btn btn-outline-dark btn-sm ml-1" 
         (click)="charge.ischanged(true)" i18n>Edit</button>
+      <button class="btn btn-warning btn-sm ml-1" 
+        (click)="disencumberCharge(charge)" *ngIf="canDisencumber(charge)" i18n>Disencumber</button>
       <button class="btn btn-danger btn-sm ml-1" 
         (click)="removeCharge(charge)" *ngIf="canModify" i18n>Remove</button>
     </div>
index 34a4be3..705be70 100644 (file)
@@ -1,10 +1,14 @@
-import {Component, OnInit, OnDestroy, Input} from '@angular/core';
+import {Component, OnInit, OnDestroy, Input, ViewChild} from '@angular/core';
 import {Subscription} from 'rxjs';
 import {Router, ActivatedRoute, ParamMap} from '@angular/router';
 import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {AuthService} from '@eg/core/auth.service';
+import {NetService} from '@eg/core/net.service';
+import {EventService} from '@eg/core/event.service';
 import {PcrudService} from '@eg/core/pcrud.service';
 import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
 import {PoService} from './po.service';
+import {DisencumberChargeDialogComponent} from './disencumber-charge-dialog.component';
 
 @Component({
   templateUrl: 'charges.component.html',
@@ -17,8 +21,13 @@ export class PoChargesComponent implements OnInit, OnDestroy {
     autoId = -1;
     poSubscription: Subscription;
 
+    @ViewChild('disencumberChargeDialog') disencumberChargeDialog: DisencumberChargeDialogComponent;
+
     constructor(
         private idl: IdlService,
+        private net: NetService,
+        private evt: EventService,
+        private auth: AuthService,
         private pcrud: PcrudService,
         public  poService: PoService
     ) {}
@@ -77,6 +86,49 @@ export class PoChargesComponent implements OnInit, OnDestroy {
         }
     }
 
+    canDisencumber(charge: IdlObject): boolean {
+        if (!this.po() || !this.po().order_date() || this.po().state() === 'cancelled') {
+            return false; // order must be loaded, activated, and not cancelled
+        }
+        if (!charge.fund_debit()) {
+            return false; // that which is not encumbered cannot be disencumbered
+        }
+
+        const debit = charge.fund_debit();
+        if (debit.encumbrance() === 'f') {
+            return false; // that which is expended cannot be disencumbered
+        }
+        if (debit.invoice_entry()) {
+            return false; // we shouldn't actually be a po_item that is
+                          // linked to an invoice_entry, but if we are,
+                          // do NOT touch
+        }
+        if (debit.invoice_items() && debit.invoice_items().length) {
+            return false; // we're linked to an invoice item, so the disposition of the
+                          // invoice entry should govern things
+        }
+        if (Number(debit.amount()) === 0) {
+            return false; // we're already at zero
+        }
+        return true; // we're likely OK to disencumber
+    }
+
+    disencumberCharge(charge: IdlObject) {
+        this.disencumberChargeDialog.charge = charge;
+        this.disencumberChargeDialog.open().subscribe(doIt => {
+            if (!doIt) { return; }
+
+            return this.net.request(
+                'open-ils.acq',
+                'open-ils.acq.po_item.disencumber',
+                this.auth.token(), charge.id()
+            ).toPromise().then(res => {
+                const evt = this.evt.parse(res);
+                if (evt) { return Promise.reject(evt + ''); }
+            }).then(_ => this.poService.refreshOrderSummary(true));
+        });
+    }
+
     removeCharge(charge: IdlObject) {
         this.po().po_items( // remove local copy
             this.po().po_items().filter(item => item.id() !== charge.id())
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/po/disencumber-charge-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/acq/po/disencumber-charge-dialog.component.html
new file mode 100644 (file)
index 0000000..03a6cc4
--- /dev/null
@@ -0,0 +1,49 @@
+<ng-template #dialogContent>
+  <form class="form-validated">
+    <div class="modal-header bg-info">
+      <h3 class="modal-title" i18n>Disencumber Direct Charge</h3>
+      <button type="button" class="close"
+        i18n-aria-label aria-label="Close" (click)="close()">
+        <span aria-hidden="true">&times;</span>
+      </button>
+    </div>
+    <div class="modal-body">
+      <div class="d-flex">
+        <div class="flex-2" i18n>Charge:</div>
+        <div class="flex-3">
+          <eg-combobox idlClass="aiit" [selectedId]="charge.inv_item_type()"
+            [readOnly]="true"></eg-combobox>
+        </div>
+      </div>
+      <div class="d-flex">
+        <div class="flex-2" i18n>Amount:</div>
+        <div class="flex-3">{{charge.estimated_cost()}}</div>
+      </div>
+      <div class="d-flex">
+        <div class="flex-2" i18n>Original Fund:</div>
+        <div class="flex-3">
+          <eg-combobox idlClass="acqf" [selectedId]="charge.fund()"
+            [readOnly]="true"></eg-combobox>
+        </div>
+      </div>
+      <div class="d-flex">
+        <div class="flex-2" i18n>Fund Debited:</div>
+        <div class="flex-3">
+          <eg-combobox idlClass="acqf" [selectedId]="charge.fund_debit().fund()"
+            [readOnly]="true"></eg-combobox>
+        </div>
+      </div>
+      <div class="d-flex">
+        <div class="flex-2" i18n>Amount Encumbered:</div>
+        <div class="flex-3">{{charge.fund_debit().amount()}}</div>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <button type="button" class="btn btn-success"
+        (click)="close(true)" i18n>Disencumber</button>
+      <button type="button" class="btn btn-warning"
+        (click)="close()" i18n>Cancel</button>
+    </div>
+  </form>
+</ng-template>
+
diff --git a/Open-ILS/src/eg2/src/app/staff/acq/po/disencumber-charge-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/acq/po/disencumber-charge-dialog.component.ts
new file mode 100644 (file)
index 0000000..4bd442c
--- /dev/null
@@ -0,0 +1,17 @@
+import {Component, Input, ViewChild, TemplateRef, OnInit} from '@angular/core';
+import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+import {IdlService, IdlObject} from '@eg/core/idl.service';
+import {ComboboxEntry} from '@eg/share/combobox/combobox.component';
+
+@Component({
+  selector: 'eg-acq-disencumber-charge-dialog',
+  templateUrl: './disencumber-charge-dialog.component.html'
+})
+
+export class DisencumberChargeDialogComponent extends DialogComponent {
+    @Input() charge: IdlObject;
+    constructor(private modal: NgbModal) { super(modal); }
+}
+
+
index 55b0663..11345e4 100644 (file)
@@ -15,6 +15,7 @@ import {PoNotesComponent} from './notes.component';
 import {PoCreateComponent} from './create.component';
 import {PoChargesComponent} from './charges.component';
 import {PicklistUploadService} from '../picklist/upload.service';
+import {DisencumberChargeDialogComponent} from './disencumber-charge-dialog.component';
 
 @NgModule({
   declarations: [
@@ -25,7 +26,8 @@ import {PicklistUploadService} from '../picklist/upload.service';
     PoNotesComponent,
     PoCreateComponent,
     PoChargesComponent,
-    PrintComponent
+    PrintComponent,
+    DisencumberChargeDialogComponent
   ],
   imports: [
     StaffCommonModule,
index 25d1924..d8fa0a5 100644 (file)
@@ -34,6 +34,7 @@ export class PoService {
             flesh_provider: true,
             flesh_notes: true,
             flesh_po_items: true,
+            flesh_po_items_further: true,
             flesh_price_summary: true,
             flesh_lineitem_count: true
         }, params.fleshMore || {});
@@ -56,18 +57,28 @@ export class PoService {
 
     // Fetch the PO again (with less fleshing) and update the
     // order summary totals our main fully-fleshed PO.
-    refreshOrderSummary(): Promise<any> {
+    refreshOrderSummary(update_po_items = false): Promise<any> {
 
+        const flesh = Object.assign({
+            flesh_price_summary: true
+        });
+        if (update_po_items) {
+            flesh['flesh_po_items'] = true;
+            flesh['flesh_po_items_further'] = true;
+        }
         return this.net.request('open-ils.acq',
             'open-ils.acq.purchase_order.retrieve.authoritative',
             this.auth.token(), this.currentPo.id(),
-            {flesh_price_summary: true}
+            flesh
 
         ).toPromise().then(po => {
 
             this.currentPo.amount_encumbered(po.amount_encumbered());
             this.currentPo.amount_spent(po.amount_spent());
             this.currentPo.amount_estimated(po.amount_estimated());
+            if (update_po_items) {
+                this.currentPo.po_items(po.po_items());
+            }
         });
     }
 }