/* 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} (guard passed)`); 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(); }