InwardOS
Tell us who you are and what you will do
Capture Document

Ready to scan

All pages save in ONE entry. Tap page to view, × to remove.
Invoice Details
Saved locally - Demo Mode
Recent Syncs
Invoice Photos
Rejected Material
Which invoice's material got rejected - live from ERP Sheet.

Loading QC list...

Configuration
Preferences
Download Android app
APK - install directly on Android phones
Install app
Add to home screen
Supervisor unlock
Master password to change the locked role
Auto Extract (OCR)
Read text automatically
Auto-Enhance Photo
Improve text readability
Save Image
Upload image to Drive
Haptics
Vibrate on action
Apps Script Code
doPost.gs
function doPost(e) {
  var data = JSON.parse(e.postData.contents);
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  if (data.action === 'qcReview') {
    var qc0 = ss.getSheetByName('QC') || ss.insertSheet('QC');
    qc0.appendRow([new Date(), data.inv, data.id, 'Reviewed', data.name || '']);
    return ContentService.createTextOutput(JSON.stringify({status: 'success'})).setMimeType(ContentService.MimeType.JSON);
  }
  if (data.action === 'qc_rejection') {
    var qr = ss.getSheetByName('QC_Rejections') || ss.insertSheet('QC_Rejections');
    if (qr.getLastRow() === 0) qr.appendRow(['Timestamp', 'ID', 'Invoice No', 'Supplier', 'Item', 'Size', 'Qty', 'Reason', 'Status', 'By']);
    var rejRows = data.rows || [];
    for (var ri = 0; ri < rejRows.length; ri++) {
      qr.appendRow([new Date(), rejRows[ri].id, rejRows[ri].inv, rejRows[ri].sup, rejRows[ri].item, rejRows[ri].size, rejRows[ri].qty, rejRows[ri].reason, 'Pending', rejRows[ri].name || '']);
    }
    return ContentService.createTextOutput(JSON.stringify({status: 'success'})).setMimeType(ContentService.MimeType.JSON);
  }
  var sheet = ss.getActiveSheet();
  var imgs = (data.imagesBase64 && data.imagesBase64.length) ? data.imagesBase64 : (data.imageBase64 ? [data.imageBase64] : []);
  var urls = [];
  for (var i = 0; i < imgs.length; i++) {
    var b64 = imgs[i].split(',')[1];
    var blob = Utilities.newBlob(Utilities.base64Decode(b64), 'image/jpeg', data.erpKey + '_p' + (i + 1) + '.jpg');
    var file = DriveApp.createFile(blob);
    file.setSharing(DriveApp.Access.ANYONE_WITH_LINK, DriveApp.Permission.VIEW);
    urls.push(file.getUrl());
  }
  sheet.appendRow([new Date(), data.invoiceNo, data.supplierName, data.invoiceDate, data.invoiceName, data.erpKey, (data.pageCount || 1), urls.join(' | '), data.operator || '']);
  return ContentService.createTextOutput(JSON.stringify({status: 'success'})).setMimeType(ContentService.MimeType.JSON);
}

function doGet(e) {
  var ss = SpreadsheetApp.getActiveSpreadsheet();
  var out = { rejections: [], invoices: [] };
  var qc = ss.getSheetByName('QC');
  if (qc) {
    var rows = qc.getDataRange().getValues();
    // QC sheet columns: Date | InvoiceNo | Supplier | Item | Qty | Reason | Status
    for (var i = 1; i < rows.length; i++) {
      out.rejections.push({ id: 'row' + i, inv: String(rows[i][1]), sup: String(rows[i][2]), item: String(rows[i][3]), qty: String(rows[i][4]), reason: String(rows[i][5]), date: String(rows[i][0]), status: String(rows[i][6] || 'Pending') });
    }
  }
  var qrGet = ss.getSheetByName('QC_Rejections');
  if (qrGet) {
    var qrows = qrGet.getDataRange().getValues();
    // QC_Rejections columns: Timestamp | ID | InvoiceNo | Supplier | Item | Size | Qty | Reason | Status
    for (var qi = 1; qi < qrows.length; qi++) {
      if (!qrows[qi][1]) continue;
      out.rejections.push({ id: String(qrows[qi][1]), inv: String(qrows[qi][2]), sup: String(qrows[qi][3]), item: String(qrows[qi][4]), size: String(qrows[qi][5]), qty: String(qrows[qi][6]), reason: String(qrows[qi][7]), date: String(qrows[qi][0]), status: String(qrows[qi][8] || 'Pending') });
    }
  }
  // Main sheet: Timestamp | InvoiceNo | Supplier | InvoiceDate | Type | ErpKey | PageCount | PhotoURLs
  function driveView(u) {
    var m = String(u).match(/\/d\/([-\w]{20,})/) || String(u).match(/[?&]id=([-\w]{20,})/);
    if (!m) return { thumb: u, full: u };
    return { thumb: 'https://drive.google.com/thumbnail?id=' + m[1] + '&sz=w1600', full: 'https://lh3.googleusercontent.com/d/' + m[1] };
  }
  var main = ss.getSheets()[0];
  var mrows = main.getDataRange().getValues();
  for (var j = 1; j < mrows.length; j++) {
    if (!mrows[j][1]) continue;
    var c6 = String(mrows[j][6] || '');
    var c7 = String(mrows[j][7] || '');
    var raw = c7 ? c7.split(' | ').filter(Boolean) : (c6.indexOf('http') === 0 ? [c6] : []);
    var fulls = [], thumbs = [];
    for (var k = 0; k < raw.length; k++) {
      var dv = driveView(raw[k]);
      fulls.push(dv.full);
      thumbs.push(dv.thumb);
    }
    out.invoices.push({ inv: String(mrows[j][1]), sup: String(mrows[j][2]), date: String(mrows[j][3]), pages: fulls, full: fulls, thumbs: thumbs });
  }
  return ContentService.createTextOutput(JSON.stringify(out)).setMimeType(ContentService.MimeType.JSON);
}
Success
Invoice
Add Rejection
Item Name
Size & Rejected Qty tap sizes, set qty per size
Letter sizes
Number sizes
Reason
1 / 1
Full screen document
Tap = zoom