Preskoči na glavno vsebino

API Ninja: Tréning 5/7 - Pokročilé akcie a triky

V piatom predposlednom tréningu si ukážeme pokročilé akcie v API, chvaty a finty, ktoré REST API ABRA Flexi ponúka

Avtor: Petr Pech

Získané zručnosti:

  • znalosť akcií a režimov založenia, ktoré API umožňuje

  • znalosť pokročilých služieb v API

Už sme skúsení ninjovia, ktorí zvládnu vyriešiť v API množstvo zložitých úkonov, avšak REST API nám ponúka ešte viac. Niektoré akcie, operácie a služby, ktoré by sa museli všemožne obchádzať alebo doprogramovávať, možno pomocou API vyriešiť jednoduchými chvatmi, ktoré si v tejto kapitole ukážeme.

Na základnej učňovskej úrovni si ukážeme, ako cez API napríklad zmazať, uzamknúť či stornovať doklad pomocou akcie a tiež si ukážeme rôzne režimy založenia záznamu. Keďže budeme dáta meniť, už sa nezaobídeme bez aplikácie ako je napríklad Postman. Bojovník sa naučí niektoré služby, ktoré API ponúka a nemusí ich tak klikať používateľ v aplikácii. Na úrovni Ninja pôjdeme ešte ďalej a ukážeme si iný prístup k API pomocou /query a ďalšie pokročilejšie služby v API. Ďalej si na úrovni Ninja ukážeme transakčné spracovanie. Tento chvat sa môže hodiť, ak potrebujeme import rozdeliť a každý záznam zapísať v samostatnej transakcii. Pripravte si tréningový plac a poďme na to!

Úroveň: Učeň

Režimy založenia a zmeny sa riadia pomocou parametra pri evidencii, v zásade poznáme dva:

  • create

  • update

Predstavme si situáciu, že máme pravidelný import aktualizácií informácií o obchodných partneroch z CRM do ABRA Flexi a chceme nahrávať iba aktualizácie o firmách. Inými slovami, zasielame aktualizácie existujúcich partnerov v ABRA Flexi, ale nechceme zakladať nových obchodných partnerov, aby nevznikli napríklad duplicity. Ako na to?

Na to využijeme režimy založenia. Najprv si ukážeme naznačený príklad, zašleme aktualizáciu firmy do adresára a zamedzujeme tvorbe novej. Požiadavku pošleme pomocou metódy POST na známu URL:

Telo požiadavky obsahuje Flexi XML, ako ho poznáme s jednou malou zmenou. Pridáme akciu, vo forme parametra create= ignore pri elemente evidencie:

<?xml version="1.0"?>
<winstrom version="1.0">
<adresar create="ignore">
<id>code:NINJAFIRMA</id>
<ic>123456789</ic>
<nazev>Ninja firma s.r.o.</nazev>
...další informace
</adresar>
</winstrom>

A to je všetko, <adresar create="ignore"> to je to kúzlo, ktoré zaistí, že pokiaľ firma so skratkou NINJAFIRMA ešte v dátach ABRA Flexi neexistuje, importér firmu nevytvorí a pokiaľ existuje, aktualizuje jej informácie.

Jednoduché, však, učeníku?

Podobným, prísnejším príkladom môže byť kontrola existencie pomocou akcie create='fail'. Atribút create='fail' zakazuje založenie nového záznamu. Import danej firmy teda vypíše chybové hlásenie.

Druhou variantou režimu je update. Ako príklad nám môže poslúžiť import vydaných faktúr do ABRA Flexi. Predstavme si, že chceme zakladať doklady, ale nechceme existujúce faktúry nijako meniť - aktualizovať. Opäť požiadavku pošleme pomocou metódy POST na URL:

Telo požiadavky potom obsahuje XML s vydanou faktúrou rozšírenou o parameter update= ignore pri elemente evidencie:

<?xml version="1.0"?>
<winstrom version="1.0">
<faktura-vydana update="ignore">
<id>code:VF1-0123/21</id>
... povinné náležitosti faktury
</faktura-vydana>
</winstrom>

Jednoducho sme zariadili, že pokiaľ faktúra VF1-0123/21 už v ABRA Flexi existuje, nebude nijako menená. Pokiaľ faktúra neexistuje, vytvorí sa nová.

