LP1952931 ASN Receiving...
authorBill Erickson <berickxx@gmail.com>
Wed, 2 Feb 2022 18:04:34 +0000 (13:04 -0500)
committerBill Erickson <berickxx@gmail.com>
Wed, 21 Sep 2022 15:29:35 +0000 (11:29 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Open-ILS/src/eg2/src/app/staff/acq/asn/receive.component.html
Open-ILS/src/eg2/src/app/staff/acq/asn/receive.component.ts
Open-ILS/src/perlmods/lib/OpenILS/Application/Acq/Order.pm

index 68d4005..03b9053 100644 (file)
         <button class="btn btn-outline-secondary" 
           (click)="findContainer()" i18n>Submit</button>
       </div>
+      <div class="form-check form-check-inline ml-2">
+        <input class="form-check-input" type="checkbox" 
+          id="receive-on-scan" [(ngModel)]="receiveOnScan"/>
+        <label class="form-check-label" for="receive-on-scan">Receive on Scan</label>
+      </div>
     </div>
   </div>
 </div>
@@ -111,11 +116,55 @@ across different vendors to match a container code.
   </a>
 </ng-template>
 
-<eg-grid *ngIf="container" #grid [dataSource]="gridDataSource" 
+<div class="row" *ngIf="receiving">
+  <div class="col-lg-8 offset-lg-2">
+    <div class="card">
+      <div class="card-header" i18n>Receiving Items <span *ngIf="dryRun"> (Dry Run)</span></div>
+      <div class="card-body">
+        <ul class="list-group list-group-flush">
+          <li class="list-group-item">
+            <eg-progress-inline min="0" max="0" #progress></eg-progress-inline>
+          </li>
+
+          <li class="list-group-item d-flex font-weight-bold">
+            <div class="flex-1" i18n>Lineitem</div>
+            <div class="flex-1" i18n>Notified</div>
+            <div class="flex-1" i18n>Received</div>
+          </li>
+
+          <li class="list-group-item d-flex" *ngFor="let li of receiveResponse.lineitems">
+            <div class="flex-1">
+              <a routerLink="/staff/acq/po/{{li.po}}/lineitem/{{li.id}}/items"
+                target="_blank">#{{li.id}}</a>
+            </div>
+            <div class="flex-1">{{liWantedCount(li.id)}}</div>
+            <div class="flex-1" 
+              [ngClass]="{
+                'text-success': liWantedCount(li.id) == li.lids.length,
+                'text-danger': liWantedCount(li.id) > li.lids.length
+              }">
+              {{li.lids.length}}</div>
+          </li>
+        </ul>
+      </div>
+      <div class="card-footer d-flex">
+        <div class="flex-1"></div>
+        <button (click)="clearReceiving()" class="btn btn-outline-dark" i18n>
+          Close
+        </button>
+      </div>
+    </div>
+  </div>
+</div>
+
+<eg-grid *ngIf="container && !receiving" #grid [dataSource]="gridDataSource" 
   pageSize="50" (onRowActivate)="openLi($event)">
 
   <eg-grid-toolbar-button i18n-label label="Receive All Items"
     (onClick)="receiveAllItems()"></eg-grid-toolbar-button> 
+    
+  <eg-grid-toolbar-checkbox i18n-label label="Dry Run" [initialValue]="true" 
+    (onChange)="dryRun = !dryRun"></eg-grid-toolbar-checkbox> 
 
   <eg-grid-column i18n-label label="Entry ID" path="entry.id" 
     [index]="true" [hidden]="true"></eg-grid-column>
index 0f5ad15..4108a05 100644 (file)
@@ -5,10 +5,20 @@ import {Observable, Observer, of, from} from 'rxjs';
 import {tap} from 'rxjs/operators';
 import {IdlObject} from '@eg/core/idl.service';
 import {PcrudService} from '@eg/core/pcrud.service';
+import {NetService} from '@eg/core/net.service';
+import {AuthService} from '@eg/core/auth.service';
 import {LineitemService} from '../lineitem/lineitem.service';
 import {Pager} from '@eg/share/util/pager';
 import {GridDataSource, GridColumn, GridCellTextGenerator} from '@eg/share/grid/grid';
 import {GridComponent} from '@eg/share/grid/grid.component';
