LP1904036 Barcode checkdigit / dialog support
authorBill Erickson <berickxx@gmail.com>
Wed, 12 May 2021 19:12:31 +0000 (15:12 -0400)
committerGalen Charlton <gmc@equinoxOLI.org>
Fri, 28 Oct 2022 00:13:34 +0000 (20:13 -0400)
Signed-off-by: Bill Erickson <berickxx@gmail.com>
Signed-off-by: Jane Sandberg <js7389@princeton.edu>
Signed-off-by: Galen Charlton <gmc@equinoxOLI.org>
Open-ILS/src/eg2/src/app/staff/circ/checkin/checkin.component.ts
Open-ILS/src/eg2/src/app/staff/circ/patron/checkout.component.ts
Open-ILS/src/eg2/src/app/staff/circ/renew/renew.component.ts
Open-ILS/src/eg2/src/app/staff/share/circ/bad-barcode-dialog.component.html [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/circ/bad-barcode-dialog.component.ts [new file with mode: 0644]
Open-ILS/src/eg2/src/app/staff/share/circ/circ.module.ts
Open-ILS/src/eg2/src/app/staff/share/circ/circ.service.ts
Open-ILS/src/eg2/src/app/staff/share/circ/components.component.html
Open-ILS/src/eg2/src/app/staff/share/circ/components.component.ts

index e329317..fcb897c 100644 (file)
@@ -180,7 +180,8 @@ export class CheckinComponent implements OnInit, AfterViewInit {
 
         const params: CheckinParams = {
             copy_barcode: this.barcode,
-            backdate: this.backdate
+            backdate: this.backdate,
+            _checkbarcode: this.strictBarcode
         };
 
         Object.keys(this.modifiers).forEach(mod => {
index 30cf801..ed99cb5 100644 (file)
@@ -115,6 +115,7 @@ export class CheckoutComponent implements OnInit, AfterViewInit {
 
         const params: CheckoutParams = {
             patron_id: this.context.summary.id,
+            _checkbarcode: this.strictBarcode,
             _worklog: {
                 user: this.context.summary.patron.family_name(),
                 patron_id: this.context.summary.id
index 7cd30ea..473014c 100644 (file)
@@ -137,7 +137,7 @@ export class RenewComponent implements OnInit, AfterViewInit {
         const params: CheckoutParams = {
             copy_barcode: this.barcode,
             due_date: this.useDueDate ? this.dueDate : null,
-            _renewal: true
+            _checkbarcode: this.strictBarcode
         };
 
         return this.barcodeSelect.getBarcode('asset', this.barcode)
diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/bad-barcode-dialog.component.html b/Open-ILS/src/eg2/src/app/staff/share/circ/bad-barcode-dialog.component.html
new file mode 100644 (file)
index 0000000..f7921a4
--- /dev/null
@@ -0,0 +1,27 @@
+<ng-template #dialogContent>
+  <div class="modal-header bg-info">
+    <h4 i18n>Bad Barcode</h4>
+    <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">
+    <img src="/images/bad_barcode.png"/>
+    <p class="mt-2">Invalid barcode: <b>{{barcode}}</b></p>
+    <p class="mt-2">
+      Your entry has a bad check digit, possibly due to a bad scan.
+      Choose Cancel to try again, or Accept to use barcode "{{barcode}}" anyway.
+    </p>
+  </div>
+  <div class="modal-footer">
+    <button type="button" class="btn btn-info" (click)="close(true)" i18n>
+      Accept Barcode
+    </button>
+    <button type="button" class="btn btn-warning" (click)="close()" i18n>
+      Cancel
+    </button>
+  </div>
+</ng-template>
+
+
diff --git a/Open-ILS/src/eg2/src/app/staff/share/circ/bad-barcode-dialog.component.ts b/Open-ILS/src/eg2/src/app/staff/share/circ/bad-barcode-dialog.component.ts
new file mode 100644 (file)
index 0000000..e4427e5
--- /dev/null
@@ -0,0 +1,20 @@
+import {Component, OnInit, Output, Input, ViewChild, EventEmitter} from '@angular/core';
+import {NgbModal, NgbModalOptions} from '@ng-bootstrap/ng-bootstrap';
+import {DialogComponent} from '@eg/share/dialog/dialog.component';
+
+/** Bad Item Barcode Dialog */
+
+@Component({
+  templateUrl: 'bad-barcode-dialog.component.html',
+  selector: 'eg-bad-barcode-dialog'
+})
+export class BadBarcodeDialogComponent extends DialogComponent {
+
+    barcode: string;
+
+    constructor(private modal: NgbModal) {
+        super(modal);
+    }
+}
+
+
index fef714a..ecd51be 100644 (file)
@@ -15,6 +15,7 @@ import {CopyInTransitDialogComponent} from './in-transit-dialog.component';
 import {CancelTransitDialogComponent} from './cancel-transit-dialog.component';
 import {BackdateDialogComponent} from './backdate-dialog.component';
 import {WorkLogModule} from '@eg/staff/share/worklog/worklog.module';
+import {BadBarcodeDialogComponent} from './bad-barcode-dialog.component';
 
 @NgModule({
     declarations: [
@@ -28,6 +29,7 @@ import {WorkLogModule} from '@eg/staff/share/worklog/worklog.module';
         BackdateDialogComponent,
         CopyInTransitDialogComponent,
         CancelTransitDialogComponent,
+        BadBarcodeDialogComponent,
         OpenCircDialogComponent
     ],
     imports: [
index 2f4cf01..c339b73 100644 (file)
@@ -130,6 +130,7 @@ export interface CheckoutParams {
     // internal tracking
     _override?: boolean;
     _renewal?: boolean;
+    _checkbarcode?: boolean;
     _worklog?: WorkLogEntry;
 }
 
@@ -184,6 +185,7 @@ export interface CheckinParams {
     // internal / local values that are moved from the API request.
     _override?: boolean;
     _worklog?: WorkLogEntry;
+    _checkbarcode?: boolean;
 }
 
 export interface CheckinResult extends CircResultCommon {
@@ -383,11 +385,15 @@ export class CircService {
         let method = 'open-ils.circ.checkout.full';
         if (params._override) { method += '.override'; }
 
-        return this.net.request(
-            'open-ils.circ', method,
-            this.auth.token(), this.apiParams(params)).toPromise()
-        .then(result => this.unpackCheckoutData(params, result))
-        .then(result => this.processCheckoutResult(result));
+        return this.inspectBarcode(params).then(barcodeOk => {
+            if (!barcodeOk) { return null; }
+
+            return this.net.request(
+                'open-ils.circ', method,
+                this.auth.token(), this.apiParams(params)).toPromise()
+            .then(result => this.unpackCheckoutData(params, result))
+            .then(result => this.processCheckoutResult(result));
+        });
     }
 
     renew(params: CheckoutParams): Promise<CheckoutResult> {
@@ -399,11 +405,15 @@ export class CircService {
         let method = 'open-ils.circ.renew';
         if (params._override) { method += '.override'; }
 
-        return this.net.request(
-            'open-ils.circ', method,
-            this.auth.token(), this.apiParams(params)).toPromise()
-        .then(result => this.unpackCheckoutData(params, result))
-        .then(result => this.processCheckoutResult(result));
+        return this.inspectBarcode(params).then(barcodeOk => {
+            if (!barcodeOk) { return null; }
+
+            return this.net.request(
+                'open-ils.circ', method,
+                this.auth.token(), this.apiParams(params)).toPromise()
+            .then(result => this.unpackCheckoutData(params, result))
+            .then(result => this.processCheckoutResult(result));
+        });
     }
 
 
@@ -708,11 +718,15 @@ export class CircService {
         let method = 'open-ils.circ.checkin';
         if (params._override) { method += '.override'; }
 
-        return this.net.request(
-            'open-ils.circ', method,
-            this.auth.token(), this.apiParams(params)).toPromise()
-        .then(result => this.unpackCheckinData(params, result))
-        .then(result => this.processCheckinResult(result));
+        return this.inspectBarcode(params).then(barcodeOk => {
+            if (!barcodeOk) { return null; }
+
+            return this.net.request(
+                'open-ils.circ', method,
+                this.auth.token(), this.apiParams(params)).toPromise()
+            .then(result => this.unpackCheckinData(params, result))
+            .then(result => this.processCheckinResult(result));
+        });
     }
 
     fleshCommonData(result: CircResultCommon): Promise<CircResultCommon> {
@@ -1153,5 +1167,59 @@ export class CircService {
             {order_by : {circ : 'xact_start desc' }, limit : 1}
         ).toPromise();
     }
+
+    // Resolves to true if the barcode is OK or the user confirmed it or
+    // the user doesn't care to begin with
+    inspectBarcode(params: CheckoutParams | CheckinParams): Promise<boolean> {
+        if (!params._checkbarcode || !params.copy_barcode) {
+            return Promise.resolve(true);
+        }
+
+        if (this.checkBarcode(params.copy_barcode)) {
+            // Avoid prompting again on an override
+            params._checkbarcode = false;
+            return Promise.resolve(true);
+        }
+
+        this.components.badBarcodeDialog.barcode = params.copy_barcode;
+        return this.components.badBarcodeDialog.open().toPromise();
+    }
+
+    checkBarcode(barcode: string): boolean {
+        if (barcode !== Number(barcode).toString()) { return false; }
+
+        const bc = barcode.toString();
+
+        // "16.00" == Number("16.00"), but the . is bad.
+        // Throw out any barcode that isn't just digits
+        if (bc.search(/\D/) !== -1) { return false; }
+
+        const lastDigit = bc.substr(bc.length - 1);
+        const strippedBarcode = bc.substr(0, bc.length - 1);
+        return this.barcodeCheckdigit(strippedBarcode).toString() === lastDigit;
+    }
+
+    barcodeCheckdigit(bc: string): number {
+        let checkSum = 0;
+        let multiplier = 2;
+        const reverseBarcode = bc.toString().split('').reverse();
+
+        reverseBarcode.forEach(ch => {
+            let tempSum = 0;
+            const product = (Number(ch) * multiplier) + '';
+            product.split('').forEach(num => tempSum += Number(num));
+            checkSum += Number(tempSum);
+            multiplier = multiplier === 2 ? 1 : 2;
+        });
+
+        const cSumStr = checkSum.toString();
+        const nextMultipleOf10 =
+            (Number(cSumStr.match(/(\d*)\d$/)[1]) * 10) + 10;
+
+        let checkDigit = nextMultipleOf10 - Number(cSumStr);
+        if (checkDigit === 10) { checkDigit = 0; }
+
+        return checkDigit;
+    }
 }
 
index fa41b3c..691e917 100644 (file)
@@ -67,3 +67,5 @@
 
 <eg-copy-alert-manager #copyAlertManager></eg-copy-alert-manager>
 
+<eg-bad-barcode-dialog #badBarcodeDialog></eg-bad-barcode-dialog>
+
index 76dc5f5..e2ff1ee 100644 (file)
@@ -9,6 +9,7 @@ import {RouteDialogComponent} from './route-dialog.component';
 import {CopyInTransitDialogComponent} from './in-transit-dialog.component';
 import {CopyAlertManagerDialogComponent
     } from '@eg/staff/share/holdings/copy-alert-manager.component';
+import {BadBarcodeDialogComponent} from './bad-barcode-dialog.component';
 
 /* Container component for sub-components used by circulation actions.
  *
@@ -36,6 +37,7 @@ export class CircComponentsComponent {
 
     @ViewChild('holdShelfStr') holdShelfStr: StringComponent;
     @ViewChild('catalogingStr') catalogingStr: StringComponent;
+    @ViewChild('badBarcodeDialog') badBarcodeDialog: BadBarcodeDialogComponent;
 
     constructor(private circ: CircService) {
         this.circ.components = this;