Oba režimy majú ešte východiskový stav update=ok a create=ok, ktoré sú predvolené a nie je nutné ich uvádzať. Teda update=ok mení všetky existujúce záznamy, ale povolí aj založenie. Create=ok zakladá všetko, ale tiež mení všetko.

Učeníku, dokážeš vymyslieť a otestovať príklad na update=ignore?


To boli režimy založenia a zmeny. Teraz sa pozrieme na akcie, ktoré API ponúka nasledovne:

  • action=delete

  • action=storno

  • action=lock

  • action=lock-for-ucetni

  • action=unlock

Akcie sú pre Ninju chvaty, ktoré umožňujú v API simulovať stlačenie tlačidla ako v aplikácii. Predstavme si, ako bez akcie stornovať doklad. Nastaviť element storno=true, stačí to? Stačí nám rýchla akcia, ktorá stornuje aj naviazané doklady, a to je kus práce bez akcie.

Požiadavka s akciou vyzerá obdobne ako s režimami založenia/zmeny. Prejdeme teda hneď k príkladu, kedy chceme novo založenú faktúru jednoducho a rýchlo stornovať, takže pošleme požiadavku pomocou metódy POST na URL:

A telo už bystrejší učeník iste odvodil:

<?xml version="1.0"?>
<winstrom version="1.0">
<faktura-vydana action="storno">
<id>code:VF1-0123/21</id>
... další informace nejsou potřeba!
</faktura-vydana>
</winstrom>

API ABRA Flexi nám odpovie, že aktualizovalo faktúru, ktorá je teraz stornovaná.

A čo action=delete, učeníku? Učeník, ktorý pozná metódy HTTP, iste premýšľa, aký je rozdiel medzi action=delete a HTTP metódou DELETE? Obe akcie sú v jadre zhodné. Mierne sa líši činnosť pred vyvolaním akcie. Napríklad action=delete dovoľuje zmazanie dokladov aj v inom ako aktuálnom období. Request DELETE toto nedovolí a hlási "No permisson". Action=delete je použiteľné takmer všade. Nemožno ho použiť na používateľoch, alebo štandardných záznamoch prehľadov a pod. Action=delete podľa druhu väzieb vykoná buď zmazanie naviazaných záznamov, zrušenie väzieb alebo nepovolenie akcie.

Poslednou ukážkou v dnešnom tréningu pre učeníka je uzamykanie. Uzamykanie iste poznáte z desktopovej aplikácie, pohodlne pomocou tlačidla uzamkneme, odomkneme, ale ako na to v API. Príklad je už zrejme každému jasný.

Opäť POST požiadavka na URL:

A telo s action=lock:

<?xml version="1.0"?>
<winstrom version="1.0">
<faktura-vydana action="lock">
<id>code:VF1-0123/21</id>
... další informace nejsou potřeba!
</faktura-vydana>
</winstrom>

Chvat so zámkom si ešte rozšírime o dávkové spracovanie. Teda, čo ak chceme uzamknúť niekoľko faktúr naraz, podľa nejakého filtra? Filter uvedieme ako ďalší parameter a máme vyhraté:

<?xml version="1.0"?> 
<winstrom version="1.0">
<faktura-vydana action="lock-for-ucetni" filter="stavUhrK = 'stavUhr.uhrazeno'">
</faktura-vydana>
</winstrom>

Touto požiadavkou sme uzamkli všetky vydané faktúry, ktoré sú v stave Uhradené. Ale pozor, uzamkli sme ich tentoraz pre všetkých okrem pani účtovníčky, resp. používateľa, ktorý má rolu Účtovník.

Čas trénovať, učeníku, priestoru je dosť, porovnať mazanie akciou a mazanie HTTP metódou, či dávkové uzamykanie s inými filtrami a odomykanie, aby nebolo všetko vo Flexi zamknuté. Veľa šťastia!

Úroveň: Bojovník

Bojovník, v dnešnom tréningu si ukážeme rad šikovných služieb, pomocou ktorých v API môžeš automatizovať množstvo úkonov a uľahčiť tak prácu používateľom v aplikácii. Ukážeme si jednoduchšie zo služieb, ak ti tréning nebude stačiť, ďalšie pokročilejšie služby uvedieme v úrovni Ninja. Utiahni si opasok, poďme na vec.

