<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>
</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>
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'
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(
private router: Router,
private ngLocation: Location,
private pcrud: PcrudService,
+ private net: NetService,
+ private auth: AuthService,
private li: LineitemService
) {}
}
findContainer() {
+ this.receiving = false;
this.container = null;
this.containers = [];
this.entries = [];
{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();
+ }
);
}
_ => {},
() => {
this.entries = entries;
- this.grid.reload();
+
+ if (this.grid) {
+ // Hidden of receiveOnScan
+ this.grid.reload();
+ }
}
);
}
}
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;
}
}
}
+__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;