+import {ProgressInlineComponent} from '@eg/share/dialog/progress-inline.component';
+
+interface ReceiveResponse {
+    progress: number;
+    lineitems: any[];
+    complete: boolean;
+    po: number;
+}
 
 @Component({
   templateUrl: 'receive.component.html'
@@ -16,13 +26,19 @@ import {GridComponent} from '@eg/share/grid/grid.component';
 export class AsnReceiveComponent implements OnInit {
 
     barcode = '';
+    receiving = false;
+    dryRun = true;
+    receiveOnScan = false;
 
     // Technically possible for one container code to match across providers.
     container: IdlObject;
     entries: IdlObject[] = [];
     containers: IdlObject[] = [];
+    receiveResponse: ReceiveResponse;
 
     @ViewChild('grid') private grid: GridComponent;
+    @ViewChild('progress') private progress: ProgressInlineComponent;
+
     gridDataSource: GridDataSource = new GridDataSource();
 
     constructor(
@@ -30,6 +46,8 @@ export class AsnReceiveComponent implements OnInit {
         private router: Router,
         private ngLocation: Location,
         private pcrud: PcrudService,
+        private net: NetService,
+        private auth: AuthService,
         private li: LineitemService
     ) {}
 
@@ -62,6 +80,7 @@ export class AsnReceiveComponent implements OnInit {
     }
 
     findContainer() {
+        this.receiving = false;
         this.container = null;
         this.containers = [];
         this.entries = [];
@@ -70,19 +89,22 @@ export class AsnReceiveComponent implements OnInit {
             {container_code: this.barcode},
             {flesh: 1, flesh_fields: {acqsn: ['entries', 'provider']}}
         ).subscribe(
-          sn => this.containers.push(sn),
-          _ => {},
-          () => {
-
-              // TODO handle multiple containers w/ same code
-              if (this.containers.length === 1) {
-                  this.container = this.containers[0];
-                  this.loadContainer();
-              }
-
-              const node = document.getElementById('barcode-search-input');
-              (node as HTMLInputElement).select();
-          }
+            sn => this.containers.push(sn),
+            _ => {},
+            () => {
+
+                // TODO handle multiple containers w/ same code
+                if (this.containers.length === 1) {
+                    this.container = this.containers[0];
+                    this.loadContainer();
+                    if (this.receiveOnScan) {
+                        this.receiveAllItems();
+                    }
+                }
+
+                const node = document.getElementById('barcode-search-input');
+                (node as HTMLInputElement).select();
+            }
         );
     }
 
@@ -103,7 +125,11 @@ export class AsnReceiveComponent implements OnInit {
             _ => {},
             () => {
                 this.entries = entries;
-                this.grid.reload();
+
+                if (this.grid) {
+                    // Hidden of receiveOnScan
+                    this.grid.reload();
+                }
             }
         );
     }
@@ -133,7 +159,44 @@ export class AsnReceiveComponent implements OnInit {
     }
 
     receiveAllItems() {
-        alert('TODO');
+        this.receiving = true;
+
+        this.receiveResponse = {
+            progress: 0,
+            lineitems: [],
+            complete: false,
+            po: null
+        };
+
+        setTimeout(() => // Allow time to render
+            this.progress.update({value: 0, max: this.affectedItemsCount()}));
+
+        let method = 'open-ils.acq.shipment_notification.receive_items';
+        if (this.dryRun) { method += '.dry_run'; }
+
+        this.net.request('open-ils.acq',
+            method, this.auth.token(), this.container.id())
+        .subscribe(
+            resp => {
+                this.progress.update({value: resp.progress});
+                console.debug('ASN Receive returned', resp);
+                this.receiveResponse = resp;
+            },
+            err => {},
+            () => {
+            }
+        );
+    }
+
+    clearReceiving() {
+        this.receiving = false;
+        this.findContainer();
+    }
+
+    liWantedCount(liId: number): number {
+        const entry = this.entries.filter(e => e.lineitem().id())[0];
+        if (entry) { return entry.item_count(); }
+        return 0;
     }
 }
 
index 475c61f..a101892 100644 (file)
@@ -4321,5 +4321,129 @@ sub li_existing_copies {
 }
 
 
+__PACKAGE__->register_method(
+    method => 'asn_receive_items',
+    api_name => 'open-ils.acq.shipment_notification.receive_items',
+    max_bundle_count => 1,
+    signature => {
+        desc => q/
+            Mark items from a shipment notification as received.
+        /,
+        params => [
+            {desc => 'Authentication token', type => 'string'},
+            {desc => 'Shipment Notification ID', type => 'number'}
+        ],
+        return => {desc => q/Stream of status updates, event on error/}
+    }
+);
+
+__PACKAGE__->register_method(
+    method => 'asn_receive_items',
+    api_name => 'open-ils.acq.shipment_notification.receive_items.dry_run',
+    max_bundle_count => 1,
+    signature => q/dry_run variant of open-ils.acq.shipment_notification.receive_items/
+);
+
+sub asn_receive_items {
+    my ($self, $client, $auth, $asn_id) = @_;
+
+    my $e = new_editor(xact => 1, authtoken => $auth);
+    return $e->die_event unless $e->checkauth;
+
+    my $mgr = OpenILS::Application::Acq::BatchManager->new(
+        editor => $e, conn => $client, throttle => 1);
+
+    my $asn = $e->retrieve_acq_shipment_notification([$asn_id, 
+        {flesh => 1, flesh_fields => {acqsn => ['provider', 'entries']}}
+    ]) || return $e->die_event;
+
+    return $e->die_event unless 
+        $e->allowed('MANAGE_SHIPMENT_NOTIFICATION', $asn->provider->owner);
+
+    my $resp = {
+        lineitems => [],
+        progress => 0,
+    };
+
+    my @entries = sort {$a->lineitem cmp $b->lineitem} @{$asn->entries};
+
+    for my $entry (@entries) {
+
+        my $li = $e->retrieve_acq_lineitem($entry->lineitem)
+            or return $e->die_event;
+
+        my $li_resp = {
+            id => $entry->lineitem, 
+            po => $li->purchase_order, 
+            lids => []
+        };
+
+        push(@{$resp->{lineitems}}, $li_resp);
+            
+        # Include canceled items.
+        my $lids = $e->search_acq_lineitem_detail([{  
+            lineitem => $entry->lineitem, 
+            recv_time => undef
+        }, {   
+            flesh => 1,
+            flesh_fields => {acqlid => ['cancel_reason']}
+        }]);
+            
+        # Start by receiving un-canceled items.  
+        # Then try "delayed" items if it comes to that.
+        # Apply sorting for consistency with dry-run.
+
+        my @active_lids = sort {$a->id cmp $b->id} 
+            grep {!$_->cancel_reason} @$lids;
+
+        my @canceled_lids = sort {$a->id cmp $b->id} 
+            grep { $_->cancel_reason && $U->is_true($_->cancel_reason->keep_debits)
+        } @$lids;
+
+        my @potential_lids = (@active_lids, @canceled_lids);
+
+        if (scalar(@potential_lids) < $entry->item_count) {
+            $logger->warn(sprintf(
+                "ASN $asn_id entry %d found %d receivable items for lineitem %d, but wanted %d",
+                $entry->id, scalar(@potential_lids), $entry->lineitem, $entry->item_count
+            ));
+        }
+
+        my $recv_count = 0;
+
+        for my $lid (@potential_lids) {
+
+            return $e->die_event unless receive_lineitem_detail($mgr, $lid->id);
+
+            # Get an updated copy to pick up the recv_time
+            $lid = $e->retrieve_acq_lineitem_detail($lid->id);
+
+            my $note = $lid->note ? $lid->note . "\n" : '';
+            $note .= "Received via shipment notification #$asn_id";
+            $lid->note($note);
+
+            $e->update_acq_lineitem_detail($lid) or return $e->die_event;
+
+            push(@{$li_resp->{lids}}, $lid->id);
+            $resp->{progress}++;
+            $client->respond($resp);
+
+            last if ++$recv_count >= $entry->item_count;
+        }
+    }
+
+    if ($self->api_name =~ /dry_run/) {
+        $e->rollback;
+    } else {
+        $e->commit;
+    }
+
+    $resp->{complete} = 1;
+    $client->respond_complete($resp);
+
+    undef;
+}
+
+
 1;