Zameriame sa na skladové služby. Medzi najpoužívanejšie môžeme zaradiť aktualizáciu požiadaviek na výdaj, prepočet skladu, ktorý je odporúčané robiť u veľkých firiem výhradne cez API, či služby inventúry. Sú tu ale aj výstupy ako stav skladu k dátumu a pod.

Začneme požiadavkami na výdaj. V prípade, že je v nastavení firmy povolené generovanie požiadaviek na výdaj, tak možno pomocou REST API zavolať službu Aktualizácia skladových požiadaviek na výdaj. Pre volanie v REST API môžeme využiť HTTP metódy PUT a POST.

Požiadavka vyzerá nasledovne:

Jednoduché, požiadavka nepotrebuje žiadny parameter, žiaľ nie je možné využiť webový prehliadač, ktorý posiela požiadavky metódou GET. V prípade úspešného vykonania služby sa vracia HTTP status 200 a odpoveď:

<?xml version="1.0"?> 
<winstrom version="1.0">
<success>true</success>
</winstrom>

V prípade, že nemáme povolené požiadavky na výdaj v nastavení firmy, REST API nás o tom informuje chybovým stavovým kódom 400 a správou:

<?xml version="1.0"?> 
<winstrom version="1.0">
<success>false</success>
<message>Není povoleno generovat požadavky na výdej.
</message>
</winstrom>

Ďalší jednoduchý, ale veľmi mocný chvat každého API Bojovníka je prepočet skladu. Prepočet skladu má za úlohu skontrolovať a prípadne prepočítať na správne hodnoty ceny na skladovej karte. Ako už bolo spomenuté, u veľkých firiem, čo sa týka objemu skladu, je často jedinou možnosťou prepočtu skladu cez API. Opäť môžeme využiť HTTP metódy PUT a POST. V prípade prepočtu skladu máme však dve možnosti - prepočet celého skladu a prepočet skladovej karty:

Prepočet cez celý sklad má 2 parametre. Parameter ucetniObdobi, ktorý je povinný, je nutné určiť, v akom období sa má sklad prepočítať. Druhý parameter sklad je nepovinný, v prípade viacerých skladov dôjde k prepočtu všetkých. V prípade prepočtu cez skladovú kartu nie je potrebné obdobie uvádzať, keďže skladová karta sama osebe podlieha obdobiu. Môžeme však využiť filtráciu pre získanie záznamu.

Bojovník, kde sa teda vzalo dostupMj? To už poznáme, jedná sa o pole evidencie, prípadne si pripomeň 3. tréning!

Odpoveď API je potom obdobná ako pri aktualizácii požiadaviek na výdaj, úspech 200 - True, neúspech 400 - s popisom chyby.

Máme aktualizované, máme prepočítané, tak čo si výsledky skontrolovať? Ďalej sa pozrieme na službu Stav skladu k dátumu. Stav skladu k dátumu možno získať HTTP metódou GET, je teda možné si požiadavku uložiť do internetového prehliadača ako záložku a ľubovoľne volať z prehliadača:

Parameter skladu je povinný, určíme, ktorý sklad chceme zobraziť. Dátum je nepovinný, v prípade, že ho neuvedieme, získame stav skladu k aktuálnemu dňu. A ako vyzerá výstup? Ako si budeme priať podľa evidencie stav-skladu-k-datu. Poznáme už možnosti filtrácie a detailu z tretieho tréningu, takže si uvedieme jednoduchý príklad:

Bojovník, teraz je na tebe, aby si preveril, ako vyzerá výstup, hurá do toho!

Posledné služby, ktoré si na úrovni Bojovník ukážeme, sú služby pre Inventúru. Čo ak nám niektoré stavy nesedia a je potrebné sklad zinventarizovať? Najprv musíme inventúru založiť pomocou HTTP metódy POST, pošleme do REST API:

<?xml version="1.0"?> 
<winstrom>
<inventura>
<datZahaj>2021-09-30</datZahaj>
<sklad>code:PLZEŇ</sklad>
<typInventury>Kontrola skladu NINJA</typInventury>
<stavK>stavInventury.zahajena</stavK>
</inventura>
</winstrom>

