Most AI demos around ERP systems follow the same pattern.
A user opens a chatbot.
The chatbot asks questions.
The chatbot calls an API.
The chatbot returns an answer.
That is useful, but it still sits outside the real transaction flow.
For Episode 5, I wanted to test a different pattern: what happens when the agent is embedded directly inside the PeopleSoft page where the user is already working?
The result is an AI-powered invoice parser embedded inside the PeopleSoft Finance Voucher Entry page.
The goal is simple: reduce manual invoice data entry while still keeping PeopleSoft as the system of control.
Use Case
In PeopleSoft Finance, voucher entry is still a manual-heavy process in many organizations.
A user may receive an invoice PDF and then manually enter:
- Supplier name
- Invoice number
- Invoice date
- Gross amount
- Sales tax
- Freight
- Purchase order number
This is not complex work, but it is repetitive. It is also easy to make mistakes, especially when users are switching between a PDF and the PeopleSoft page.
So the idea was:
Upload an invoice PDF directly from the Voucher Entry page, extract the values, and populate the PeopleSoft fields for user review.
Not auto-save.
Not bypass validation.
Not replace PeopleSoft business rules.
Just assist the user at the point of entry.
What I Built
I created a lightweight embedded widget inside PeopleSoft.
The widget appears as a floating assistant button on the PeopleSoft page. When opened, it provides an upload option for an invoice PDF.
But the upload button is not blindly enabled everywhere. The script checks the current PeopleSoft component context and enables the upload only when the user is on the configured Voucher Entry component, VCHR_EXPRESS.
That matters because this is not just a generic browser extension style demo. It is page-aware.
The widget understands where it is running inside PeopleSoft.
High-level flow:
- User opens PeopleSoft Voucher Entry.
- Embedded widget checks the current component.
- User uploads invoice PDF.
- PDF is sent to Azure Document Intelligence using the prebuilt invoice model.
- Extracted data is normalized.
- Supplier name is populated first.
- PeopleSoft server-side validation is triggered only for supplier name.
- PeopleSoft derives Supplier ID, Short Supplier Name, Location, and Address Sequence.
- Invoice fields are populated into the page.
- User reviews the values before saving the voucher.
That last step is important. The user remains in control.
Why This Is Different from a Chatbot
This is not a chatbot asking, “Please upload your invoice.”
This is embedded automation inside the PeopleSoft transaction page.
The assistant knows:
- Which PeopleSoft component is active
- Which fields exist on the page
- Which field should trigger PeopleSoft validation
- Which fields should be populated without server refresh
- When PeopleSoft has derived supplier details
- When values need to be reapplied after page refresh behavior
That is the difference between a generic AI assistant and a contextual enterprise assistant.
A chatbot can answer questions.
A page-aware assistant can participate in the transaction flow.
Technical Design
The demo uses JavaScript injected into the PeopleSoft page.
The widget creates:
- Floating action button
- Upload panel
- File input for PDF
- Status messages
- Parsed invoice summary
- Field population logic
- Page validation logic
The script looks for the PeopleSoft content iframe, reads the current URL, extracts the component name, and checks whether the active component matches VCHR_EXPRESS.
Only then does the upload button become active.
The invoice PDF is converted to base64 and sent to Azure Document Intelligence using the prebuilt-invoice model. The result is polled until the document analysis succeeds.
From the Azure response, the script extracts and normalizes key invoice fields:
- Vendor Name
- Invoice ID
- Invoice Date
- Due Date
- Purchase Order
- Subtotal
- Total Tax
- Freight
- Invoice Total
- Currency
- Line items
Then those values are mapped back to PeopleSoft voucher fields.
Example mapping:
vendor_name -> VCHR_ADDSRCH_VW_NAME1
invoice_number -> VCHR_ADDSRCH_VW_INVOICE_ID
invoice_date -> VCHR_ADDSRCH_VW_INVOICE_DT
gross_amount -> VCHR_ADDSRCH_VW_GROSS_AMT
sales_tax -> VCHR_ADDSRCH_VW_SALETX_AMT
freight -> VCHR_ADDSRCH_VW_FREIGHT_AMT
po_number -> VCHR_ADDSRCH_VW_PO_ID
Key Design Decision: Let PeopleSoft Derive Supplier Details
One of the most important parts of this demo is how supplier handling works.
The script does not directly populate Supplier ID, Short Supplier Name, Supplier Location, or Address Sequence.
Instead, it populates only the supplier name and triggers PeopleSoft’s delivered validation behavior.
That allows PeopleSoft to derive:
- Supplier ID
- Supplier short name
- Supplier location
- Address sequence
This is the right pattern.
AI should not become the system of record.
AI should assist the user, but PeopleSoft should still apply its own rules.
The assistant extracts a supplier name from the invoice. PeopleSoft decides how that supplier resolves inside the application.
That keeps the design safer and more realistic.
Why I Avoided Server Actions for Amount Fields
Another interesting issue came up during the demo.
When amount and date fields were populated and PeopleSoft events were triggered, some values were reset back to 0.00.
That happens because PeopleSoft pages often have server-side logic, recalculation, defaulting, and refresh behavior tied to field changes.
So I changed the design.
For supplier name, I trigger PeopleSoft submit action because I want delivered supplier validation.
For invoice fields like amount, tax, freight, invoice number, invoice date, and PO number, I populate them DOM-only.
No change.
No blur.
No submitAction.
This avoids unnecessary server-side resets.
The script also reapplies invoice values a few times after short delays to survive PeopleSoft refresh/recalculation behavior.
That may sound small, but this is where real PeopleSoft UI automation gets tricky. The hard part is not extracting invoice values. The hard part is respecting how PeopleSoft pages behave after field changes.
Why This Pattern Matters
This demo shows a practical pattern for PeopleSoft AI integration:
AI extracts or recommends. PeopleSoft validates. User reviews.
That is the balance enterprise systems need.
You do not want AI blindly writing transactional data into ERP pages and saving it.
You also do not want AI sitting completely outside the workflow.
The sweet spot is embedded assistance.
The user stays on the PeopleSoft page.
The assistant reduces manual work.
PeopleSoft keeps control of validation and transaction rules.
That same pattern can apply beyond invoices.
Examples:
- Expense entry from receipts
- Supplier onboarding document extraction
- Employee document classification
- Benefits form extraction
- Journal upload assistance
- Contract metadata extraction
- Procurement request assistance
- Page-aware help and validation guidance
The bigger idea is not invoice parsing alone.
The bigger idea is embedding contextual AI inside PeopleSoft transactions.
Demo vs Production
This version is a demo.
The current script stores the Azure Document Intelligence key directly in JavaScript. That is acceptable only for a controlled proof of concept. It should never be used like that in production.
For a production-ready design, I would move the document processing behind a secure backend layer.
A better production flow would be:
- PeopleSoft page uploads invoice to a secure backend endpoint.
- Backend validates user/session/context.
- Backend calls Azure Document Intelligence.
- Secrets stay server-side.
- Backend returns normalized invoice fields.
- PeopleSoft page populates values for review.
- PeopleSoft validation and save process remain unchanged.
The backend could be:
- Azure Function
- FastAPI service
- API Management plus backend API
- PeopleSoft Integration Broker wrapper
- Internal middleware service
The important part is this: do not expose keys in browser JavaScript.
The demo proves the user experience.
The production version needs proper security.
Code
Make sure you move the url and secret key to IScript.
/* ============================================================
AI POWERED INVOICE PARSER — PEOPLESOFT FINANCE DEMO
DEMO ONLY:
Azure key is stored in JavaScript.
Final design:
1. Upload invoice PDF
2. Parse using Azure Document Intelligence prebuilt-invoice
3. Populate Supplier Name first
4. Trigger PeopleSoft submitAction ONLY for Supplier Name
5. Wait for PeopleSoft to derive Supplier ID / Short Name / Location
6. Populate invoice fields DOM-only:
- Invoice Number
- Invoice Date
- Gross Invoice Amount
- Sales Tax Amount
- Freight Amount
- PO Number
7. Reapply invoice fields a few times to survive PeopleSoft refresh/recalc
IMPORTANT:
Do NOT trigger input/change/blur/submitAction for amount/date fields.
PeopleSoft may reset them to 0.00.
============================================================ */
(function () {
var CONFIG = {
docIntelEndpoint: 'https://xxxxxxxxxxxxx.cognitiveservices.azure.com',
docIntelKey: ' xxxxxxxxxxxxxxxx',
apiVersion: '2024-11-30',
modelId: 'prebuilt-invoice',
maxPdfBytes: 10 * 1024 * 1024,
pollIntervalMs: 1200,
maxPollAttempts: 30,
supplierResolutionTimeoutMs: 7000,
supplierResolutionPollMs: 300,
reapplyDelaysMs: [500, 1500, 3000]
};
var INVOICE_UPLOAD_COMPONENTS = ['VCHR_EXPRESS'];
/*
Only fields we intentionally write.
Supplier ID / Short Supplier Name / Supplier Location are NOT written.
PeopleSoft should derive those from Supplier Name.
*/
var VOUCHER_FIELD_MAP = {
vendor_name: 'VCHR_ADDSRCH_VW_NAME1',
invoice_number: 'VCHR_ADDSRCH_VW_INVOICE_ID',
invoice_date: 'VCHR_ADDSRCH_VW_INVOICE_DT',
gross_amount: 'VCHR_ADDSRCH_VW_GROSS_AMT',
sales_tax: 'VCHR_ADDSRCH_VW_SALETX_AMT',
freight: 'VCHR_ADDSRCH_VW_FREIGHT_AMT',
po_number: 'VCHR_ADDSRCH_VW_PO_ID'
};
/*
Read-only check fields.
Used to confirm PeopleSoft resolved supplier.
*/
var PS_DERIVED_SUPPLIER_FIELDS = {
supplier_id: 'VCHR_ADDSRCH_VW_VENDOR_ID',
vendor_short: 'VCHR_ADDSRCH_VW_VENDOR_NAME_SHORT',
vendor_location: 'VCHR_ADDSRCH_VW_VNDR_LOC',
address_seq: 'VCHR_ADDSRCH_VW_ADDR_SEQ_NUM'
};
function domReady(fn) {
if (document.body) {
fn();
} else if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', fn);
} else {
setTimeout(fn, 300);
}
}
function initInvoiceParserWidget() {
if (document.getElementById('invoice-ai-fab')) return;
if (!document.body) {
setTimeout(initInvoiceParserWidget, 300);
return;
}
injectStyles();
buildWidgetShell();
wireWidgetEvents();
setTimeout(function () {
var tooltip = document.getElementById('invoice-ai-tooltip');
if (tooltip) {
tooltip.classList.add('iai-show');
setTimeout(function () {
tooltip.classList.remove('iai-show');
}, 3500);
}
}, 1800);
setInterval(refreshUploadButton, 2000);
refreshUploadButton();
}
function injectStyles() {
var css =
'#invoice-ai-fab{position:fixed!important;bottom:24px!important;right:24px!important;width:56px!important;height:56px!important;border-radius:50%!important;background:linear-gradient(135deg,#1a4d2e,#2e8b57)!important;border:none!important;cursor:pointer!important;box-shadow:0 4px 16px rgba(26,77,46,.45)!important;z-index:2147483647!important;display:flex!important;align-items:center!important;justify-content:center!important;padding:0!important;transition:transform .2s ease!important;}'
+ '#invoice-ai-fab:hover{transform:scale(1.08)!important;}'
+ '#invoice-ai-badge{position:absolute;top:-2px;right:-2px;width:18px;height:18px;background:#e74c3c;border-radius:50%;font-size:10px;font-weight:700;color:white;display:flex;align-items:center;justify-content:center;border:2px solid white;pointer-events:none;}'
+ '#invoice-ai-tooltip{position:fixed!important;bottom:90px!important;right:24px!important;background:#1a4d2e;color:white;padding:8px 14px;border-radius:20px;font-size:12px;font-weight:500;white-space:nowrap;box-shadow:0 3px 12px rgba(0,0,0,.2);z-index:2147483646!important;opacity:0;transform:translateY(6px);transition:opacity .3s,transform .3s;pointer-events:none;font-family:Segoe UI,sans-serif;}'
+ '#invoice-ai-tooltip.iai-show{opacity:1;transform:translateY(0);}'
+ '#invoice-ai-tooltip:after{content:"";position:absolute;bottom:-5px;right:20px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #1a4d2e;}'
+ '#invoice-ai-panel{position:fixed!important;bottom:92px!important;right:24px!important;width:390px!important;height:560px!important;border-radius:16px!important;overflow:hidden!important;box-shadow:0 8px 40px rgba(0,0,0,.22)!important;border:1px solid #d0e0d0!important;display:flex!important;flex-direction:column!important;background:#fff!important;z-index:2147483645!important;transform:scale(.85) translateY(20px);transform-origin:bottom right;opacity:0;pointer-events:none;transition:transform .25s cubic-bezier(.34,1.56,.64,1),opacity .2s ease;}'
+ '#invoice-ai-panel.iai-open{transform:scale(1) translateY(0)!important;opacity:1!important;pointer-events:all!important;}'
+ '#invoice-ai-hdr{background:linear-gradient(135deg,#1a4d2e,#2e8b57);padding:14px 16px;display:flex;align-items:center;gap:10px;flex-shrink:0;}'
+ '#invoice-ai-hdr h3{font-size:13px;font-weight:600;margin:0;color:white;font-family:Segoe UI,sans-serif;}'
+ '#invoice-ai-hdr p{font-size:11px;opacity:.75;margin:2px 0 0;color:white;font-family:Segoe UI,sans-serif;}'
+ '#invoice-ai-hdr-status{margin-left:auto;display:flex;align-items:center;gap:5px;font-size:11px;color:white;opacity:.85;flex-shrink:0;font-family:Segoe UI,sans-serif;}'
+ '#invoice-ai-hdrdot{width:7px;height:7px;background:#4ade80;border-radius:50%;animation:iaiBlink 2s infinite;}'
+ '#invoice-ai-closebtn{background:rgba(255,255,255,.2);border:none;color:white;width:26px;height:26px;border-radius:50%;cursor:pointer;font-size:14px;line-height:26px;text-align:center;flex-shrink:0;margin-left:6px;}'
+ '@keyframes iaiBlink{0%,100%{opacity:1;}50%{opacity:.35;}}'
+ '#invoice-ai-body{flex:1;overflow:auto;position:relative;padding:14px;background:#ffffff;font-family:Segoe UI,sans-serif;}'
+ '#invoice-ai-summary{font-size:13px;color:#333;line-height:1.45;}'
+ '#invoice-ai-summary h4{margin:0 0 8px 0;font-size:14px;color:#1a4d2e;}'
+ '#invoice-ai-summary p{margin:6px 0;}'
+ '#invoice-ai-summary table{width:100%;border-collapse:collapse;margin-top:10px;font-size:12px;}'
+ '#invoice-ai-summary th,#invoice-ai-summary td{border-bottom:1px solid #e5e5e5;text-align:left;padding:6px 4px;vertical-align:top;}'
+ '#invoice-ai-summary th{color:#1a4d2e;font-weight:600;}'
+ '#invoice-ai-summary code{background:#f4f4f4;padding:2px 4px;border-radius:4px;}'
+ '#invoice-ai-action-bar{display:flex;align-items:center;gap:8px;padding:8px 12px;background:#f8faf8;border-top:1px solid #e0e8e0;flex-shrink:0;}'
+ '#invoice-ai-upload-btn{width:38px;height:38px;border-radius:50%;background:#2e8b57;border:2px solid transparent;cursor:pointer;display:flex;align-items:center;justify-content:center;flex-shrink:0;transition:background .2s;}'
+ '#invoice-ai-upload-btn:hover{background:#1a4d2e;}'
+ '#invoice-ai-upload-btn[disabled]{background:#aaa;cursor:not-allowed;}'
+ '#invoice-ai-upload-btn:focus{outline:3px solid #1a4d2e;outline-offset:3px;}'
+ '#invoice-ai-status{font-size:11px;color:#555;font-family:Segoe UI,sans-serif;flex:1;}'
+ '#invoice-ai-status.iai-uploading{color:#1a4d2e;font-weight:600;}'
+ '#invoice-ai-status.iai-success{color:#1a7f3e;font-weight:600;}'
+ '#invoice-ai-status.iai-error{color:#c0392b;font-weight:600;}'
+ '#invoice-ai-toast{position:fixed;top:20px;left:50%;transform:translateX(-50%);background:#1a4d2e;color:white;padding:12px 20px;border-radius:8px;font-size:13px;font-family:Segoe UI,sans-serif;box-shadow:0 4px 16px rgba(0,0,0,.2);z-index:2147483647;display:none;}'
+ '#invoice-ai-toast.iai-show{display:block;animation:iaiToastIn .3s ease;}'
+ '@keyframes iaiToastIn{from{opacity:0;transform:translateX(-50%) translateY(-10px);}to{opacity:1;transform:translateX(-50%) translateY(0);}}';
var style = document.createElement('style');
style.id = 'invoice-ai-styles';
style.textContent = css;
document.head.appendChild(style);
}
function buildWidgetShell() {
var tooltip = document.createElement('div');
tooltip.id = 'invoice-ai-tooltip';
tooltip.textContent = 'AI Powered Invoice Parser';
document.body.appendChild(tooltip);
var fab = document.createElement('button');
fab.id = 'invoice-ai-fab';
fab.title = 'AI Powered Invoice Parser';
fab.setAttribute('aria-label', 'Open AI Powered Invoice Parser');
fab.setAttribute('aria-expanded', 'false');
fab.innerHTML =
'<span id="invoice-ai-badge">AI</span>'
+ '<svg id="iai-ic-doc" width="26" height="26" viewBox="0 0 24 24" fill="none">'
+ '<path d="M7 3h7l5 5v13H7V3z" stroke="white" stroke-width="2" stroke-linejoin="round"/>'
+ '<path d="M14 3v5h5" stroke="white" stroke-width="2" stroke-linejoin="round"/>'
+ '<path d="M9 13h6M9 17h6" stroke="white" stroke-width="2" stroke-linecap="round"/>'
+ '</svg>'
+ '<svg id="iai-ic-close" width="26" height="26" viewBox="0 0 24 24" fill="none" style="display:none">'
+ '<path d="M18 6L6 18M6 6L18 18" stroke="white" stroke-width="2.5" stroke-linecap="round"/>'
+ '</svg>';
document.body.appendChild(fab);
var panel = document.createElement('div');
panel.id = 'invoice-ai-panel';
panel.setAttribute('role', 'dialog');
panel.setAttribute('aria-modal', 'true');
panel.innerHTML =
'<div id="invoice-ai-hdr">'
+ '<div style="width:34px;height:34px;background:rgba(255,255,255,.18);border-radius:8px;display:flex;align-items:center;justify-content:center;font-size:16px;flex-shrink:0" aria-hidden="true">📄</div>'
+ '<div><h3>AI Powered Invoice Parser</h3><p>Upload invoice PDF and populate voucher fields</p></div>'
+ '<div id="invoice-ai-hdr-status" aria-live="polite"><div id="invoice-ai-hdrdot" aria-hidden="true"></div><span>Demo</span></div>'
+ '<button id="invoice-ai-closebtn" aria-label="Close">✕</button>'
+ '</div>'
+ '<div id="invoice-ai-body">'
+ '<div id="invoice-ai-summary">'
+ '<h4>Upload invoice PDF</h4>'
+ '<p>This will extract invoice values and populate the Voucher page fields.</p>'
+ '<p><strong>Page check:</strong> <span id="invoice-ai-page-check">Checking...</span></p>'
+ '<p><strong>Expected page:</strong> Voucher Entry / <code>VCHR_EXPRESS</code></p>'
+ '<p style="color:#c0392b"><strong>Review all values before saving the voucher.</strong></p>'
+ '</div>'
+ '</div>'
+ '<div id="invoice-ai-action-bar">'
+ '<button id="invoice-ai-upload-btn" aria-label="Upload invoice PDF" title="Upload invoice PDF" disabled>'
+ '<svg width="17" height="17" viewBox="0 0 24 24" fill="none" aria-hidden="true">'
+ '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>'
+ '</svg></button>'
+ '<input type="file" id="invoice-ai-file-input" accept="application/pdf,.pdf" style="display:none">'
+ '<span id="invoice-ai-status" aria-live="polite">Open Voucher Entry, then upload invoice</span>'
+ '</div>';
document.body.appendChild(panel);
var toast = document.createElement('div');
toast.id = 'invoice-ai-toast';
toast.setAttribute('role', 'status');
document.body.appendChild(toast);
}
function wireWidgetEvents() {
var fab = document.getElementById('invoice-ai-fab');
var panel = document.getElementById('invoice-ai-panel');
var closeBtn = document.getElementById('invoice-ai-closebtn');
var uploadBtn = document.getElementById('invoice-ai-upload-btn');
var fileInput = document.getElementById('invoice-ai-file-input');
var isOpen = false;
function openPanel() {
isOpen = true;
panel.classList.add('iai-open');
fab.setAttribute('aria-expanded', 'true');
document.getElementById('iai-ic-doc').style.display = 'none';
document.getElementById('iai-ic-close').style.display = 'block';
var tooltip = document.getElementById('invoice-ai-tooltip');
if (tooltip) tooltip.classList.remove('iai-show');
var badge = document.getElementById('invoice-ai-badge');
if (badge) badge.remove();
refreshUploadButton();
}
function closePanel() {
isOpen = false;
panel.classList.remove('iai-open');
fab.setAttribute('aria-expanded', 'false');
document.getElementById('iai-ic-doc').style.display = 'block';
document.getElementById('iai-ic-close').style.display = 'none';
fab.focus();
}
closeBtn.addEventListener('click', function (e) {
e.stopPropagation();
closePanel();
});
fab.addEventListener('click', function (e) {
e.stopPropagation();
isOpen ? closePanel() : openPanel();
});
document.addEventListener('click', function (e) {
if (isOpen && !panel.contains(e.target) && e.target !== fab) {
closePanel();
}
});
document.addEventListener('keydown', function (e) {
if (isOpen && (e.key === 'Escape' || e.keyCode === 27)) {
closePanel();
}
});
uploadBtn.addEventListener('click', function () {
if (uploadBtn.disabled) return;
fileInput.value = '';
fileInput.click();
});
fileInput.addEventListener('change', function (e) {
var file = e.target.files && e.target.files[0];
if (file) {
handleInvoiceFile(file);
}
});
}
function refreshUploadButton() {
var btn = document.getElementById('invoice-ai-upload-btn');
var pageCheck = document.getElementById('invoice-ai-page-check');
if (!btn) return;
var pg = getPageContext();
var ok = isInvoiceUploadPage();
btn.disabled = !ok;
btn.title = ok ? 'Upload invoice PDF' : 'Open Voucher Entry to upload';
btn.setAttribute('aria-label', btn.title);
if (pageCheck) {
pageCheck.textContent = ok
? 'Ready on ' + (pg.component || 'Voucher page')
: 'Not on Voucher page. Current component: ' + (pg.component || 'unknown');
pageCheck.style.color = ok ? '#1a7f3e' : '#c0392b';
}
if (!ok) {
setStatus('Open Voucher Entry, then upload invoice');
}
}
function isInvoiceUploadPage() {
var pg = getPageContext();
for (var i = 0; i < INVOICE_UPLOAD_COMPONENTS.length; i++) {
if (pg.component === INVOICE_UPLOAD_COMPONENTS[i]) {
return true;
}
}
return false;
}
function handleInvoiceFile(file) {
if (file.type !== 'application/pdf' && !/\.pdf$/i.test(file.name)) {
setStatus('Only PDF files are supported', 'iai-error');
showToast('Only PDF files are supported');
return;
}
if (file.size > CONFIG.maxPdfBytes) {
setStatus('PDF too large', 'iai-error');
showToast('PDF too large. Max allowed: ' + Math.round(CONFIG.maxPdfBytes / 1024 / 1024) + 'MB');
return;
}
if (!CONFIG.docIntelKey || CONFIG.docIntelKey === 'PASTE_KEY_1_HERE') {
setStatus('Azure key missing in CONFIG', 'iai-error');
showToast('Add Azure key in CONFIG.docIntelKey');
return;
}
if (!isInvoiceUploadPage()) {
setStatus('Open Voucher Entry first', 'iai-error');
showToast('Not on Voucher page');
return;
}
setStatus('Reading PDF...', 'iai-uploading');
renderWorking('Reading PDF and preparing request...');
fileToBase64(file)
.then(function (base64Pdf) {
setStatus('Analyzing invoice...', 'iai-uploading');
renderWorking('Analyzing invoice using AI powered document extraction...');
return analyzeInvoiceWithAzure(base64Pdf);
})
.then(function (azureResult) {
setStatus('Normalizing extracted invoice data...', 'iai-uploading');
var normalized = normalizeAzureInvoiceResult(azureResult);
console.log('[InvoiceAI] Normalized invoice:', normalized);
console.log('[InvoiceAI] Raw extraction result:', azureResult);
renderSummary(normalized);
populateVoucherFields(normalized.voucherFields);
})
.catch(function (err) {
console.error('[InvoiceAI] Error:', err);
setStatus('Invoice parsing failed', 'iai-error');
renderError(err);
showToast(err.message);
});
}
function fileToBase64(file) {
return new Promise(function (resolve, reject) {
var reader = new FileReader();
reader.onload = function () {
var dataUrl = String(reader.result || '');
var parts = dataUrl.split(',');
if (parts.length < 2) {
reject(new Error('Could not read PDF as base64'));
return;
}
resolve(parts[1]);
};
reader.onerror = function () {
reject(new Error('Could not read file'));
};
reader.readAsDataURL(file);
});
}
function analyzeInvoiceWithAzure(base64Pdf) {
var endpoint = removeTrailingSlash(CONFIG.docIntelEndpoint);
var analyzeUrl =
endpoint
+ '/documentintelligence/documentModels/'
+ encodeURIComponent(CONFIG.modelId)
+ ':analyze?_overload=analyzeDocument&api-version='
+ encodeURIComponent(CONFIG.apiVersion);
return fetch(analyzeUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Ocp-Apim-Subscription-Key': CONFIG.docIntelKey
},
body: JSON.stringify({
base64Source: base64Pdf
})
})
.then(function (response) {
if (response.status !== 202) {
return response.text().then(function (text) {
throw new Error('Analyze failed. HTTP ' + response.status + ': ' + safeShortText(text));
});
}
var operationLocation =
response.headers.get('Operation-Location')
|| response.headers.get('operation-location');
if (!operationLocation) {
throw new Error('No Operation-Location header returned');
}
return pollAzureResult(operationLocation, 0);
});
}
function pollAzureResult(operationLocation, attempt) {
if (attempt >= CONFIG.maxPollAttempts) {
throw new Error('Analysis timed out. Try again with a smaller or clearer PDF.');
}
return delay(CONFIG.pollIntervalMs)
.then(function () {
setStatus('Waiting for result... attempt ' + (attempt + 1), 'iai-uploading');
return fetch(operationLocation, {
method: 'GET',
headers: {
'Ocp-Apim-Subscription-Key': CONFIG.docIntelKey
}
});
})
.then(function (response) {
if (!response.ok) {
return response.text().then(function (text) {
throw new Error('Result polling failed. HTTP ' + response.status + ': ' + safeShortText(text));
});
}
return response.json();
})
.then(function (result) {
var status = String(result.status || '').toLowerCase();
if (status === 'succeeded') {
return result;
}
if (status === 'failed') {
var msg = 'Invoice analysis failed';
if (result.error && result.error.message) {
msg += ': ' + result.error.message;
}
throw new Error(msg);
}
return pollAzureResult(operationLocation, attempt + 1);
});
}
function normalizeAzureInvoiceResult(result) {
var analyzeResult = result.analyzeResult || {};
var documents = analyzeResult.documents || [];
var doc = documents.length ? documents[0] : {};
var fields = doc.fields || {};
function getField(names) {
for (var i = 0; i < names.length; i++) {
if (fields[names[i]]) return fields[names[i]];
}
return null;
}
function fieldValue(names) {
var f = getField(names);
if (!f) return '';
if (f.valueString !== undefined) return f.valueString;
if (f.valueDate !== undefined) return f.valueDate;
if (f.valueTime !== undefined) return f.valueTime;
if (f.valuePhoneNumber !== undefined) return f.valuePhoneNumber;
if (f.valueNumber !== undefined) return String(f.valueNumber);
if (f.valueInteger !== undefined) return String(f.valueInteger);
if (f.valueCurrency !== undefined) {
if (f.valueCurrency.amount !== undefined) return String(f.valueCurrency.amount);
if (f.content !== undefined) return cleanAmount(f.content);
}
if (f.content !== undefined) return f.content;
return '';
}
function fieldConfidence(names) {
var f = getField(names);
if (!f || f.confidence === undefined || f.confidence === null) {
return null;
}
return f.confidence;
}
var vendorName = fieldValue(['VendorName', 'MerchantName', 'SupplierName']);
var vendorAddress = fieldValue(['VendorAddress']);
var invoiceNumber = fieldValue(['InvoiceId', 'InvoiceNumber']);
var invoiceDate = fieldValue(['InvoiceDate']);
var dueDate = fieldValue(['DueDate']);
var purchaseOrder = fieldValue(['PurchaseOrder', 'PurchaseOrderNumber']);
var subTotal = fieldValue(['SubTotal']);
var totalTax = fieldValue(['TotalTax', 'Tax']);
var freight = fieldValue(['Freight', 'Shipping', 'ShippingAmount']);
var invoiceTotal = fieldValue(['InvoiceTotal', 'AmountDue', 'Total']);
var amountDue = fieldValue(['AmountDue']);
var currency = inferCurrency(fields, invoiceTotal);
var voucherFields = {
vendor_name: vendorName,
invoice_number: invoiceNumber,
invoice_date: invoiceDate,
gross_amount: cleanAmount(invoiceTotal || amountDue || subTotal),
sales_tax: cleanAmount(totalTax),
freight: cleanAmount(freight),
po_number: purchaseOrder
};
var lineItems = extractLineItems(fields);
var confidenceValues = [
fieldConfidence(['VendorName', 'MerchantName', 'SupplierName']),
fieldConfidence(['InvoiceId', 'InvoiceNumber']),
fieldConfidence(['InvoiceDate']),
fieldConfidence(['InvoiceTotal', 'AmountDue', 'Total']),
fieldConfidence(['TotalTax', 'Tax'])
].filter(function (v) {
return typeof v === 'number';
});
var avgConfidence = null;
if (confidenceValues.length) {
avgConfidence = confidenceValues.reduce(function (a, b) {
return a + b;
}, 0) / confidenceValues.length;
}
return {
status: 'success',
confidence: avgConfidence,
voucherFields: voucherFields,
rawExtracted: {
vendorName: vendorName,
vendorAddress: vendorAddress,
invoiceNumber: invoiceNumber,
invoiceDate: invoiceDate,
dueDate: dueDate,
purchaseOrder: purchaseOrder,
subTotal: cleanAmount(subTotal),
totalTax: cleanAmount(totalTax),
freight: cleanAmount(freight),
invoiceTotal: cleanAmount(invoiceTotal),
amountDue: cleanAmount(amountDue),
currency: currency
},
lineItems: lineItems
};
}
function extractLineItems(fields) {
var itemsField = fields.Items || fields.LineItems;
var items = [];
if (!itemsField || !itemsField.valueArray) {
return items;
}
itemsField.valueArray.forEach(function (item, idx) {
var obj = item.valueObject || {};
function itemVal(possibleNames) {
for (var i = 0; i < possibleNames.length; i++) {
var f = obj[possibleNames[i]];
if (!f) continue;
if (f.valueString !== undefined) return f.valueString;
if (f.valueNumber !== undefined) return String(f.valueNumber);
if (f.valueInteger !== undefined) return String(f.valueInteger);
if (f.valueCurrency && f.valueCurrency.amount !== undefined) return String(f.valueCurrency.amount);
if (f.content !== undefined) return f.content;
}
return '';
}
items.push({
line: idx + 1,
description: itemVal(['Description', 'ProductCode', 'Item']),
quantity: itemVal(['Quantity']),
unitPrice: cleanAmount(itemVal(['UnitPrice', 'Price'])),
amount: cleanAmount(itemVal(['Amount', 'TotalPrice']))
});
});
return items;
}
/*
Core field population logic.
*/
function populateVoucherFields(fields) {
var cf = getContentFrame();
if (!cf || !cf.document) {
setStatus('Could not access voucher page', 'iai-error');
showToast('Voucher page not loaded in iframe');
return;
}
var doc = cf.document;
var supplierName = fields.vendor_name || '';
var supplierEl = doc.getElementById(VOUCHER_FIELD_MAP.vendor_name);
if (!supplierName || !supplierEl) {
setStatus('Supplier not detected. Populating invoice fields only.', 'iai-uploading');
setInvoiceFieldsDomOnly(fields);
scheduleInvoiceFieldReapply(fields);
setStatus('Invoice fields populated. Review before saving.', 'iai-success');
showToast('Invoice fields populated. Review before saving.');
return;
}
var existingSupplierName = getElementValue(supplierEl);
var alreadySameSupplier = normalizeText(existingSupplierName) === normalizeText(supplierName);
var alreadyResolved = isSupplierResolved();
if (alreadySameSupplier && alreadyResolved) {
console.log('[InvoiceAI] Supplier already resolved. Skipping supplier submitAction.');
setStatus('Supplier already resolved. Populating invoice fields...', 'iai-uploading');
setInvoiceFieldsDomOnly(fields);
scheduleInvoiceFieldReapply(fields);
setStatus('Invoice fields populated. Review before saving.', 'iai-success');
showToast('Invoice fields populated. Review before saving.');
return;
}
setStatus('Populating supplier and waiting for PeopleSoft validation...', 'iai-uploading');
setDomValueOnly(cf, supplierEl, supplierName);
triggerSupplierSubmitActionOnly(cf, supplierEl);
waitForSupplierResolution()
.then(function () {
setStatus('Supplier resolved. Populating invoice fields...', 'iai-uploading');
setInvoiceFieldsDomOnly(fields);
scheduleInvoiceFieldReapply(fields);
setStatus('Invoice fields populated. Review before saving.', 'iai-success');
showToast('Invoice fields populated. Review before saving.');
})
.catch(function (err) {
console.warn('[InvoiceAI] Supplier wait ended:', err.message);
/*
Still populate invoice fields for demo.
Supplier may have resolved visually even if polling missed it.
*/
setInvoiceFieldsDomOnly(fields);
scheduleInvoiceFieldReapply(fields);
setStatus('Invoice fields populated. Review supplier and amounts.', 'iai-success');
showToast('Invoice fields populated. Review supplier and amounts.');
});
}
/*
Supplier Name:
One PeopleSoft server action only.
No duplicate onchange/onblur calls.
*/
function triggerSupplierSubmitActionOnly(frameWindow, element) {
try {
if (typeof frameWindow.submitAction_win0 === 'function' && element.form && element.id) {
frameWindow.submitAction_win0(element.form, element.id);
console.log('[InvoiceAI] submitAction_win0 fired for supplier:', element.id);
return;
}
/*
Fallback only if submitAction_win0 does not exist.
Avoid this path if possible.
*/
element.focus();
element.dispatchEvent(new frameWindow.Event('change', {
bubbles: true,
cancelable: true
}));
element.dispatchEvent(new frameWindow.Event('blur', {
bubbles: true,
cancelable: true
}));
console.log('[InvoiceAI] Fallback change/blur fired for supplier:', element.id);
} catch (e) {
console.warn('[InvoiceAI] Supplier submit action failed:', e);
}
}
/*
Invoice fields:
DOM value only.
No input/change/blur/onchange/onblur/submitAction.
*/
function setInvoiceFieldsDomOnly(fields) {
var cf = getContentFrame();
if (!cf || !cf.document) {
setStatus('Could not access voucher page', 'iai-error');
showToast('Voucher page not loaded in iframe');
return;
}
var doc = cf.document;
var populated = 0;
var skipped = [];
Object.keys(fields).forEach(function (key) {
if (key === 'vendor_name') return;
var psFieldId = VOUCHER_FIELD_MAP[key];
if (!psFieldId) {
skipped.push(key + ' no mapping');
return;
}
var el = doc.getElementById(psFieldId);
if (!el) {
skipped.push(key + ' field not found: ' + psFieldId);
return;
}
var value = normalizeVoucherFieldValue(key, fields[key]);
if (value === null || value === undefined || value === '') {
return;
}
setDomValueOnly(cf, el, value);
populated++;
console.log('[InvoiceAI] DOM-only populated', psFieldId, '=', value);
});
if (skipped.length) {
console.warn('[InvoiceAI] Skipped fields:', skipped);
}
console.log('[InvoiceAI] DOM-only field count:', populated);
}
/*
Reapply values after PeopleSoft delayed refresh/recalculation.
This is the key fix for amounts resetting to 0.00.
*/
function scheduleInvoiceFieldReapply(fields) {
CONFIG.reapplyDelaysMs.forEach(function (ms) {
setTimeout(function () {
console.log('[InvoiceAI] Reapplying invoice fields after', ms, 'ms');
setInvoiceFieldsDomOnly(fields);
}, ms);
});
}
function waitForSupplierResolution() {
var startTime = Date.now();
return new Promise(function (resolve, reject) {
function check() {
if (isSupplierResolved()) {
resolve(true);
return;
}
if (Date.now() - startTime >= CONFIG.supplierResolutionTimeoutMs) {
reject(new Error('Supplier resolution timeout'));
return;
}
setTimeout(check, CONFIG.supplierResolutionPollMs);
}
check();
});
}
function isSupplierResolved() {
var cf = getContentFrame();
if (!cf || !cf.document) {
return false;
}
var doc = cf.document;
var supplierIdEl = doc.getElementById(PS_DERIVED_SUPPLIER_FIELDS.supplier_id);
var shortNameEl = doc.getElementById(PS_DERIVED_SUPPLIER_FIELDS.vendor_short);
var locEl = doc.getElementById(PS_DERIVED_SUPPLIER_FIELDS.vendor_location);
var supplierId = supplierIdEl ? getElementValue(supplierIdEl) : '';
var shortName = shortNameEl ? getElementValue(shortNameEl) : '';
var loc = locEl ? getElementValue(locEl) : '';
return !!(supplierId || shortName || loc);
}
function setDomValueOnly(frameWindow, element, value) {
var tag = String(element.tagName || '').toUpperCase();
var proto;
if (tag === 'TEXTAREA') {
proto = frameWindow.HTMLTextAreaElement.prototype;
} else if (tag === 'SELECT') {
proto = frameWindow.HTMLSelectElement.prototype;
} else {
proto = frameWindow.HTMLInputElement.prototype;
}
var descriptor = Object.getOwnPropertyDescriptor(proto, 'value');
if (descriptor && descriptor.set) {
descriptor.set.call(element, value);
} else {
element.value = value;
}
}
function normalizeVoucherFieldValue(key, value) {
if (value === null || value === undefined) return '';
var s = String(value).trim();
if (!s) return '';
if (key === 'invoice_date' && /^\d{4}-\d{2}-\d{2}/.test(s)) {
var parts = s.substring(0, 10).split('-');
return parts[1] + '/' + parts[2] + '/' + parts[0];
}
if (key === 'gross_amount' || key === 'sales_tax' || key === 'freight') {
return cleanAmount(s);
}
return s;
}
function getElementValue(el) {
if (!el) return '';
return String(el.value || el.getAttribute('value') || '').trim();
}
function normalizeText(value) {
return String(value || '')
.trim()
.replace(/\s+/g, ' ')
.toUpperCase();
}
function renderWorking(message) {
var body = document.getElementById('invoice-ai-summary');
if (!body) return;
body.innerHTML =
'<h4>Processing invoice...</h4>'
+ '<p>' + escapeHtml(message) + '</p>'
+ '<p>Please wait. The invoice parser may take a few seconds.</p>';
}
function renderError(err) {
var body = document.getElementById('invoice-ai-summary');
if (!body) return;
body.innerHTML =
'<h4 style="color:#c0392b">Invoice parsing failed</h4>'
+ '<p>' + escapeHtml(err.message || String(err)) + '</p>'
+ '<p>Check the key, endpoint, CORS settings, PDF format, and browser console.</p>';
}
function renderSummary(normalized) {
var body = document.getElementById('invoice-ai-summary');
if (!body) return;
var v = normalized.voucherFields || {};
var raw = normalized.rawExtracted || {};
var confidenceText = normalized.confidence === null || normalized.confidence === undefined
? 'Not available'
: Math.round(normalized.confidence * 100) + '%';
var lineItemsHtml = '';
if (normalized.lineItems && normalized.lineItems.length) {
lineItemsHtml =
'<h4 style="margin-top:14px">Line Items</h4>'
+ '<table>'
+ '<thead><tr><th>#</th><th>Description</th><th>Qty</th><th>Amount</th></tr></thead>'
+ '<tbody>'
+ normalized.lineItems.slice(0, 5).map(function (item) {
return '<tr>'
+ '<td>' + escapeHtml(item.line) + '</td>'
+ '<td>' + escapeHtml(item.description || '') + '</td>'
+ '<td>' + escapeHtml(item.quantity || '') + '</td>'
+ '<td>' + escapeHtml(item.amount || '') + '</td>'
+ '</tr>';
}).join('')
+ '</tbody></table>';
}
body.innerHTML =
'<h4>Invoice processed</h4>'
+ '<p><strong>Supplier Name:</strong> ' + escapeHtml(v.vendor_name || 'Not detected') + '</p>'
+ '<p><strong>Invoice #:</strong> ' + escapeHtml(v.invoice_number || 'Not detected') + '</p>'
+ '<p><strong>Invoice Date:</strong> ' + escapeHtml(v.invoice_date || 'Not detected') + '</p>'
+ '<p><strong>Due Date:</strong> ' + escapeHtml(raw.dueDate || 'Not detected') + '</p>'
+ '<p><strong>Total:</strong> ' + escapeHtml(raw.currency || '') + ' ' + escapeHtml(v.gross_amount || 'Not detected') + '</p>'
+ '<p><strong>Tax:</strong> ' + escapeHtml(v.sales_tax || 'Not detected') + '</p>'
+ '<p><strong>Freight:</strong> ' + escapeHtml(v.freight || 'Not detected') + '</p>'
+ '<p><strong>PO:</strong> ' + escapeHtml(v.po_number || 'Not detected') + '</p>'
+ '<p><strong>Avg Confidence:</strong> ' + escapeHtml(confidenceText) + '</p>'
+ '<p style="color:#c0392b"><strong>Review before saving voucher.</strong></p>'
+ lineItemsHtml;
}
function inferCurrency(fields, totalText) {
var currencyFields = ['InvoiceTotal', 'AmountDue', 'SubTotal', 'TotalTax'];
for (var i = 0; i < currencyFields.length; i++) {
var f = fields[currencyFields[i]];
if (f && f.valueCurrency && f.valueCurrency.currencyCode) {
return f.valueCurrency.currencyCode;
}
}
var text = String(totalText || '');
if (text.indexOf('$') >= 0) return 'USD';
if (text.indexOf('₹') >= 0) return 'INR';
if (text.indexOf('€') >= 0) return 'EUR';
if (text.indexOf('£') >= 0) return 'GBP';
return '';
}
function getPageContext() {
var ctx = {
component: '',
menu: '',
portal: '',
node: '',
pageTitle: '',
href: ''
};
var cf = getContentFrame();
try {
ctx.href = cf ? cf.location.href : window.location.href;
var m = ctx.href.match(/\/ps[cp][^/]*\/[^/]+\/([^/]+)\/([^/]+)\/[ch]\/([^?#/]+)/i);
if (m) {
ctx.portal = m[1];
ctx.node = m[2];
var parts = m[3].split('.');
ctx.menu = parts[0] || '';
ctx.component = parts[1] || '';
}
} catch (e) {}
try {
if (cf) {
ctx.pageTitle = (cf.szCrefLabel || cf.szPinCrefLabel || '').trim();
if (!ctx.pageTitle && cf.document) {
ctx.pageTitle = cf.document.title || '';
}
}
} catch (e2) {}
return ctx;
}
function getContentFrame() {
var iframeEl =
document.getElementById('ptifrmtgtframe')
|| document.getElementById('ptifrmcontent')
|| document.querySelector('iframe[name="TargetContent"]')
|| document.querySelector('iframe[id*="tgtframe"]')
|| document.querySelector('iframe[id*="content"]');
if (iframeEl) {
try {
return iframeEl.contentWindow;
} catch (e) {}
}
return null;
}
function setStatus(msg, cls) {
var el = document.getElementById('invoice-ai-status');
if (!el) return;
el.textContent = msg;
el.className = cls || '';
}
function showToast(msg) {
var toast = document.getElementById('invoice-ai-toast');
if (!toast) return;
toast.textContent = msg;
toast.classList.add('iai-show');
setTimeout(function () {
toast.classList.remove('iai-show');
}, 3500);
}
function cleanAmount(value) {
if (value === null || value === undefined) return '';
var s = String(value).trim();
if (!s) return '';
s = s.replace(/[,$₹€£\s]/g, '');
var negative = false;
if (/^\(.*\)$/.test(s)) {
negative = true;
s = s.replace(/[()]/g, '');
}
s = s.replace(/[^0-9.\-]/g, '');
if (!s) return '';
return negative ? '-' + s : s;
}
function removeTrailingSlash(s) {
return String(s || '').replace(/\/+$/, '');
}
function delay(ms) {
return new Promise(function (resolve) {
setTimeout(resolve, ms);
});
}
function safeShortText(text) {
text = String(text || '');
return text.length > 500 ? text.substring(0, 500) + '...' : text;
}
function escapeHtml(value) {
return String(value === null || value === undefined ? '' : value)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
domReady(function () {
initInvoiceParserWidget();
});
})();
No comments:
Post a Comment