test/.env ships DOLIBARR_ADDRESS pointing at PRODUCTION and test/main.ts defaults to it, so any Playwright admin script run with the ambient environment drives the real ERP. The REST path has been structurally safe since ADR-0003 (dol-write.sh refuses non-sandbox hosts); the UI path had no equivalent. scripts/guard.ts closes that gap — assertSandbox() resolves the target and refuses anything that is not erp-sandbox.*, with the override spelled out in the error. Verified: an unqualified run now dies instead of reaching prod. Two settings the write agent cannot reach (non-admin by design, 403 on /setup/conf), rehearsed on the sandbox: - sandboxLegalSetup.ts — capital social + multi-currency module. Finding: the capital was NOT missing, it was stored as "1000€"; the symbol made the value unusable by the PDF template, which is why "Capital de 1 000 €" was absent from every invoice since January (C. com. R.123-238). Normalised to "1000" → the mention now renders. - sandboxCurrencySetup.ts — registers the USD reference rate. Enabling the module is not enough: a currency absent from the rate table makes Dolibarr silently fall back to EUR (observed on a probe invoice). With the rate registered, an invoice carries USD 3,000.00 with its EUR counter-value, i.e. the contractual obligation itself rather than a drifting equivalent. Both scripts are report-only when they cannot recognise a form, and screenshot what they did. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01VRShc4QhLLU73FLHx9vskh
103 lines
3.9 KiB
TypeScript
103 lines
3.9 KiB
TypeScript
/*
|
|
Register the USD reference rate in the multi-currency module (sandbox only).
|
|
|
|
Enabling the module is not enough: an invoice can only carry a currency that
|
|
exists in the module's rate table, otherwise Dolibarr silently falls back to
|
|
EUR (observed: POST /invoices with multicurrency_code=USD returned EUR).
|
|
|
|
The rate is the ECB EUR/USD reference of the day the invoice is settled —
|
|
per the KM amendment, art. 2/4. It is written here so the invoice carries the
|
|
contractual obligation ($2,500 / $3,000) rather than a drifting EUR value.
|
|
The invoice then carries the obligation itself, not a counter-value.
|
|
|
|
Run:
|
|
DOLIBARR_ADDRESS=https://erp-sandbox.arcodange.lab \
|
|
deno run -A test/sandboxCurrencySetup.ts --code USD --rate 1.14416
|
|
*/
|
|
import "load_dotenv";
|
|
import { chromium } from "playwright";
|
|
import login from "./scripts/login.ts";
|
|
import { assertSandbox } from "./scripts/guard.ts";
|
|
|
|
const argv = Deno.args;
|
|
const pick = (flag: string, dflt: string) =>
|
|
argv.includes(flag) ? argv[argv.indexOf(flag) + 1] : dflt;
|
|
const code = pick("--code", "USD");
|
|
const rate = pick("--rate", "1.14416");
|
|
const shotDir = Deno.env.get("SHOT_DIR") ?? "/tmp";
|
|
|
|
const dolibarrAddress = assertSandbox();
|
|
console.log(`target: ${dolibarrAddress} (sandbox verified)`);
|
|
|
|
const adminCredentials = {
|
|
username: Deno.env.get("DOLI_ADMIN_LOGIN") || "undefined",
|
|
password: Deno.env.get("DOLI_ADMIN_PASSWORD") || "undefined",
|
|
};
|
|
|
|
const browser = await chromium.launch({ headless: true });
|
|
const context = await browser.newContext({ locale: "fr-FR" });
|
|
const page = await context.newPage();
|
|
|
|
try {
|
|
await login.doAdminLogin({ page, dolibarrAddress, adminCredentials });
|
|
|
|
// The module's admin page moved across Dolibarr versions — probe the known
|
|
// paths and report which one answers instead of assuming.
|
|
const candidates = [
|
|
"/multicurrency/admin/multicurrency.php",
|
|
"/admin/multicurrency.php",
|
|
"/multicurrency/admin/setup.php",
|
|
];
|
|
let adminUrl = "";
|
|
for (const p of candidates) {
|
|
const resp = await page.goto(`${dolibarrAddress}${p}`, {
|
|
waitUntil: "domcontentloaded",
|
|
});
|
|
const status = resp?.status() ?? 0;
|
|
const isError = await page.locator("text=/Forbidden|not found|erreur/i")
|
|
.count();
|
|
console.log(`probe ${p} -> HTTP ${status}${isError ? " (error page)" : ""}`);
|
|
if (status === 200 && !isError) {
|
|
adminUrl = `${dolibarrAddress}${p}`;
|
|
break;
|
|
}
|
|
}
|
|
if (!adminUrl) {
|
|
console.warn("no multicurrency admin page found — reporting only");
|
|
await page.screenshot({ path: `${shotDir}/currency-probe.png` });
|
|
} else {
|
|
console.log(`admin page: ${adminUrl}`);
|
|
// Add-rate form: a currency code field + a rate field, then submit.
|
|
const codeInput = page.locator(
|
|
'input[name="code"], select[name="code"], input[name="currency_code"]',
|
|
).first();
|
|
const rateInput = page.locator(
|
|
'input[name="rate"], input[name="value"], input[name="currency_rate"]',
|
|
).first();
|
|
if (await codeInput.count() && await rateInput.count()) {
|
|
const tag = await codeInput.evaluate((e) => e.tagName.toLowerCase());
|
|
if (tag === "select") {
|
|
await codeInput.selectOption({ label: new RegExp(code, "i") as never })
|
|
.catch(async () => await codeInput.selectOption(code));
|
|
} else {
|
|
await codeInput.fill(code);
|
|
}
|
|
await rateInput.fill(rate);
|
|
await page.locator('input[type="submit"], button[type="submit"]').first()
|
|
.click();
|
|
await page.waitForLoadState("networkidle");
|
|
console.log(`submitted ${code} @ ${rate}`);
|
|
} else {
|
|
console.warn("add-rate form not recognised — reporting only");
|
|
}
|
|
await page.screenshot({ path: `${shotDir}/currency.png`, fullPage: true });
|
|
console.log(await page.locator("body").innerText().then((t) =>
|
|
t.split("\n").filter((l) => /USD|EUR|taux|rate/i.test(l)).slice(0, 8)
|
|
.join(" | ")
|
|
));
|
|
}
|
|
} finally {
|
|
await context.close();
|
|
await browser.close();
|
|
}
|