Povinné pole je iba dátum zahájenia datZahaj, ale prečo neuviesť viac a mať v dátach poriadok. Teraz máme predpis inventúry a je potrebné uviesť, aké položky budeme kontrolovať. Predstavme si, že fyzická kontrola stavu prebehla a poznáme reálne množstvo danej položky, pošleme stav položky obratom do inventúry pomocou POST metódy požiadavku:

Obsahom tela bude práve položka a jej stav, nesmieme zabudnúť prepojiť na danú inventúru pomocou ID:

<?xml version="1.0"?>
<winstrom>
<inventura-polozka>
<cenik>code:NINJASŤÍT</cenik>
<mnozMjReal>150</mnozMjReal>
<inventura>3</inventura>
</inventura-polozka>
</winstrom>

Bojovník iste vie, ako získať ID inventúry.

Teraz máme v inventúre položku NINJAŠTÍT s reálnym stavom 150 ks. Programový stav inventúra načítala z aktuálneho stavu skladu. Avšak môže sa stať, že inventúra trvá týždeň a počas inventúry prebehnú pohyby na sklade. Na tieto situácie nám slúži Aktualizuj stavy, ktorá je v API opäť veľmi jednoduchá. Službu voláme pomocou metódy GET, žiadne parametre, žiadne filtre, iba je opäť nutné poznať ID inventúry:

Máme takmer hotovo, máme zinventarizovanú položku, máme aktuálny reálny stav skladu vs. programový stav skladu, posledným krokom je teda vygenerovať inventúrne rozdiely a porovnať tak stavy. Použijeme metódu vygeneruj-doklady. Opäť použijeme metódu GET, pribudne nám však jeden povinný parameter typDokId:

Pre skúsených bojovníkov je iste hračka získať ID typu skladového pohybu, tak iba pre pripomenutie požiadavka na tieto typy vyzerá nasledovne:

Volaním získame všetky typy skladových dokladov. Zvládneš si volanie lepšie prispôsobiť, bojovník?

API odpovedá príslušnými správami podľa chyby alebo úspechu:

Doklady byly úspěšně vygenerovány.

--nebo--

Při inventuře nevznikl žádný inventurní rozdíl.

--nebo--

Na položce s ID = 2 se vyskytla chyba Na skladě není dostatek zboží.

Bojovník, to je pre dnešok všetko, pusti sa do tréningu, je čas vyčistiť sklad!

Úroveň: API Ninja

Ninja, priprav sa, dnes to bude obsiahle a výživné. Naučíme sa ďalšie zo série služieb ako Bojovník. Potom si ukážeme prístup k API pomocou /query a na záver transakčné spracovanie.

REST API v oblasti párovania platieb ponúka rad možností, niektoré z nich si ukážeme na príkladoch. Pokladňu alebo banku možno spárovať s jednou alebo viacerými vydanými alebo prijatými faktúrami.

Keďže sme na úrovni Ninja, nebudeme sa zaoberať tvorbou dokladov, ktorú sme sa už naučili, a pristúpime rovno k párovaniu. Párovanie vždy zasielame metódou POST buď na endpoint banka alebo pokladni-pohyb v závislosti od toho, aké doklady chceme párovať. Poďme na príklad:

<?xml version="1.0"?> 
<winstrom version="1.0">
<banka>
<id>code:B+001/2021</id>
<sparovani>
<uhrazovanaFak type="faktura-vydana" castka="1000">
code:FV1
</uhrazovanaFak>
<uhrazovanaFak type="faktura-vydana">code:FV2</uhrazovanaFak>
<zbytek>ignorovat</zbytek>
</sparovani>
</banka>
</winstrom>

Prvou časťou je identifikácia banky, využívame identifikáciu podľa interného čísla. Zároveň možno v tomto kroku banku aj založiť, teda ak uvedieme ďalšie povinné informácie evidencie banka, môžeme bankový pohyb rovno založiť. Potom prichádza samotné sparovani. Ako názov elementu uhrazovanaFak napovedá, špecifikujeme tu, aký doklad uhrádzame. V jednom spárovaní možno uhrádzať viacero faktúr naraz. Pri spárovaní s viacerými faktúrami musia byť všetky uvedené faktúry rovnakého typu faktúry (vydané alebo prijaté). Pri každej uhrádzanej faktúre možno uviesť atribút castka, ktorého hodnota obmedzuje celkovú sumu na úhradu, ktorá bude z faktúry uhradená. Hodnota atribútu castka nesmie prekročiť zostávajúcu sumu na úhradu na uhrádzanej faktúre.

