From 13c0c03ed4f12420cf4ec64705d85a6f661a4b97 Mon Sep 17 00:00:00 2001 From: JiriUhlir <149317995+JiriUhlir@users.noreply.github.com> Date: Thu, 20 Aug 2026 08:50:03 +0200 Subject: [PATCH] Krok Najit nebo zalozit kontakt a druhy vstup vlastniho skriptu Chybelo k tomu, aby sla postavit cela cesta objednavka -> faktura. - Novy skript idoklad.upsert-contact: dohleda odberatele podle ICO nebo e-mailu, a kdyz neni, zalozi ho a vrati nove ID. Vystup `created` rekne, co se stalo, takze na to jde navazat podminkou. Proc jeden krok a ne dvojice ve vetvich podminky: vetev nepridava nic do sekvence za podminkou, protoze nemusela probehnout. ID odberatele by za podminkou nebylo k dispozici, i kdyz ho ve skutecnosti daji obe vetve. Zaklada se az kdyz hledani nic nenaslo - opacne poradi by pri kazde objednavce vyrobilo dalsiho odberatele se stejnym ICO. - Krok Vlastni skript ma druhy vstup "Co pridat ke vstupu". Skript casto potrebuje krome hlavniho objektu jednu hodnotu z predchoziho kroku, typicky ID odberatele. Prijde v input.extra, aby se hlavni data nemichala s tim, co jsme doplnili my. Co-Authored-By: Claude Opus 5 (1M context) --- scripts/idoklad.upsert-contact.js | 146 ++++++++++++++++++++++++++++++ src/data/services.ts | 9 ++ src/runtime/builtinSteps.ts | 15 ++- 3 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 scripts/idoklad.upsert-contact.js diff --git a/scripts/idoklad.upsert-contact.js b/scripts/idoklad.upsert-contact.js new file mode 100644 index 0000000..14f14db --- /dev/null +++ b/scripts/idoklad.upsert-contact.js @@ -0,0 +1,146 @@ +/** + * iDoklad: najde odberatele, a kdyz neni, zalozi ho. + * + * Sluzba: https://services.csbot.cz/apps/idoklad + * Endpointy: GET /contacts?filter=..., POST /contacts + * + * Proc jeden krok a ne dvojice "najdi" plus "zaloz" ve vetvich podminky: + * vetev podminky **nepridava nic do sekvence za podminkou**, protoze nemusela + * probehnout. ID odberatele by tedy za podminkou nebylo k dispozici, i kdyz + * ho ve skutecnosti daji obe vetve. Sloucit to do jednoho kroku je poctivejsi + * nez to obchazet - a navic je to presne ta operace, kterou clovek chce: + * "chci mit jistotu, ze odberatel v iDokladu je". + * + * Vystup `created` rekne, co se stalo, takze se na to da navazat podminkou + * (poslat uvitaci mail jen novemu zakaznikovi). + */ + +export const manifest = { + id: 'idoklad.upsert-contact', + name: 'Najít nebo založit kontakt', + description: + 'Dohledá odběratele podle IČO nebo e-mailu. Když neexistuje, založí ho ' + + 'a vrátí nové ID. Používá se před vystavením faktury.', + timeoutMs: 25000, + + inputs: [ + { + id: 'identificationNumber', + label: 'IČO', + type: 'string', + required: false, + pattern: '^[0-9]{6,12}$', + hint: 'Jen číslice. Přesnější než e-mail, hledá se podle něj první.', + }, + { + id: 'email', + label: 'E-mail', + type: 'string', + required: false, + hint: 'Použije se, když IČO není k dispozici.', + }, + { + id: 'companyName', + label: 'Název odběratele', + type: 'string', + required: true, + hint: 'U firmy název, u fyzické osoby jméno. Bez něj kontakt založit nejde.', + }, + { id: 'vatNumber', label: 'DIČ', type: 'string', required: false }, + { id: 'street', label: 'Ulice', type: 'string', required: false }, + { id: 'city', label: 'Město', type: 'string', required: false }, + { id: 'postalCode', label: 'PSČ', type: 'string', required: false }, + { + id: 'countryCode', + label: 'Země', + type: 'string', + required: false, + default: 'CZ', + hint: 'Dvoupísmenný kód. Nevyplněno = CZ.', + }, + { id: 'phone', label: 'Telefon', type: 'string', required: false }, + ], + + outputs: [ + { id: 'contactId', label: 'ID kontaktu', type: 'number', required: true }, + { id: 'created', label: 'Kontakt vznikl teď', type: 'boolean', required: true }, + { id: 'companyName', label: 'Název odběratele', type: 'string', required: false }, + { id: 'matchedBy', label: 'Podle čeho se našel', type: 'string', required: true }, + ], +}; + +export async function run(inputs, ctx) { + const { unwrap, pick, text, num, need } = ctx.util; + + const ico = text(inputs.identificationNumber); + const email = text(inputs.email); + + if (!ico && !email) { + ctx.fail('Vyplňte IČO nebo e-mail, jinak není podle čeho hledat ani zakládat.'); + } + + /** Jedno hledani podle jednoho pole. Vraci kontakt, nebo null. */ + async function search(field, value) { + const { body } = await ctx.http.get('/contacts', { + query: { filter: `(${field}~eq~${value})`, filtertype: 'and', pageSize: 1 }, + }); + const items = unwrap(body); + return Array.isArray(items) && items.length > 0 ? items[0] : null; + } + + // Poradi je zamer: ICO je jednoznacne, e-mail muze mit vic firem stejny. + const attempts = []; + if (ico) attempts.push(['IdentificationNumber', ico, 'IČO']); + if (email) attempts.push(['Email', email, 'e-mail']); + + for (const [field, value, label] of attempts) { + const contact = await search(field, value); + if (!contact) { + ctx.log(`Podle ${label} ${value} se nic nenašlo.`); + continue; + } + + ctx.log(`Kontakt nalezen podle ${label}, zakládat se nebude.`); + return { + contactId: need(num(pick(contact, 'id')), 'ID nalezeného kontaktu'), + created: false, + companyName: text(pick(contact, 'companyName', 'name')), + matchedBy: label, + }; + } + + /* + * Zaklada se az tady, kdyz uz je jiste, ze kontakt neexistuje. Poradi je + * dulezite: zalozit napred a hledat potom by pri kazde objednavce vyrobilo + * dalsiho odberatele se stejnym ICO. + */ + const companyName = text(inputs.companyName); + if (!companyName) { + ctx.fail('Kontakt v iDokladu neexistuje a bez názvu odběratele ho nejde založit.'); + } + + const body = { + CompanyName: companyName, + ...(ico ? { IdentificationNumber: ico } : {}), + ...(text(inputs.vatNumber) ? { VatIdentificationNumber: text(inputs.vatNumber) } : {}), + ...(email ? { Email: email } : {}), + ...(text(inputs.phone) ? { Mobile: text(inputs.phone) } : {}), + ...(text(inputs.street) ? { Street: text(inputs.street) } : {}), + ...(text(inputs.city) ? { City: text(inputs.city) } : {}), + ...(text(inputs.postalCode) ? { PostalCode: text(inputs.postalCode) } : {}), + CountryId: null, + ...(text(inputs.countryCode) ? { CountryCode: text(inputs.countryCode) } : {}), + }; + + const response = await ctx.http.post('/contacts', body); + const created = unwrap(response.body); + + ctx.log(`Kontakt ${companyName} založen.`); + + return { + contactId: need(num(pick(created, 'id')), 'ID založeného kontaktu'), + created: true, + companyName: text(pick(created, 'companyName', 'name')) || companyName, + matchedBy: 'založeno', + }; +} diff --git a/src/data/services.ts b/src/data/services.ts index 266b820..e258bd0 100644 --- a/src/data/services.ts +++ b/src/data/services.ts @@ -1762,6 +1762,15 @@ export const services: Service[] = [ required: true, hint: 'Objekt, který skript dostane jako input. Třeba {{_body}}.', }, + { + id: 'extra', + label: 'Co přidat ke vstupu', + kind: 'json', + required: false, + hint: + 'Hodnoty z předchozích kroků, například ' + + '{"partnerId": "{{st_kontakt.contactId}}"}. Skript je najde v input.extra.', + }, ], outputFields: [ { id: 'transform.result', name: 'result', type: 'object', required: true }, diff --git a/src/runtime/builtinSteps.ts b/src/runtime/builtinSteps.ts index b0e0f06..f696762 100644 --- a/src/runtime/builtinSteps.ts +++ b/src/runtime/builtinSteps.ts @@ -544,7 +544,16 @@ const handlers: Record = { } const source = inputs.source ? safeJson(inputs.source) : {}; - const result = runSandbox(script.code, source); + + /* + * Skript casto potrebuje krome hlavniho objektu i jednu hodnotu + * z predchoziho kroku - typicky ID odberatele. Prijde v `input.extra`, + * aby se hlavni data nemichala s tim, co jsme doplnili my. + */ + const extra = inputs.extra?.trim() ? safeJson(inputs.extra) : undefined; + const payload = extra === undefined ? source : { ...source, extra }; + + const result = runSandbox(script.code, payload); noteRun(script.id, script.tenantId, result.ok); if (!result.ok) { @@ -552,7 +561,7 @@ const handlers: Record = { ok: false, summary: `${script.name}: ${result.error?.split('\n')[0] ?? 'chyba'}`, // Cely vstup i chyba. Bez vstupu se chyba v prevodu hleda naslepo. - detail: JSON.stringify({ vstup: source, chyba: result.error }, null, 2), + detail: JSON.stringify({ vstup: payload, chyba: result.error }, null, 2), outputs: {}, }; } @@ -560,7 +569,7 @@ const handlers: Record = { return { ok: true, summary: `${script.name}: hotovo za ${result.durationMs} ms`, - detail: JSON.stringify({ vstup: source, vystup: result.value }, null, 2), + detail: JSON.stringify({ vstup: payload, vystup: result.value }, null, 2), outputs: { result: result.value }, }; },