Initial commit
This commit is contained in:
@@ -0,0 +1,28 @@
|
|||||||
|
# Unit4 ERP (UBW) requisition filler
|
||||||
|
|
||||||
|
A Tampermonkey userscript that fills the line items of a UBW purchase requisition from a supplier cart CSV. It fills and stops; you review, then Save/Send yourself. `ubw-requisition-SOP.md` covers the manual procedure and gotchas.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
1. Install [Tampermonkey](https://addons.mozilla.org/en-US/firefox/addon/tampermonkey/).
|
||||||
|
2. Create a new script, paste in `ubw-requisition-filler.user.js`, and Save.
|
||||||
|
|
||||||
|
## Use
|
||||||
|
|
||||||
|
1. Go to https://ubw.unit4cloud.com/uk_swa_prod_web/ and start a new requisition. Open the Header, set the Supplier and Sub Project (budget), and Close it. Do this first: each line inherits the supplier from the committed header.
|
||||||
|
2. Click the Tampermonkey icon and choose "Show UBW Requisition Filler".
|
||||||
|
3. Set a Default product code (e.g. "LK1") for lines whose CSV has no Product column.
|
||||||
|
4. Browse to the cart CSV and click Fill requisition.
|
||||||
|
5. Review every line and the total, then Save as draft or Send for approval. Attach the source CSV and any supporting documentation.
|
||||||
|
|
||||||
|
## CSV format
|
||||||
|
|
||||||
|
A header row with these columns (case-insensitive):
|
||||||
|
|
||||||
|
- Item Number
|
||||||
|
- Quantity
|
||||||
|
- Description
|
||||||
|
- Unit Price
|
||||||
|
- Product (optional; per-line product code, otherwise the Default is used)
|
||||||
|
|
||||||
|
A Thorlabs cart export works directly. `example-cart.csv` is a small sample.
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
Item Number,Quantity,Description,Unit Price
|
||||||
|
LMR1,5,"Lens Mount for Ø1in Optics",£12.50
|
||||||
|
PH2,10,"Ø1/2in Post Holder, Spring-Loaded Hex-Locking Thumbscrew, L = 2in",£4.80
|
||||||
|
TR3,10,"Ø1/2in Optical Post, SS, 8-32 Setscrew, 1/4in-20 Tap, L = 3in",£3.90
|
||||||
|
KM100,4,"Kinematic Mirror Mount for Ø1in Optics",£42.00
|
||||||
|
BB1-E02,2,"Ø1in Broadband Dielectric Mirror, 400 - 750 nm",£68.30
|
||||||
|
@@ -0,0 +1,74 @@
|
|||||||
|
# Raising a purchase requisition in Unit4 ERP (UBW)
|
||||||
|
|
||||||
|
How to turn a supplier cart or quote into a purchase requisition (which becomes a purchase order once the workflow approves it) in UBW at `https://ubw.unit4cloud.com/`. The supplier, budget and product codes below are placeholders; replace them with your own. A small `example-cart.csv` is included to try the script.
|
||||||
|
|
||||||
|
You can do this by hand (the procedure below) or with the helper script `ubw-requisition-filler.user.js`, which fills the whole requisition from a cart CSV. Either way you review and click Save/Send yourself; nothing is submitted automatically. The manual procedure stays the reference for what the script does and for one-off edits.
|
||||||
|
|
||||||
|
## Script: ubw-requisition-filler.user.js
|
||||||
|
|
||||||
|
`ubw-requisition-filler.user.js` does steps 3 and 6 below for you (names the request and adds every line) from a cart CSV, logging progress and a computed total. Install, usage and CSV format are in `README.md`. You still set the Header by hand first (step 4) and review before Save/Send; check the script's per-line total against your quote.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- You are logged in to UBW (the ERP treats you as the requisitioner).
|
||||||
|
- You have the **supplier account code**, the **budget (Sub Project) code**, and the line items (SKU, description, quantity, unit price ex-VAT).
|
||||||
|
- Keep the lookup tables at the bottom of this file current rather than searching the register each time.
|
||||||
|
|
||||||
|
## Procedure
|
||||||
|
|
||||||
|
1. **Homescreen → Purchasing.** Under *Start pages*, click **Purchasing**.
|
||||||
|
2. **New request.** Under *New request*, click the **Purchase request** tile. This opens a blank request with you as requisitioner.
|
||||||
|
3. **Name the request**, e.g. `<Supplier> order <YYYY-MM-DD>`.
|
||||||
|
4. **Fill the Header first, before adding any lines.** Click **Header** (top right) and set:
|
||||||
|
- **Supplier\*.** Type the supplier account code and Tab; it validates and shows the supplier name. Do this before adding lines so each line inherits the supplier.
|
||||||
|
- **Sub Project code\*.** Type the budget code and Tab; it resolves to the project name.
|
||||||
|
- *External reference* and *Source To Contract ID* are optional.
|
||||||
|
- Click **Close**.
|
||||||
|
5. **Check delivery.** Confirm the *Delivery contact* and *Delivery address*.
|
||||||
|
6. **Add each line item.** For every line:
|
||||||
|
1. Click **Add product**. A new editable row appears; its Supplier cell inherits the header supplier.
|
||||||
|
2. In **Product**, type the generic free-text product code and press **Tab**.
|
||||||
|
3. **Wait for the row to reload.** Tab fires a server round-trip (a spinner shows) that re-renders the row with a default description, Account, Sub Project and Tax code. Do not type until it finishes (see gotchas).
|
||||||
|
4. Set **Product description** to `<SKU> - <full item description>`.
|
||||||
|
5. Set **Quantity**.
|
||||||
|
6. Set **Price** (unit, ex-VAT) and press **Tab**. The line total recalculates.
|
||||||
|
7. **Check the running total.** *Total amount* (bottom right) should match the sum of your line totals ex-VAT.
|
||||||
|
8. **Finish.** Review every line, then **Save as draft** or **Send for approval**. Do not submit until the whole order is checked.
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- **The Product field will not accept a supplier SKU.** It is a validated lookup against your internal product register, so a raw part number is rejected (the field goes red, "Errors: 1"). Use the generic free-text code and put the SKU in the description.
|
||||||
|
- **Timing after the Product code is the one that bites.** After you Tab out of the product code, the row reloads from the server. Edits made before the reload finishes land on the stale row and are silently wiped (description reverts, qty 1, price 0). Wait for the spinner to clear, then fill the rest of the line.
|
||||||
|
- **Set the Header supplier before adding lines**, or the line keeps a blank Supplier cell.
|
||||||
|
- **Prices are unit, ex-VAT.** A default tax code and account are applied to the generic line; change them only if the coding for that spend differs.
|
||||||
|
|
||||||
|
## Mapping a supplier CSV/quote to lines by hand
|
||||||
|
|
||||||
|
The CSV columns are defined in `README.md`. To transcribe a cart into lines:
|
||||||
|
|
||||||
|
- **Product** = your generic free-text code (see the tables below), or the row's own code where it differs
|
||||||
|
- **Product description** = `{Item Number} - {Description}`
|
||||||
|
- **Quantity** = `Quantity`
|
||||||
|
- **Price** = `Unit Price`, ex-VAT
|
||||||
|
|
||||||
|
## Local lookup tables (maintain these)
|
||||||
|
|
||||||
|
Replace the placeholder rows with the codes your ERP uses.
|
||||||
|
|
||||||
|
### Supplier account codes
|
||||||
|
|
||||||
|
| Supplier | Code |
|
||||||
|
|----------|------|
|
||||||
|
| Acme Optics Ltd | AC100200 |
|
||||||
|
|
||||||
|
### Budget / Sub Project codes
|
||||||
|
|
||||||
|
| Code | Project |
|
||||||
|
|------|---------|
|
||||||
|
| ABC1234-100 | My Project 2026 |
|
||||||
|
|
||||||
|
### Generic (free-text) product codes
|
||||||
|
|
||||||
|
| Code | Meaning | Default account | Default tax |
|
||||||
|
|------|---------|-----------------|-------------|
|
||||||
|
| GEN1 | Laboratory consumables (free-text line) | (your account) | (your tax code) |
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
// ==UserScript==
|
||||||
|
// @name UBW Requisition Filler
|
||||||
|
// @namespace unit4.ubw.requisitions
|
||||||
|
// @version 1.0
|
||||||
|
// @description Fill a UBW purchase requisition's line items from a supplier cart CSV. Fills only; never saves or submits.
|
||||||
|
// @match https://ubw.unit4cloud.com/*
|
||||||
|
// @run-at document-idle
|
||||||
|
// @grant GM_registerMenuCommand
|
||||||
|
// @grant unsafeWindow
|
||||||
|
// ==/UserScript==
|
||||||
|
//
|
||||||
|
// Install, usage and CSV format: see README.md. The comments below are the design notes: why the
|
||||||
|
// script does things the way it does, for anyone maintaining it.
|
||||||
|
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// Any @grant puts us in Tampermonkey's sandbox, whose window lacks the page's Ext; reach the real
|
||||||
|
// page window (and its event constructors) via unsafeWindow. Falls back to window in the console.
|
||||||
|
const win = (typeof unsafeWindow !== 'undefined') ? unsafeWindow : window;
|
||||||
|
|
||||||
|
const DEFAULTS = {
|
||||||
|
productCode: '',
|
||||||
|
requestName: 'Order ' + new Date().toISOString().slice(0, 10),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---- Small async helpers ------------------------------------------------------------------
|
||||||
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||||
|
|
||||||
|
// "Server is done" = no outstanding Ext Ajax. Don't watch for .x-mask nodes; UBW keeps permanent ones.
|
||||||
|
async function waitIdle(timeout = 20000) {
|
||||||
|
const t0 = Date.now();
|
||||||
|
await sleep(120);
|
||||||
|
while (Date.now() - t0 < timeout) {
|
||||||
|
if (!win.Ext.Ajax.isLoading()) {
|
||||||
|
await sleep(120);
|
||||||
|
if (!win.Ext.Ajax.isLoading()) return true;
|
||||||
|
}
|
||||||
|
await sleep(100);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const visByName = (name, label) =>
|
||||||
|
win.Ext.ComponentQuery.query('[name="' + name + '"]').filter(
|
||||||
|
(c) => c.isVisible && c.isVisible() && (label === undefined || c.fieldLabel === label)
|
||||||
|
)[0];
|
||||||
|
|
||||||
|
const detailsGrid = () => win.Ext.ComponentQuery.query('abw_requisitions_detailsgridview')[0];
|
||||||
|
|
||||||
|
// Fire the full press/release sequence, not a bare .click(): ExtJS copies the header supplier onto
|
||||||
|
// a new "Add product" row on mousedown, so a lone click adds the row with Supplier left blank.
|
||||||
|
const clickButton = (btn) => {
|
||||||
|
if (!btn) return false;
|
||||||
|
const el = btn.el.dom.querySelector('a,span') || btn.el.dom;
|
||||||
|
// Page-world event constructors, to match the DOM they are dispatched onto.
|
||||||
|
const opts = { bubbles: true, cancelable: true, view: win, button: 0 };
|
||||||
|
const fire = (Ctor, type) => { try { el.dispatchEvent(new win[Ctor](type, opts)); } catch (e) {} };
|
||||||
|
fire('PointerEvent', 'pointerover');
|
||||||
|
fire('MouseEvent', 'mouseover');
|
||||||
|
fire('PointerEvent', 'pointerdown');
|
||||||
|
fire('MouseEvent', 'mousedown');
|
||||||
|
fire('PointerEvent', 'pointerup');
|
||||||
|
fire('MouseEvent', 'mouseup');
|
||||||
|
el.click();
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const actionButton = (text) =>
|
||||||
|
win.Ext.ComponentQuery.query('u4_actionbutton').find((b) => b.text === text);
|
||||||
|
|
||||||
|
// Resolve a remote-lookup field to an exact code. setValue sets only the display, not the record,
|
||||||
|
// so run the field's own query and select the exact match, firing the 'select' that commits it.
|
||||||
|
async function resolveCombo(field, code) {
|
||||||
|
if (!field) return { ok: false, err: 'field not found' };
|
||||||
|
field.focus();
|
||||||
|
if (typeof field.doQuery === 'function') {
|
||||||
|
field.doQuery(String(code), true, true);
|
||||||
|
await waitIdle();
|
||||||
|
await sleep(250);
|
||||||
|
let match = null;
|
||||||
|
field.getStore().each((r) => {
|
||||||
|
if (String(r.get(field.valueField)) === String(code)) {
|
||||||
|
match = r;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!match) return { ok: false, err: 'no exact match for ' + code, count: field.getStore().getCount() };
|
||||||
|
field.select(match);
|
||||||
|
try { field.fireEvent('select', field, [match]); } catch (e) {}
|
||||||
|
await waitIdle();
|
||||||
|
await sleep(200);
|
||||||
|
if (field.isExpanded) field.collapse();
|
||||||
|
} else {
|
||||||
|
field.setValue(String(code));
|
||||||
|
await waitIdle();
|
||||||
|
}
|
||||||
|
return { ok: field.isValid ? field.isValid() : true, value: field.getValue && field.getValue() };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Form operations ----------------------------------------------------------------------
|
||||||
|
// Supplier and budget live in the Header (set by hand); each line inherits the supplier from it.
|
||||||
|
// The script fills only the request name and the line grid.
|
||||||
|
function setRequestName(cfg) {
|
||||||
|
const nameF = win.Ext.ComponentQuery.query('[name="requisitionName"]')[0];
|
||||||
|
if (nameF && cfg.requestName) nameF.setValue(cfg.requestName);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addLine(cfg, line, log) {
|
||||||
|
const grid = detailsGrid();
|
||||||
|
const store = grid.getStore();
|
||||||
|
const ce = grid.editingPlugin;
|
||||||
|
const before = store.getCount();
|
||||||
|
|
||||||
|
if (!clickButton(actionButton('Add product'))) throw new Error('Add product button not found');
|
||||||
|
await waitIdle();
|
||||||
|
await sleep(400);
|
||||||
|
const rec = store.getAt(store.getCount() - 1);
|
||||||
|
if (store.getCount() !== before + 1) return { ok: false, err: 'row not added' };
|
||||||
|
|
||||||
|
if (!ce.editing) {
|
||||||
|
const col = grid.getColumnManager().getColumns().filter((c) => c.dataIndex === 'article')[0];
|
||||||
|
ce.startEdit(rec, col);
|
||||||
|
await sleep(300);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The product code populates the default account/tax/sub-project. Use this line's own code if
|
||||||
|
// the CSV gave one, otherwise the panel default.
|
||||||
|
const productCode = line.productCode || cfg.productCode;
|
||||||
|
const artR = await resolveCombo(visByName('article'), productCode);
|
||||||
|
if (!artR.ok) return { ok: false, err: 'product code ' + productCode + ': ' + artR.err };
|
||||||
|
await sleep(200);
|
||||||
|
|
||||||
|
visByName('articleDescription').setValue(line.description);
|
||||||
|
visByName('quantity').setValue(line.qty);
|
||||||
|
visByName('unitPrice').setValue(line.price);
|
||||||
|
await sleep(150);
|
||||||
|
ce.completeEdit();
|
||||||
|
await waitIdle();
|
||||||
|
await sleep(400);
|
||||||
|
|
||||||
|
if (!rec.get('supplierId')) log(' WARNING: supplier blank on this line');
|
||||||
|
|
||||||
|
// Set the line amount so the on-screen total is right for review; the server recomputes
|
||||||
|
// amounts, tax and the header total authoritatively on Save/Send.
|
||||||
|
rec.beginEdit();
|
||||||
|
rec.set('amount', line.qty * line.price);
|
||||||
|
rec.set('currencyAmount', line.qty * line.price);
|
||||||
|
rec.set('amountSet', true);
|
||||||
|
rec.endEdit();
|
||||||
|
try { grid.setTotalAmount(); } catch (e) {}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
valid: rec.isValid && rec.isValid(),
|
||||||
|
supplier: rec.get('supplierId'),
|
||||||
|
account: rec.get('account'),
|
||||||
|
amount: rec.get('amount'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fillOrder(cfg, lines, log) {
|
||||||
|
if (!detailsGrid()) {
|
||||||
|
log('ERROR: no requisition form on screen. Open Purchasing > Purchase request first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setRequestName(cfg);
|
||||||
|
log('adding ' + lines.length + ' lines... (Header supplier/budget must already be set)');
|
||||||
|
let total = 0;
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const r = await addLine(cfg, lines[i], log);
|
||||||
|
if (!r.ok) {
|
||||||
|
log('ABORT at line ' + (i + 1) + ': ' + r.err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
total += lines[i].qty * lines[i].price;
|
||||||
|
log('line ' + (i + 1) + '/' + lines.length + ' ' + lines[i].description.slice(0, 40) +
|
||||||
|
' -> ' + (r.valid ? 'valid' : 'INVALID'));
|
||||||
|
}
|
||||||
|
log('DONE. ' + lines.length + ' lines, computed total GBP ' + total.toFixed(2) + '.');
|
||||||
|
log('Review every line, then Save as draft / Send for approval yourself.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- CSV parsing --------------------------------------------------------------------------
|
||||||
|
// Minimal RFC-4180-ish parser: handles quoted fields, commas inside quotes and "" escapes.
|
||||||
|
function parseCSV(text) {
|
||||||
|
const rows = [];
|
||||||
|
let row = [], field = '', inQuotes = false;
|
||||||
|
for (let i = 0; i < text.length; i++) {
|
||||||
|
const c = text[i];
|
||||||
|
if (inQuotes) {
|
||||||
|
if (c === '"') {
|
||||||
|
if (text[i + 1] === '"') { field += '"'; i++; }
|
||||||
|
else inQuotes = false;
|
||||||
|
} else field += c;
|
||||||
|
} else if (c === '"') inQuotes = true;
|
||||||
|
else if (c === ',') { row.push(field); field = ''; }
|
||||||
|
else if (c === '\r') { /* skip */ }
|
||||||
|
else if (c === '\n') { row.push(field); rows.push(row); row = []; field = ''; }
|
||||||
|
else field += c;
|
||||||
|
}
|
||||||
|
if (field.length || row.length) { row.push(field); rows.push(row); }
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function linesFromCSV(text) {
|
||||||
|
const rows = parseCSV(text).filter((r) => r.some((c) => c && c.trim() !== ''));
|
||||||
|
if (!rows.length) throw new Error('empty CSV');
|
||||||
|
const header = rows[0].map((h) => h.trim().toLowerCase());
|
||||||
|
const col = (name) => header.indexOf(name);
|
||||||
|
const iItem = col('item number'), iQty = col('quantity'),
|
||||||
|
iDesc = col('description'), iPrice = col('unit price'), iProd = col('product');
|
||||||
|
if (iItem < 0 || iQty < 0 || iDesc < 0 || iPrice < 0) {
|
||||||
|
throw new Error('CSV must have columns: Item Number, Quantity, Description, Unit Price');
|
||||||
|
}
|
||||||
|
const money = (s) => parseFloat(String(s).replace(/[^0-9.\-]/g, ''));
|
||||||
|
const lines = [];
|
||||||
|
for (let r = 1; r < rows.length; r++) {
|
||||||
|
const item = (rows[r][iItem] || '').trim();
|
||||||
|
const qty = parseFloat(rows[r][iQty]);
|
||||||
|
const price = money(rows[r][iPrice]);
|
||||||
|
if (!item || !qty || isNaN(qty) || isNaN(price)) continue; // skip blanks / footer text
|
||||||
|
lines.push({
|
||||||
|
// Optional per-line product code; blank falls back to the panel default in addLine.
|
||||||
|
productCode: iProd >= 0 ? (rows[r][iProd] || '').trim() : '',
|
||||||
|
description: item + ' - ' + (rows[r][iDesc] || '').trim(),
|
||||||
|
qty: qty,
|
||||||
|
price: price,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Control panel UI ---------------------------------------------------------------------
|
||||||
|
function buildPanel() {
|
||||||
|
if (document.getElementById('ubw-filler-panel')) return;
|
||||||
|
const wrap = document.createElement('div');
|
||||||
|
wrap.id = 'ubw-filler-panel';
|
||||||
|
wrap.style.cssText =
|
||||||
|
'position:fixed;top:70px;right:16px;z-index:100000;width:300px;background:#fff;' +
|
||||||
|
'border:1px solid #b7bcc2;border-radius:6px;box-shadow:0 4px 14px rgba(0,0,0,.18);' +
|
||||||
|
'font:12px/1.4 Arial,sans-serif;color:#222;';
|
||||||
|
const field = (label, id, value) =>
|
||||||
|
'<label style="display:block;margin:6px 0 2px;font-weight:bold">' + label + '</label>' +
|
||||||
|
'<input id="' + id + '" value="' + (value || '') + '" ' +
|
||||||
|
'style="width:100%;box-sizing:border-box;padding:4px;border:1px solid #ccc;border-radius:3px">';
|
||||||
|
wrap.innerHTML =
|
||||||
|
'<div style="background:#f2f4f6;padding:8px 10px;font-weight:bold;border-bottom:1px solid #e0e0e0">' +
|
||||||
|
'UBW Requisition Filler' +
|
||||||
|
'<span id="ubw-close" style="float:right;cursor:pointer;color:#888">×</span></div>' +
|
||||||
|
'<div style="padding:10px">' +
|
||||||
|
field('Request name', 'ubw-name', DEFAULTS.requestName) +
|
||||||
|
field('Default product code', 'ubw-prod', DEFAULTS.productCode) +
|
||||||
|
'<label style="display:block;margin:6px 0 2px;font-weight:bold">Cart CSV</label>' +
|
||||||
|
'<input type="file" id="ubw-file" accept=".csv,text/csv" style="width:100%">' +
|
||||||
|
'<button id="ubw-fill" style="margin-top:10px;width:100%;padding:7px;border:0;border-radius:3px;' +
|
||||||
|
'background:#1f6fbf;color:#fff;font-weight:bold;cursor:pointer">Fill requisition</button>' +
|
||||||
|
'<div id="ubw-log" style="margin-top:8px;max-height:180px;overflow:auto;white-space:pre-wrap;' +
|
||||||
|
'font:11px/1.35 monospace;color:#333;background:#f7f7f7;border:1px solid #eee;border-radius:3px;padding:6px">' +
|
||||||
|
'Ready. Set the Header supplier/budget by hand, then pick a CSV and Fill.</div>' +
|
||||||
|
'<div style="margin-top:6px;color:#a33;font-size:11px">Set the Header (supplier, budget) first. This does not Save or Submit.</div>' +
|
||||||
|
'</div>';
|
||||||
|
document.body.appendChild(wrap);
|
||||||
|
|
||||||
|
const logEl = wrap.querySelector('#ubw-log');
|
||||||
|
const log = (m) => { logEl.textContent += '\n' + m; logEl.scrollTop = logEl.scrollHeight; };
|
||||||
|
|
||||||
|
wrap.querySelector('#ubw-close').onclick = () => wrap.remove();
|
||||||
|
wrap.querySelector('#ubw-fill').onclick = () => {
|
||||||
|
const fileInput = wrap.querySelector('#ubw-file');
|
||||||
|
const cfg = {
|
||||||
|
requestName: wrap.querySelector('#ubw-name').value.trim(),
|
||||||
|
productCode: wrap.querySelector('#ubw-prod').value.trim(),
|
||||||
|
};
|
||||||
|
if (!fileInput.files.length) { log('Pick a CSV file first.'); return; }
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => {
|
||||||
|
let lines;
|
||||||
|
try { lines = linesFromCSV(reader.result); }
|
||||||
|
catch (e) { log('CSV error: ' + e.message); return; }
|
||||||
|
logEl.textContent = 'Parsed ' + lines.length + ' lines from CSV.';
|
||||||
|
wrap.querySelector('#ubw-fill').disabled = true;
|
||||||
|
fillOrder(cfg, lines, log).finally(() => { wrap.querySelector('#ubw-fill').disabled = false; });
|
||||||
|
};
|
||||||
|
reader.readAsText(fileInput.files[0]);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// UBW is a SPA, so the script boots once. A Tampermonkey menu command opens the panel on demand
|
||||||
|
// (so you can navigate to Purchase request first). buildPanel() no-ops if shown, rebuilds after close.
|
||||||
|
const showPanel = () => {
|
||||||
|
if (win.Ext && win.Ext.ComponentQuery) buildPanel();
|
||||||
|
else alert('UBW is still loading its UI. Wait a moment, then try again.');
|
||||||
|
};
|
||||||
|
if (typeof GM_registerMenuCommand === 'function') {
|
||||||
|
GM_registerMenuCommand('Show UBW Requisition Filler', showPanel);
|
||||||
|
} else {
|
||||||
|
// Fallback (e.g. pasted into the DevTools console, where GM_* is unavailable): show at once.
|
||||||
|
const boot = setInterval(() => {
|
||||||
|
if (win.Ext && win.Ext.ComponentQuery) { clearInterval(boot); buildPanel(); }
|
||||||
|
}, 800);
|
||||||
|
}
|
||||||
|
})();
|
||||||
Reference in New Issue
Block a user