Ďalší element je zbytek, ktorý môže nadobúdať viacero hodnôt, pretože zvyšok môže nastať, keď uhrádzajúca suma na uhrádzajúcom doklade a súčet súm na uhrádzaných faktúrach nesúhlasia (napr. pri kurzovom rozdiele alebo chýba doplatiť pár korún). Možné hodnoty sú:

  • ne: zvyšok nesmie nastať; ak k nemu dôjde, API vráti chybu

  • zauctovat: zvyšok sa zaúčtuje

  • ignorovat: zvyšok sa ignoruje

  • castecnaUhrada: ak je suma na uhrádzajúcom doklade menšia ako na uhrádzanom, jedná sa o čiastočnú úhradu

  • castecnaUhradaNeboZauctovat: ak je suma na uhrádzajúcom doklade väčšia ako na uhrádzanom, zvyšok sa zaúčtuje; ak je menšia, jedná sa o čiastočnú úhradu

  • castecnaUhradaNeboIgnorovat: ak je suma na uhrádzajúcom doklade väčšia ako na uhrádzanom, zvyšok sa ignoruje; ak je menšia, jedná sa o čiastočnú úhradu

Ninja, teraz poznáš princíp, iste ťa napadá, čo sa stane, keď sa líši mena dokladov? Možno spárovať pokladňu alebo banku v domácej mene s faktúrami v cudzej mene. Krásny príklad na tréning!

Párovanie by sme mali, čo ak potrebujeme odpárovať? To je veľmi jednoduché, bez atribútov a veľmi obdobné sparovani existuje odparovani. POST metóda, požiadavka na totožný endpoint banka alebo pokladni-pohyb a telo požiadavky vyzerá nasledovne:

<?xml version="1.0"?> 
<winstrom version="1.0">
<banka>
<id>code:B+001/2021</id>
<odparovani>
<uhrazovanaFak type="faktura-vydana">code:FV1</uhrazovanaFak>
</odparovani>
</banka>
</winstrom>


Opäť možno uviesť viacero uhrádzaných faktúr alebo aj žiadnu, odpárujú sa tak všetky naviazané na daný bankový pohyb.

API samozrejme ponúka aj automatické párovanie, ktoré má rad možných nastavení, aby bol API Ninja chvat čo najefektívnejší. Opäť posielame POST požiadavku:

Použili sme filtráciu na bankové pohyby vystavené od 1. 10. 2021 a ďalej niekoľko parametrov, ktoré nám umožňujú párovanie riadiť. Možnosti sú nasledujúce:

mod – mód automatického párovania:

  • varCasUcet: párovať podľa variabilného symbolu a sumy a účtu

  • varCas: párovať podľa variabilného symbolu a sumy (predvolená hodnota)

  • jenVar: párovať podľa variabilného symbolu

  • jenCastka: pripojiť – párovať, keď súhlasí suma a nesúhlasí VS

obdobi – v ktorých obdobiach sa budú hľadať doklady na úhradu

  • aktualni: aktuálne účtovné obdobie

  • aktualni-predchozi: aktuálne aj predchádzajúce účtovné obdobie

  • vsechna: všetky účtovné obdobia (predvolená hodnota)

ignorovat-rozdil-castka – aký veľký rozdiel medzi úhradou a uhrádzaným dokladom ignorovať (predvolená hodnota 0.0 – sumy musia zodpovedať, v móde jenVar sa nastavenie rozdielu ignoruje)

zauctovat-rozdil – či sa zaúčtujú doklady, ak dôjde k spojeniu úhrad, kedy nie sú sumy dokladov zhodné (predvolená hodnota true – vznikne interný doklad na rozdiel medzi dokladmi a doklady budú plne spárované)

Ninja, v oblasti služieb API je stále čo preskúmavať, na to nám však tréning nestačí - väzby ZDD, zápočty, priznanie DPH, inicializácia obdobia, a ďalšie. Nudiť sa nedá, trénovať a zlepšovať sa áno!

Pomocou volania /query možno zaslať všetky parametre a filtre, ktoré sa štandardne posielajú v URL adrese, pomocou tela volania. V tejto dokumentácii si popíšeme základnú funkčnosť. V tele volania POST možno zaslať úroveň detailu, stránkovanie, filtráciu a pod. Pre /query vždy používame HTTP metódu POST. Aktuálne je /query dostupné iba vo formáte JSON. Ukážeme si príklad nad vydanou faktúrou, volanie štandardne vyzerá takto:


Telo potom môže vyzerať napríklad takto:

{ "winstrom": { 

"detail":"custom:kod,nazFirmy,datVyst,datSplat,zbyvaUhradit,
storno,juhSum,sumCelkem,stavUhrK,sumCelkemMen,mena(kod),
stredisko(nazev,kod,id),typDokl(typDoklK)",

"includes":"/faktura-vydana/mena,/faktura-vydana/stredisko,/faktura-vydana/typDokl",

"filter":"(kod like \"2021\" and datSplat lt now() and mena eq \"31\" and typDokl eq \"code:FAKTURA\")",

"no-ext-ids":"true",
}
}

Filter obsahuje logické operátory and alebo or prípadne ďalšie, ako je uvedené v dokumentácii filtrácie. Ďalej sú tu escapované významové úvodzovky pomocou spätných lomítok pre zápis reťazcov. Taktiež si môžete všimnúť funkciu now(), ktorá zaisťuje odovzdanie dnešného dátumu. Možná je aj kombinácia, kedy niektoré parametre zapíšeme do URL:

Telo je potom totožné:

{ "winstrom": { 
"detail":"custom:kod,sumCelkem,varSym,typDokl,firma(email,tel)",

"limit":"0",

"filter" : "(datVyst > 2021-06-01) and typDokl = \"code:OBP\" ",

"includes":"/faktura-vydana/stredisko",

"order":["sumCelkem","kod"],
}
}

Jedná sa o iný prístup k API, napríklad ak chcete získavať dáta pomocou POST požiadaviek a nechcete parametre uvádzať v adrese, kde ich môže používateľ vidieť.

Posledné, čo si v dnešnom tréningu ukážeme, je transakčné spracovanie. V predvolenom nastavení sa každý import cez REST API vykonáva ako jedna databázová transakcia – teda buď sa uloží všetko, čo zašleme, alebo vôbec nič. Toto správanie však možno zmeniť pokročilou variantou XML importu, ktorá pri nevhodnom použití môže viesť k nekonzistencii dát. Trénujeme, takže sa nie je čoho báť!

Ukážeme si rovno príklad. Predstavme si situáciu, kedy tvoríme vydané faktúry a zasielame túto požiadavku:

<winstrom version="1.0" atomic="false">
<skupina-zbozi update="ignore">
<kod>VYBAVENI</kod>
<id>code:VYBAVENI</id>
<nazev>Ninjovské vybavení</nazev>
</skupina-zbozi>
<cenik>
<nazev>Ninjovské ostří</nazev>
<id>code:NINJAOSTRI</id>
<typZasobyK>typZasoby.zbozi</typZasobyK>
<skupZboz>code:VYBAVENI</skupZboz>
</cenik>
</winstrom>

Môže však nastať situácia, kedy transakčné správanie nie je nutné, potom možno atribútom atomic toto správanie zmeniť. Ak nastavíte atribút atomic na hodnotu false, importuje sa každý záznam v samostatnej transakcii. Teda v príklade vyššie prebehnú dve databázové transakcie, jedna pre skupinu tovaru VYBAVENI, druhá pre cenník NINJAOSTRI. Ak niektorý import zlyhá, ďalší sa môže bez problémov dokončiť.

Aký to prináša úžitok? Keď importujete veľké XML s mnohými záznamami, napríklad dávku faktúr, transakcia trvá dlho a mnoho informácií sa musí držať v pamäti. Oboje má nepriaznivý vplyv na výkon. Ak je však každý záznam samostatný a nevadí, že uloženie niektorého z nich zlyhá (napr. keď import pravidelne opakujete a/alebo dokážete v prípade problémov zasiahnuť ručne), môžete podstatne znížiť pamäťovú náročnosť importu.

To je pre dnešný tréning všetko, Ninja, je čas trénovať samostatne. Odporúčame rôzne kombinácie dnes naučeného.

Nie je čas strácať čas, môžeš pokračovať na posledný tréning - používateľské tlačidlo

Ste s tem dobili odgovor na svoje vprašanje?