# -*- coding: utf-8 -*- """ POLEO FISCAL - exemple de integrare, intr-un singur fisier. Nu are nevoie de alte instlari in afara Python 3.8+, cu biblioteca standard. Pentru programatorul care leaga un program de vanzari la o casa de marcat prin Poleo Fiscal. Ruleaza exemplele pe rand, arata cererea trimisa si raspunsul primit, si explica ce s-a intamplat. python poleo_demo.py --lista # ce exemple exista python poleo_demo.py 5 # ruleaza un exemplu python poleo_demo.py 5 7 19 # mai multe python poleo_demo.py toate # toate, mai putin cele periculoase python poleo_demo.py 30 --da # si cele periculoase (raport Z), cu acceptul tau CONFIGURARE - trei valori, din variabile de mediu sau modificate mai jos: set POLEO_URL=http://127.0.0.1:9111 # adresa statiei set POLEO_TOKEN=... # Setari -> API -> "Copiaza tokenul" set POLEO_CASA=DB9999999999 # NUI-ul casei (exemplul 2 ti-l arata) ### DE CITIT INAINTE DE PRIMA LINIE DE COD IN PRODUCTIE ### Bonul fiscal (inscrisul tiparit), este foarte complicat de anulat si comporta anumite constrangeri si consecinte. De aceea, orice operatiune care lasa urma trebuie trimisa cu o CHEIE DE OPERATIUNE (`Idempotency-Key`): daca reteaua cade dupa ce bonul a iesit si programul tau reincearca, cheia opreste al doilea bon si iti intoarce rezultatul primului. Exemplul 31 arata asta pe viu. Procedura anularii unui bon fiscal (tiparit pe casa de marcat cu mentiunea 'BON FISCAL' este complexa si este indicat nu doar sa nu abuzezi de ea, dar sa nici nu ajungi sa-ti fie necesara). Nu trimite 'probe' catre casa de marcat. Daca vrei sa te asiguri ca ai stabilit conexiunea cu aceasta, fara ca asta sa fie o recomandare, poti cere un raport X (Atentie! NU Z). Fișierul este un catalog de exemple, nu rulează nimic fără argumente. În PowerShell: $env:POLEO_TOKEN=""; $env:POLEO_CASA=""; & path_catre/python.exe path_catre/poleo_demo.py --lista """ import argparse import json import os import sys import time import urllib.error import urllib.request try: sys.stdout.reconfigure(encoding="utf-8", errors="replace") except Exception: # noqa: BLE001 pass BAZA = os.environ.get("POLEO_URL", "http://127.0.0.1:9111").rstrip("/") TOKEN = os.environ.get("POLEO_TOKEN", "PUNE-TOKENUL-AICI") CASA = os.environ.get("POLEO_CASA", "") # gol = statia alege casa ei obisnuita - ca goode practice, nu recomandam asta ci recomandam sa identifici casa dupa NUI (id-ul unic al fiecarui aparat fiscal; similar cu seria de sasiu -VIN- a unei masini), acesta fiind singurul element ce nu se va schimba indiferent de orice alta variabila; VERSIUNE = "v1" TIMEOUT_SCURT = 15 # casa poate tipari zeci de secunde la un raport din memoria fiscala. Un timeout de 5 secunde, obisnuit in clientii HTTP, ar taia exact operatiunile lungi - si ai crede ca au esuat, desi ele continua la casa. TIMEOUT_LUNG = 180 # == transportul ====== class EroareApi(Exception): """un refuz al statiei, cu tot cu explicatia ei `cod` - statusul HTTP (400, 401, 422, 501, 503 ...) `cod_eroare` - codul STABIL din raspuns ("bad_request", "device_error", "unsupported" ...); pe ASTA te ramifici in cod, nu pe mesaj: mesajul este pentru om si se poate schimba fara sa strice integrarea ta `mesaj` - propozitia pentru om `detalii` - ce a mai trimis statia (ex. ce terminal a refuzat) """ def __init__(self, cod, mesaj, corp=None, cod_eroare="", detalii=None): super().__init__("HTTP %s: %s" % (cod, mesaj)) self.cod = cod self.mesaj = mesaj self.corp = corp or {} self.cod_eroare = cod_eroare self.detalii = detalii or {} def cere(metoda, cale, corp=None, cheie=None, timeout=TIMEOUT_SCURT, brut=False): """o cerere catre Poleo Fiscal -> dict (sau octeti, cu `brut=True`) `cheie` devine antetul `Idempotency-Key`; pune-o la TOT ce lasa urma: bonuri, rapoarte, numerar; foloseste un identificator al TAU - numarul comenzii, al bonului din gestiune - nu ceva generat la fiecare incercare, fiindca atunci fiecare reincercare ar fi alta operatiune""" url = "%s/api/%s/%s" % (BAZA, VERSIUNE, cale.lstrip("/")) date = None if corp is None else json.dumps(corp, ensure_ascii=False).encode("utf-8") r = urllib.request.Request(url, data=date, method=metoda) r.add_header("Authorization", "Bearer " + TOKEN) r.add_header("Accept", "application/json") if date is not None: r.add_header("Content-Type", "application/json; charset=utf-8") if cheie: r.add_header("Idempotency-Key", str(cheie)) try: with urllib.request.urlopen(r, timeout=timeout) as raspuns: octeti = raspuns.read() except urllib.error.HTTPError as e: octeti = e.read() try: d = json.loads(octeti.decode("utf-8", "replace") or "{}") except ValueError: d = {} er = d.get("error") if isinstance(er, dict): # forma normala a API-ului raise EroareApi(e.code, er.get("message") or e.reason, d, cod_eroare=er.get("code") or "", detalii=er.get("details") or {}) raise EroareApi(e.code, d.get("message") or (er if isinstance(er, str) else "") or e.reason, d) except urllib.error.URLError as e: raise EroareApi(0, "nu am ajuns la statie (%s). Ruleaza Poleo Fiscal? E pornit API-ul din Setari -> API? Adresa %s este buna?" % (e.reason, BAZA)) if brut: return octeti return json.loads(octeti.decode("utf-8", "replace") or "{}") def _pe_casa(): """filtrul de casa pentru listele de evidenta; pe o statie cu mai multe case, listele intorc salvarile TUTUROR caselor; daca apoi ceri detaliile uneia care nu este a ta, primesti 404 - nu fiindca este stricat ceva, ci fiindca ai intrebat de casa altcuiva; deci filtram de la inceput""" return ("&case=" + CASA) if CASA else "" def bon(receipt, cheie, case=None): """trimite un bon fiscal -> raspunsul statiei; `receipt` este dictionarul cu articole si plati""" corp = {"receipt": receipt} tinta = case or CASA if tinta: corp["case"] = tinta return cere("POST", "receipts", corp, cheie=cheie, timeout=TIMEOUT_LUNG) # == afisare ====== def titlu(text): print("\n" + "-" * 74) print(text) print("-" * 74) def arata(eticheta, obiect): print("\n%s:" % eticheta) print(json.dumps(obiect, ensure_ascii=False, indent=2)) def nota(text): for linie in text.strip().split("\n"): print(" " + linie.strip()) def cheie_demo(nume): """cheie de operatiune pentru exemple; ATENTIE, aici este o ALTA cheie la fiecare rulare (are timestamp in ea) - altfel al doilea rulaj al demonstratiei n-ar mai tipari nimic, ti-ar intoarce rezultatul primului si ai crede ca s-a stricat ceva. IN PROGRAMUL TAU cheia trebuie sa fie STABILA pentru aceeasi vanzare, ca reincercarea sa nu tipareasca al doilea bon""" return "demo-%s-%d" % (nume, int(time.time() * 1000)) # == 1-4: legatura si ce stie statia ====== def ex1_conectare(): """esti acolo? si ce stii sa faci?""" titlu("1. Conectarea la API") arata("GET /health (singura cale care nu cere token)", cere("GET", "health")) metode = cere("GET", "methods") print("\nMetodele pe care le stie ACEASTA statie (%d):" % len(metode.get("methods", []))) for m in metode.get("methods", []): print("%-6s %-28s %s" % (m.get("method"), m.get("path"), m.get("about", ""))) nota("""Cere /methods la pornirea integrarii tale, nu presupune. O statie mai veche poate sa nu aiba ultimele metode, iar asa afli din cod, nu dintr-o eroare la client.""") def ex2_casele(): """de unde iei identificatorul casei""" titlu("2. Ce case sunt configurate (si de unde iei NUI-ul)") r = cere("GET", "cases") arata("GET /cases", r) nota("""NUI-ul este identificatorul casei. Pune-l in `POLEO_CASA` sau trimite-l la fiecare cerere, in campul `case`. Pe o statie cu o singura casa poti sa-l omiti - dar daca maine se adauga a doua, cererile fara `case` devin ambigue. Trimite-l de la inceput. Identificarea dupa denumire functioneaza si ea, dar daca un utilizator decide intr-o zi sa ii schimbe denumirea, vei avea probleme. Similar si cu id-ul inregistrarii din db, un utilizator poate edita si inversa conexiunile. De asta recomandam sa folosesti seria (NUI-ul) casei care va fi acelasi pe toata durata vietii casei si nici nu preconizam ca va mai fi vreun alt aparat AMEF eliberat cu seria asta vreodata (este unica).""") return [c.get("nui") for c in r.get("cases", [])] def ex3_stare(): """conectata? monitorizata? ziua fiscala este deschisa? care-i care? - conectata -> casa de marcat este conectat la Poelo Fiscal; fie prin COM, fie TCP-IP, fie bluetooth, Poleo Fiscal 'ajunge' la casa - monitorizata -> driverul 'se uita' in folderul de bonuri monitorizate; daca vrei sa nu deconectezi casa de marcat, dar sa verifici daca softul trimite bonurile in folder poti intrerupe monitorizarea si Poleo Fiscal nu va tipari bonurile, desi casa este conectata - ziua fiscala deschisa -> o casa fiscala poate inregistra vanzari timp de 24 de ore, iar dupa 24 de ore va cere emiterea unui raport Z (raportul care pune toate valorile pe 0 si 'inchide' ziua); fara sa emiti acel raport, niciun alt bon nu se va printa; este util sa vezi daca ziua este 'deschisa' si daca nu trebuie tiparit un raport Z inainte de a trimite la listat pentru ca, asa cum ziceam putin mai devreme, dupa scurgerea termenului de 24 de ore (chiar fara a fi inregistrat vanzari de la 'deschiderea ultimei zile'), casa nu mai accepta tiparirea altor bonuri pana nu emite un raport Z; """ titlu("3. Starea casei, intrebata acum") if not CASA: print("(pune POLEO_CASA ca sa interoghezi o casa anume)") cale = "cases/%s/status" % (CASA or "-") arata("GET /" + cale, cere("GET", cale, timeout=TIMEOUT_LUNG)) nota("""Ce iti da CHIAR aceasta metoda: daca aparatul raspunde ACUM (nu daca este configurat), plus ce a raspuns el despre sine - model, serie, hartie, capac. Fiecare apel vorbeste chiar cu aparatul, deci intreaba la intervale rezonabile, nu in bucla. Ce NU iti da: starea monitorizarii si ziua fiscala. Pe acestea doua le afli din GET /problems (exemplul 18), care iti spune inclusiv de cate ore este deschisa ziua si daca urmeaza blocarea la 24 de ore. Monitorizarea NU se poate opri prin API - se opreste doar din aplicatie, de la "Case de marcat" -> "Opreste monitorizarea". Este intentionat: oprirea monitorizarii inseamna ca bonurile lasate in folder nu se mai tiparesc, iar asta este o decizie de om, nu una pe care sa o poata lua un program de la distanta.""") def ex4_config(): """cotele de TVA si sloturile de incasare - amandoua vin de aici; casa de marcat este configurata cu niste mapari de forma 'cota 0 -> numerar | cota 1 -> card' sau 'indice 1 -> TVA 21% | indice 2 -> TVA 11% '; trebuie sa le cunosti pentru ca tu vei scrie acele informatii in structura bonului; de exemplu, vei zice ca produsul este incasat cu numerar si are TVA 21%;""" titlu("4. Cotele de TVA si metodele de incasare, citite din casa") cale = "cases/%s/config" % (CASA or "-") r = cere("GET", cale, timeout=TIMEOUT_LUNG) arata("GET /" + cale, r) disp = (r.get("case") or {}).get("device") or {} print("\nCote programate in aparat:") for v in disp.get("vat", []): print("grupa %-3s = %s%%" % (v.get("group"), v.get("rate"))) print("Sloturi de incasare:") for p in disp.get("payments", []): print("index %-3s = %s" % (p.get("index"), p.get("name"))) nota("""Nu sunt doua liste separate in doua metode: TVA-ul si incasarile vin impreuna, din configuratia casei, fiindca amandoua sunt programate de tehnicianul care a fiscalizat aparatul. Astea sunt adevarul - nu presupune ca 21% este mereu grupa A sau ca 0 = NUMERAR pentru ca vei altera datele fiscal-contabile.""") # == 5-16: bonuri ====== def ex5_bon_numerar(): """bonul de baza""" titlu("5. Bon fiscal platit cu NUMERAR") r = bon({"items": [{"name": "APA PLATA 0.5L", "price_val": 3.50, "qty": 2, "unit": "buc", "vat_rate": 21}], "payments": [{"method": "cash", "amount": 7.00}]}, cheie=cheie_demo("numerar")) arata("POST /receipts", r) nota("""`receipt_no` este numarul bonului tiparit de casa. Pune-l in evidenta ta - este legatura dintre vanzarea din programul tau si hartia pe care o are clientul. Raspunsul vine DUPA ce bonul a iesit; doua-trei secunde este normal.""") return r.get("receipt_no") def ex6_citeste_bonul(): """ce s-a tiparit, vazut din evidenta""" titlu("6. Citirea raspunsului: ultimul bon din evidenta") lista = cere("GET", "receipts?limit=1") arata("GET /receipts?limit=1", lista) bonuri = lista.get("receipts") or [] if bonuri: bid = bonuri[0].get("id") arata("GET /receipts/%s (cu liniile lui)" % bid, cere("GET", "receipts/%s" % bid)) nota("""Raspunsul imediat al lui POST /receipts iti da numarul. Lista de aici este pentru verificare ulterioara: reconciliere de sfarsit de zi, sau aflarea a ce s-a intamplat cu un bon care a intrat in coada cat timp casa era deconectata.""") def ex7_bon_card(): """plata cu cardul""" titlu("7. Bon fiscal platit cu CARDUL") r = bon({"items": [{"name": "CAFEA LA FILTRU", "price_val": 12.00, "qty": 1, "unit": "buc", "vat_rate": 21}], "payments": [{"method": "card", "amount": 12.00}]}, cheie=cheie_demo("card")) arata("POST /receipts", r) nota("""`method: "card"` spune casei sa treaca suma pe slotul de card. Pe ce index sta cardul la aparatul tau ai vazut la exemplul 4. Asta TIPARESTE bonul; nu cere banii de pe card. Pentru terminalul bancar este /pos.""") def ex8_plati_card(): """ce s-a incasat cu cardul, inclusiv platile ramase neclarificate""" titlu("8. Citirea raspunsului pentru platile cu cardul") arata("GET /card-payments?limit=5", cere("GET", "card-payments?limit=5")) nota("""Pastreaza `auth` si `rrn`: cu ele reconciliezi cu extrasul bancii si cu ele se lamureste o disputa. Plata INCERTA este cazul care conteaza: terminalul a primit comanda, dar raspunsul nu a ajuns inapoi, deci cardul poate sa fi fost debitat. Nu relua plata automat - se verifica pe terminal si se confirma de un om.""") def ex9_bon_adaos(): """majorare - pe articol si pe tot bonul; anumite aparate AMEF nu suporta astfel de metode; Tremol sau Datecs suporta, dar nu toate aparatele o fac; """ titlu("9. Bon cu ADAOS (majorare)") r = bon({"items": [{"name": "LIVRARE URGENTA", "price_val": 20.00, "qty": 1, "vat_rate": 21, "adjust_kind": "sur_pct", "adjust_value": 15}], "payments": [{"method": "cash", "amount": 23.00}]}, cheie=cheie_demo("adaos")) arata("POST /receipts", r) nota("""`adjust_kind`: sur_val (majorare in lei) sau sur_pct (procent). Produsul apare la pretul intreg, iar majorarea pe randul de sub el - asa cere legea, clientul trebuie sa vada din ce s-a compus suma. `payments.amount` trebuie sa fie totalul DUPA ajustare: 20 + 15% = 23,00.""") def ex10_bon_reducere(): """reducere pe articol si pe tot bonul; iti recomandam sa individualizezi expres si fara exceptie produsul pe care il reduci, atunci cand reduci un produs; chiar daca aplici si o reducere globala si o reducere punctuala, individualizeaza expres produsul redus punctual -asta are nu doar implicatii fiscal-contabile ci si juridice-;""" titlu("10. Bon cu REDUCERE") print("\n--- a) CORECT: reducerea individualizata PE PRODUSUL redus ---") r = bon( { "items": [ { "name": "TRICOU BUMBAC", "price_val": 80.00, "qty": 1, "unit": "buc", "vat_rate": 21, "adjust_kind": "disc_val", "adjust_value": 10.00}, { "name": "SOSETE BUMBAC", "price_val": 20.00, "qty": 2, "unit": "buc", "vat_rate": 21} ], "payments": [ { "method": "cash", "amount": 110.00 } ] }, cheie=cheie_demo("reducere-produs") ) arata("POST /receipts (reducere doar pe tricou)", r) print("\n--- b) reducere pe TOT bonul, peste una de produs ---") r = bon( { "items": [ { "name": "TRICOU BUMBAC", "price_val": 80.00, "qty": 1, "unit": "buc", "vat_rate": 21, "adjust_kind": "disc_val", "adjust_value": 10.00} ], "receipt_adjust": { "kind": "disc_pct", "value": 5 }, "payments": [ { "method": "cash", "amount": 66.50 } ] }, cheie=cheie_demo("reducere-bon") ) arata("POST /receipts (produs -10 lei, apoi bon -5%)", r) nota("""CUM SE FACE CORECT (exemplul a): daca reducerea priveste un anumit produs, trimite-o PE ACEL PRODUS, cu `adjust_kind` + `adjust_value` in linia lui. Pe bon iese produsul la pretul intreg si reducerea pe randul de sub el, legata de el. Asa se vede care produs a fost redus si cu cat - individualizare care are urmari fiscal-contabile SI juridice. Poleo Fiscal trimite reducerea la casa CU FELUL EI (valoare sau procent), nu o preface in alt fel: daca ai cerut 10%, la casa pleaca 10%, ca sa nu se aseze rotunjirea noastra peste a aparatului si sa iasa un ban diferit fata de ce a socotit programul tau. CAND se foloseste `receipt_adjust`; (exemplul b): doar pentru o reducere care priveste INTREGUL bon si niciun produs anume - un voucher pe cos, o reducere de fidelitate. NU o folosi ca sa strecori acolo o reducere care de fapt tine de un produs. Cele doua se pot folosi impreuna: 80 - 10 = 70; 70 - 5% = 66,50. Reducerea de bon se sparge pe cote de TVA, proportional - o face APARATUL, nu noi, fiindca fiecare cota isi are TVA-ul ei. Daca trimiti reduceri mai mari decat marfa, cererea este refuzata cu explicatie: un bon cu total zero sau negativ nu are sens fiscal.""") def ex11_plati_multiple(): """clientul plateste o parte cu cardul, restul cash (sau altfel, dar nu cu un singur mijloc de plata); operatie cunoscuta si ca metoda mixta""" titlu("11. Bon cu MAI MULTE METODE DE PLATA") r = bon({"items": [{"name": "COS CUMPARATURI", "price_val": 150.00, "qty": 1, "vat_rate": 21}], "payments": [{"method": "card", "amount": 100.00}, {"method": "cash", "amount": 50.00}]}, cheie=cheie_demo("plati")) arata("POST /receipts", r) nota("""Suma platilor trebuie sa dea exact totalul bonului. Casa tipareste fiecare forma de plata pe randul ei, iar raportul Z le desface la fel - de aceea ordinea si sumele conteaza pentru impacarea sertarului la sfarsitul zilei. `method` poate fi: cash, card, other.""") def ex12_cote_multiple(): """mai multe cote TVA pe acelasi bon""" titlu("12. Bon cu MAI MULTE COTE DE TVA") r = bon({"items": [{"name": "PAINE INTEGRALA", "price_val": 5.00, "qty": 2, "vat_rate": 11}, {"name": "DETERGENT VASE", "price_val": 15.00, "qty": 1, "vat_rate": 21}, {"name": "TIMBRU POSTAL", "price_val": 2.00, "qty": 1, "vat_rate": 0}], "payments": [{"method": "cash", "amount": 27.00}]}, cheie=cheie_demo("cote")) arata("POST /receipts", r) nota("""POTRIVIREA O DAI TU, NU O GHICIM NOI. Cotele si sloturile de incasare sunt programate in aparat de tehnicianul care l-a fiscalizat. Noi nu avem de unde sti ca la aparatul TAU grupa A este 21% si nu 9%, sau ca indicele 1 este cardul si nu numerarul. Daca am presupune, am gresi tacut - si s-ar vedea abia in raportul Z, cand nu mai poti repara. Deci: citeste maparea reala cu exemplul 4 (GET /cases/{nui}/config) si trimite `vat_group` luat de acolo. Daca trimiti doar `vat_rate`, il traducem dupa conventia obisnuita (21->A, 11->B, 9->C, 5->D, 0->E) - o conventie, nu un adevar despre aparatul tau. Cel mai sigur: trimite-le pe amandoua, potrivite intre ele. Cand trimiti DOAR grupa, bonul de pe hartie este corect, dar in evidenta noastra linia intra cu TVA 0 - deci tabloul de bord si /totals iti vor arata alte cifre decat hartia. DACA ESTI NEPLATITOR DE TVA: nu inseamna "cota 0". Aparatul se programeaza cu o grupa pentru operatiuni fara TVA (de regula ultima, dar NUMAI configuratia casei tale o spune), iar tu trimiti acea grupa. "TVA 0%" si "scutit / neplatitor" arata la fel ca suma, dar nu sunt acelasi lucru pe bon si nici in declaratii - intreaba-ti contabilul ce grupa ti-a cerut sa fie programata. SI CEL MAI IMPORTANT: daca se schimba ceva in aparat - se reprogrameaza cotele de TVA sau indicii de incasare - trebuie schimbat SI in programul tau, in aceeasi zi. Aparatul nu te anunta, iar bonurile vor continua sa iasa; doar ca pe alta cota si pe alta forma de plata.""") def ex13_bon_complet(): """cazul real, cu tot ce se poate intampla intr-un cos""" titlu("13. Bonul complet: produse, cote, reduceri pe articol si pe bon, plati multiple") r = bon( { "client": "EXEMPLU COMERT SRL", "client_vat": "RO99999999", "items": [ { "name": "LAPTE 1.5% 1L", "price_val": 6.50, "qty": 3, "unit": "buc", "vat_rate": 11}, { "name": "CAFEA BOABE 1KG", "price_val": 89.90, "qty": 1, "unit": "buc", "vat_rate": 21, "adjust_kind": "disc_pct", "adjust_value": 10}, { "name": "BRANZA TELEMEA", "price_val": 32.00, "qty": 0.354, "unit": "kg", "vat_rate": 11}, { "name": "PUNGA BIODEGRADABILA", "price_val": 0.50, "qty": 1, "unit": "buc", "vat_rate": 21} ], "receipt_adjust": { "kind": "disc_val", "value": 5.00 }, "payments": [ { "method": "card", "amount": 100.00}, { "method": "cash", "amount": 7.24 } ], "texts": [ "Card fidelitate: 4471", "Puncte acumulate: 112" ] }, cheie=cheie_demo("complet") ) arata("POST /receipts", r) nota(""" Ce se vede aici, pe langa cele de dinainte: - `qty` accepta zecimale, pentru cantar (0.354 kg) - `client` + `client_vat` tiparesc datele firmei pe bon (bon cu CUI) - `texts` adauga randuri libere, netaxabile - numere de card de fidelitate, mesaje SOCOTEALA, ca sa o poti verifica: 3 x 6,50 = 19,50; 89,90 - 10% = 80,91; 0,354 kg x 32,00 = 11,33; punga 0,50. Subtotal 112,24; minus 5,00 pe bon = 107,24. De aceea platile sunt 100,00 card + 7,24 numerar. Suma platilor TREBUIE sa dea exact totalul - altfel cererea este refuzata inainte sa se atinga hartia. ATENTIE la bonul cu CUI: incasarea in NUMERAR de la o persoana juridica este plafonata legal la 5.000 lei (la persoane fizice este 10.000 de lei). Poleo Fiscal te avertizeaza, iar din Setari poti cere chiar blocarea. """) def ex14_storno(): """retur de marfa""" titlu("14. Bon de RETUR (storno)") r = bon({"items": [{"name": "TRICOU BUMBAC", "price_val": 80.00, "qty": -1, "vat_rate": 21}], "payments": [{"method": "cash", "amount": -80.00}]}, cheie=cheie_demo("storno")) arata("POST /receipts", r) nota("""NU EXISTA, si o spunem inainte sa pierzi timp: cererea este REFUZATA de noi, cu 400, inainte sa se atinga hartia. De ce: casele romanesti cer pentru storno un DOCUMENT de alt tip, cu referinta la bonul initial (numar, data, numarul memoriei fiscale) - nu o vanzare cu cantitate negativa. Probat pe DP-25 (17.08.2026): trimis ca vanzare negativa, aparatul incepe documentul si il refuza la mijloc, deci iese un BON ANULAT, cu numar de bon consumat. Pentru restituirea banilor pe un bon deja emis exista METODA EI: POST /returns, exemplul 42. Ea face retragerea de numerar (sau virarea pe card), marcheaza in evidenta ce s-a returnat si scoate procesul-verbal de semnat. Bonul fiscal initial ramane valabil - asta nu este o limitare a noastra, asa lucreaza casele.""") def ex15_stil_coduri(): """text ingrosat, aliniat, si coduri de bare""" titlu("15. Text stilizat si coduri de bare pe bon") r = bon( { "items": [ { "name": "VOUCHER CADOU", "price_val": 50.00, "qty": 1, "unit": "buc", "vat_rate": 21} ], "payments": [ { "method": "cash", "amount": 50.00 } ], "texts": [ "Valabil 6 luni de la emitere", { "text": "MULTUMIM!", "bold": True, "double_height": True, "align": "center" }, { "barcode": "5941234567890", "type": "EAN13" }, { "barcode": "https://magazinul-tau.ro/voucher/4471", "type": "QR" } ] }, cheie=cheie_demo("stil") ) arata("POST /receipts", r) nota("""Stiluri: bold, italic, underline, double_height, align (left/center/right). Coduri: EAN8, EAN13, CODE128, ITF (I2OF5), PDF417, QR. Fiecare tip are lungimile lui acceptate (EAN13 = fix 13 cifre) - o valoare gresita este refuzata INAINTE sa plece la casa, cu explicatie, ca sa nu iasa un bon stramb. Nu toate casele stiu toate codurile; ce nu suporta aparatul se tipareste ca text (unele case nu suport QR, altele nu stiu sa encodeze EAN / CODE samd). DESPRE CODUL QR TIPARIT PE BON, acelasi avertisment pe care il vezi si in aplicatie, la Setari -> Bon & aspect: "Acest cod QR nu este generat conform dreptului pozitiv conex, iar imprimarea acestuia nu echivaleaza indeplinirii obligatiilor astfel prevazute. De la data aplicarii normelor specifice, recomandam armonizarea acestora cu metoda de print si, dupa caz, eliminarea acestei optiuni din driverul Poleo. Indiferent de contextul legislativ, este necesara consultarea -si a- tehnicianului dumneavoastra de case de marcat." Cu alte cuvinte: codul QR pe care il pui tu aici are rol comercial si nu acopera exigentele dreptului pozitiv. Daca pentru a tipari codul QR indicat de legiuitor este nevoie sa renunti la acesta -comercial-, iti recomandam sa te conformezi normele de drept.""") def ex16_copie(): """clientul a pierdut bonul""" titlu("16. Copia ultimului bon") arata("POST /copy", cere("POST", "copy", {}, cheie=cheie_demo("copie"), timeout=TIMEOUT_LUNG)) nota("""Iese o copie marcata NEFISCAL. Nu este un bon nou, nu intra in totaluri si nu inlocuieste originalul - este doar dovada a ce s-a vandut. Unele case de marcat nu suporta astfel de comenzi (comanda ilegala). Atentie nu retipari acel bon trimitand un nou request si opreste functionarul din a apasa retiparire pentru un bon pentru ca asta va dubla acea vanzare (casa va considera ca ai perfectat doua tranzactii si nu una).""") # == 17-18: deblocare ====== def ex17_void(): """casa a ramas blocata, cu un bon deschis""" titlu("17. Anularea bonului ramas deschis") try: arata("POST /void", cere("POST", "void", {}, cheie=cheie_demo("void"), timeout=TIMEOUT_LUNG)) except EroareApi as e: print("\nRefuz (normal, daca nu este niciun bon deschis): HTTP %s - %s" % (e.cod, e.mesaj)) nota("""Asta este butonul de deblocare. O vanzare inceputa si neterminata tine casa ocupata, iar bonurile urmatoare nu mai ies. Iese pe hartie un document marcat ANULAT. Merge si cu licenta expirata - o casa blocata trebuie sa poata fi deblocata. Recomandam ferm ca dupa un astfel de eveniment, sa tiparesti un raport X, gestionarul sa-si verifice casa (isi numara banii din sertar, vede un settlement de pe POS samd) si sa continui vanzarea DOAR dupa ce exista certitudinea ca toate rapoartele 'bat'. Daca vezi o diferenta intre soldul casei de marcat si soldul de sertar / POS, este posibil ca acel bon sa nu fi fost fiscalizat. Spunem este posibil pentru ca la fel de adevarat este si este posibil sa fi fiscalizat acel bon, dar gestionarul sa fi gresit in alt moment al zilei fiscale. Trebuie lamurita diferenta (posibil egala cu valoarea sumei ultimului bon) si pentru asta, recomandat este sa confrunti un raport JE pe acea zi cu vanzarile pe care le-ai trimis spre casa de marcat. Desi poti identifica eroarea de soft: un bon netransmis spre fiscalizare, o retragere sau depunere neconsemnata pe aparatul fiscal samd, softul nu iti va spune unde este eroarea gestionarului.""") def ex18_probleme(): """ce nu este in regula acum""" titlu("18. Problemele active ale casei") arata("GET /problems", cere("GET", "problems")) nota("""Aici sunt lucrurile pe care le-ar vedea omul in panoul de alerte: hartie terminata, ziua fiscala deschisa de prea mult timp, rapoarte Z netrimise la ANAF, memorie fiscala aproape plina, plafon de numerar depasit. Clientul tau le rezolva mai repede decat daca le afla de la casier.""") # == 19-20: rapoarte pe casa ====== def ex19_raport_x(): """Fotografia zilei, fara reset""" titlu("19. Tiparirea unui raport X") arata("POST /reports {kind: X}",cere("POST", "reports", {"kind": "X"}, cheie=cheie_demo("x"), timeout=TIMEOUT_LUNG)) nota("""Raportul X nu schimba nimic: nu inchide ziua, nu pune casa pe zero, se poate cere de cate ori situatia o cere. E raportul de control al schimbului.""") def ex20_nomenclator(): """ce este programat in casa""" titlu("20. Rapoarte de nomenclator (verificarea maparii)") for fel, ce in (("D", "departamente"), ("G", "grupe"), ("OPERATORI", "operatori"), ("PLU", "articole")): try: r = cere("POST", "reports", {"kind": fel}, cheie=cheie_demo("nom-" + fel), timeout=TIMEOUT_LUNG) print("%-10s (%s): %s" % (fel, ce, r.get("message") or "OK")) except EroareApi as e: print("%-10s (%s): refuzat - %s" % (fel, ce, e.mesaj)) nota("""Cu ele iti verifici maparea INAINTE sa vinzi o luna pe grupa gresita. Nu ating ziua fiscala, nu ocupa loc in memorie si nu se pot desface, fiindca nu schimba nimic. Totusi, iti zicem de acum ca unele case nu suporta aceste metode.""") # == 21-23: numerar ====== def ex21_depunere(): """fondul de rulaj, dimineata""" titlu("21. Depunerea de numerar") arata("POST /cash {kind: in}",cere("POST", "cash", {"kind": "in", "amount": 200, "reason": "Fond de rulaj"}, cheie=cheie_demo("dep"), timeout=TIMEOUT_LUNG)) nota("""Casa tipareste un document nefiscal, iar soldul sertarului creste. `reason` este optional dar ajunge in evidenta - peste o luna o sa-ti para bine ca l-ai trimis.""") def ex22_retragere(): """ridicarea incasarilor""" titlu("22. Retragerea de numerar") arata("POST /cash {kind: out}", cere("POST", "cash", {"kind": "out", "amount": 150, "reason": "Ridicare incasari"}, cheie=cheie_demo("retr"), timeout=TIMEOUT_LUNG)) nota("""Se foloseste ca sa nu depasesti plafonul de casa (50.000 lei, Legea 296/2023). Poleo Fiscal te avertizeaza din timp, la pragul pe care il pui in Setari.""") def ex23_sertar(): """cat este in sertar acum""" titlu("23. Soldul sertarului si plafonul") arata("GET /drawer", cere("GET", "drawer")) nota("""Soldul este calculat din bonuri, depuneri si retrageri. Daca vrei doar sa-l deschizi, este POST /drawer - si acela merge si cu licenta expirata.""") # == 24-26: evidenta si ANAF ====== def ex24_totaluri(): """ziua, in cifre""" titlu("24. Totalurile zilei si defalcarea pe cote") arata("GET /totals", cere("GET", "totals")) arata("GET /vat", cere("GET", "vat")) nota("""Sunt cifrele din evidenta noastra, nu din memoria casei - deci raspund instant si nu deranjeaza aparatul. Pentru adevarul fiscal al zilei, raportul X sau Z. Metoda este dependenta de capacitatea casei de a furniza aceste informatii.""") def ex25_anaf_stare(): """cate rapoarte Z n-au fost transmise""" titlu("25. Starea raportarii catre ANAF") arata("GET /anaf", cere("GET", "anaf")) nota("""Transmiterea o face CASA, prin conexiunea ei la internet (TCP, WIFI, GSM, etc) - nu programul. Aici afli doar cate n-au plecat. Netransmiterea atrage sanctiuni, deci merita o alerta la tine. Uneori netransmiterea vine din modificarea parolei de WiFi si neschimbarea ei peste tot, din scoaterea unui cablu network din casa de marcat, din motive nevinovate asa ca nu-i rau sa ai raspunsul acestei metode sub observatie.""") def ex26_raport_periodic(): """ce cere contabilul la inchiderea lunii""" titlu("26. Raport periodic din memoria fiscala") arata("POST /reports {kind: periodic}", cere("POST", "reports", {"kind": "periodic", "from": "01-08-2026", "to": "31-08-2026", "detailed": False}, cheie=cheie_demo("periodic"), timeout=TIMEOUT_LUNG)) nota("""Datele sunt zi-luna-an. `detailed: true` scoate fiecare Z din interval, false doar totalurile. Citeste din memoria fiscala, deci pe intervale mari dureaza - de aceea exemplul foloseste un timeout lung. In functie de numarul de vanzari, poti astepta chiar zeci de minute pentru un raport pe o perioada mai indelungata. Este important sa cunosti limitarile de memorie pe care casele le au la stocarea acestor rapoarte (chiar si doar 2 luni in urma).""") # == 27-29: jurnal electronic ====== def ex27_export_je(): """generarea exportului pentru ANAF / jurnalul electronic""" titlu("27. Generarea exportului de jurnal electronic (ANAF)") arata("POST /anaf", cere("POST", "anaf", {"from": "01-08-2026", "to": "31-08-2026"}, cheie=cheie_demo("je"), timeout=TIMEOUT_LUNG)) nota("""Casa citeste din memoria fiscala si scrie fisierele semnate. `message` spune unde au ajuns; poti da `out_dir` ca sa alegi tu folderul. Pe un punct de vanzare cu flux mare, poate dura minute intregi. Nu taia conexiunea - operatiunea continua la casa chiar daca tu ai renuntat la raspuns.""") def ex28_lista_je(): """ce salvari exista""" titlu("28. Salvarile de jurnal electronic") r = cere("GET", "je?limit=3" + _pe_casa()) arata("GET /je?limit=3 (doar casa noastra)", r) exp = (r.get("exports") or []) if exp: eid = exp[0].get("id") arata("GET /je/%s (fisierele salvarii)" % eid, cere("GET", "je/%s" % eid)) nota("""ATENTIE la ce citeste aceasta metoda: registrul salvarilor DEJA FACUTE PE DISC, nu memoria aparatului. Nu se atinge de casa si raspunde instant. Registrul este umplut de BACKUP-UL AUTOMAT de jurnal (Setari -> Firma & backup), cel care ruleaza dupa raportul Z. Verificat pe aparat (17.08.2026): exportul ANAF cerut cu exemplul 27 scrie fisierul in folderul de export al casei, dar NU adauga un rand aici - sunt doua lucruri diferite, nu doua trepte ale aceluiasi lucru. Deci: 27 = export ANAF pe interval, la cerere; 28 = ce salvari de jurnal am pe disc; 29 = da-mi un fisier dintr-o salvare. Pe o statie cu mai multe case filtreaza cu `&case=`: altfel lista da salvarile tuturor caselor, iar detaliile uneia straine intorc 404. Fisierele .p7b sunt semnate de casa - ele constituie jurnalul cerut de lege. Daca lista este goala desi ai emis rapoarte Z, inseamna ca backup-ul automat nu este pornit, nu ca aparatul n-are jurnal.""") def ex29_descarca_je(): """fisierul propriu-zis, adus la tine""" titlu("29. Descarcarea unui fisier semnat de jurnal") r = cere("GET", "je?limit=1" + _pe_casa()) exp = (r.get("exports") or []) if not exp: print("Nicio salvare inca - ruleaza intai exemplul 27.") return eid = exp[0].get("id") fisiere = (cere("GET", "je/%s" % eid).get("files") or []) if not fisiere: print("Salvarea nu are fisiere.") return nume = fisiere[0].get("name") if isinstance(fisiere[0], dict) else fisiere[0] octeti = cere("GET", "je/%s/files/%s" % (eid, nume), timeout=TIMEOUT_LUNG, brut=True) with open(nume, "wb") as f: f.write(octeti) print("Descarcat: %s (%d octeti), in folderul curent." % (nume, len(octeti))) nota("""Raspunsul este fisierul BINAR, nu JSON. Nu-l trece prin json.loads - scrie-l pe disc asa cum vine, altfel semnatura casei devine invalida.""") # == 30-32: definitive, idempotenta, erori ====== def ex30_raport_z(): """inchiderea zilei; ireversibila""" titlu("30. Tiparirea unui raport Z (INCHIDE ZIUA FISCALA)") ziua = time.strftime("%Y-%m-%d") arata("POST /reports {kind: Z}", cere("POST", "reports", {"kind": "Z"}, cheie="z-" + ziua, timeout=TIMEOUT_LUNG)) nota("""Nu are undo. Consuma o zi fiscala din memoria casei si produce documentul pe care il vede contabilitatea si controlul. Observa cheia: `z-2026-08-17`, cu ZIUA in ea, NU cu ceasul. Daca reteaua cade si programul tau reincearca, aceeasi cheie opreste a doua inchidere. Asta este singura aparare impotriva a doua rapoarte Z in aceeasi zi.""") def ex31_idempotenta(): """aceeasi cerere, de doua ori; un singur bon""" titlu("31. Cheia de operatiune: aceeasi cerere, trimisa de doua ori") cheie = "demo-idem-%d" % int(time.time()) corp = {"items": [{"name": "TEST IDEMPOTENTA", "price_val": 1.00, "qty": 1, "vat_rate": 21}], "payments": [{"method": "cash", "amount": 1.00}]} print("\nPrima trimitere, cu cheia %s:" % cheie) arata("raspuns", bon(corp, cheie=cheie)) print("\nA doua trimitere, cu ACEEASI cheie:") arata("raspuns", bon(corp, cheie=cheie)) nota("""A iesit UN SINGUR bon. A doua cerere n-a mai executat nimic - ti-a intors rezultatul primei. Asta este tot ce te desparte de un al doilea bon fiscal tiparit dintr-un timeout de retea. Pune cheia la orice lasa urma, si fa-o stabila pentru aceeasi vanzare.""") def ex32_erori(): """cum arata un refuz si ce faci cu el""" titlu("32. Tratarea erorilor") incercari = [ ( "bon fara articole", lambda: bon( { "items": [], "payments": [] }, cheie=cheie_demo("e1") ) ), ( "reduceri mai mari decat marfa", lambda: bon( { "items": [ { "name": "X", "price_val": 10, "qty": 1, "vat_rate": 21, "adjust_kind": "disc_val", "adjust_value": 20} ], "payments": [ { "method": "cash", "amount": 0 } ] }, cheie=cheie_demo("e2") ) ), ( "fel de raport inexistent", lambda: cere( "POST", "reports", { "kind": "QQQ" }, cheie=cheie_demo("e3") ) ), ] for nume, f in incercari: try: f() print("%-34s -> a trecut (neasteptat)" % nume) except EroareApi as e: print("%-34s -> HTTP %s %-14s %s" % (nume, e.cod, e.cod_eroare or "-", e.mesaj)) nota(""" Cum le imparti in programul tau: 400 - cererea ta este gresita. NU reincerca; repar-o. 401 - token gresit sau lipsa. Nu reincerca in bucla: incercarile gresite sunt incetinite progresiv, per adresa. 422 - casa a refuzat, sau licenta nu permite. Mesajul spune de ce. Arata-l omului. 503 - casa este deconectata. Bonul poate intra in coada; verifica mai tarziu starea. timeout - NU STII daca s-a tiparit. Reincearca CU ACEEASI CHEIE. Niciodata fara. """) # == 33-38: restul aparatului si al statiei ====== def ex33_sertar_deschide(): """sertarul, fara sa tiparesti nimic""" titlu("33. Deschiderea sertarului") arata("POST /drawer", cere("POST", "drawer", {}, cheie=cheie_demo("sertar"), timeout=TIMEOUT_LUNG)) nota("""Necesita un sertar legat electric la casa. Ca si anularea bonului deschis, functioneaza si cu licenta expirata - sunt cele doua supape ale programului. Unele case nu pot gestiona sertarele de bani (desi au port dedicat) sau nu pot gestiona toate situatiile in care un sertar trebuie deschis, iar altele sunt dezactivate din soft. Pe site gasesti mai multe detalii.""") def ex34_pos_card(): """suma trimisa la terminalul bancar""" titlu("34. Plata la terminalul de card (POS bancar)") try: arata( "POST /pos", cere( "POST", "pos", { "amount": 12.00 }, cheie=cheie_demo("pos"), timeout=TIMEOUT_LUNG ) ) except EroareApi as e: print("\nRefuz (normal, daca nu ai terminal configurat): HTTP %s - %s" % (e.cod, e.mesaj)) nota("""Asta CERE BANII de pe card; nu tipareste bonul. Fluxul obisnuit e: /pos intai, si doar daca terminalul a aprobat, /receipts cu plata pe card. Daca raspunsul terminalului nu ajunge inapoi, plata ramane INCERTA - vezi exemplul 8. Nu o relua automat: cardul poate fi deja debitat.""") def ex35_terminale(): """ce terminale sunt configurate si CE STIE fiecare; ce poate fiecare terminal, scos la vedere: cu asta afli INAINTE ce nu are rost sa ceri""" titlu("35. Terminalele de card configurate") r = cere("GET", "terminals") arata("GET /terminals", r) for t in (r.get("terminals") or []): poate = t.get("can") or {} print("%-22s %-16s retur: %-5s anulare: %-5s inchidere de zi: %s" % (t.get("name") or "-", t.get("model") or "-", poate.get("refund"), poate.get("void"), poate.get("settlement"))) nota("""Cu mai multe terminale, aici afli pe care poti ruta plata: al casei care tipareste, sau unul anume - de exemplu cel al bancii cu comisionul mai mic. `can` spune ce operatiuni ARE fiecare terminal: retur, anulare din borderoul curent, inchidere de zi. Citeste-l INAINTE sa promiti ceva clientului - nu costa nimic, se citeste din driver, nu de pe fir. Ce este `false` acolo nu se reincearca niciodata: operatiunea va raspunde 501 cu `unsupported` oricand ai incerca, iar solutia este alt terminal sau alt contract cu banca. Astazi, dintre terminalele pe care le stim, niciunul nu are retur.""") def ex36_licenta(): """cat mai are licenta""" titlu("36. Licenta fiecarei case") arata("GET /license", cere("GET", "license")) nota("""O licenta expirata opreste tiparirea bonurilor, rapoartele X si Z, cele periodice si de nomenclator, si miscarile de numerar. Afla inainte, nu in dimineata in care casa refuza.""") def ex37_statie(): """cartea de vizita a statiei""" titlu("37. Amprenta statiei, locatia si casele ei") arata("GET /station", cere("GET", "station")) nota("""AMPRENTA este numele sub care te cunosc partenerii. Nu are legatura cu licenta si nu se schimba singura - nici la actualizare, nici daca schimbi portul, IP-ul sau casa. Daca integrezi un serviciu din alta retea, cu ea te inregistrezi la el.""") def ex38_restul_evidentei(): """Ce mai poti citi, pe scurt.""" titlu("38. Restul evidentei: numerar, rapoarte tiparite, declaratii, posta") for cale, ce in ( ("cash?limit=3", "miscarile de numerar"), ("reports?limit=3", "rapoartele tiparite"), ("declarations?limit=3", "declaratiile de bon gresit"), ("emails?limit=3", "posta de iesire") ): try: arata("GET /" + cale + " (" + ce + ")", cere("GET", cale)) except EroareApi as e: print("/%s -> HTTP %s: %s" % (cale, e.cod, e.mesaj)) # PDF-ul unei declaratii de bon gresit: singurul raspuns BINAR in afara de fisierele de jurnal. decl = (cere("GET", "declarations?limit=1").get("declarations") or []) if decl: did = decl[0].get("id") pdf = cere("GET", "declarations/%s/file" % did, timeout=TIMEOUT_LUNG, brut=True) nume = os.path.abspath("declaratie-%s.pdf" % did) # raspunsul este BINAR, nu JSON: se scrie pe disc asa cum vine with open(nume, "wb") as f: f.write(pdf) print("PDF-ul declaratiei %s: %d octeti -> %s" % (did, len(pdf), nume)) else: print("(nicio declaratie de bon gresit - normal pe o casa fara incidente)") nota("""Cu astea, plus ce ai vazut mai sus, exemplele acopera TOATE metodele API-ului. Lista completa si mereu la zi o da chiar statia, la /methods - exemplul 1 sau site-ul https://poleo.ro/docs/api. Depunem eforturi pentru a tine si acest fisier in sync, dar pot aparea unele intarzieri.""") # == 39-41: retur, anulare si inchidere de zi pe terminalul de card ====== # cele trei nu exista pe orice terminal si nu sunt in orice contract cu banca; de aceea raspunsul lor are un caz in plus fata de "a mers / n-a mers": HTTP 501 cu `error: "unsupported"`, adica "aparatul asta nu are operatiunea"; este un raspuns DEFINITIV - nu se reincearca niciodata def _card_operatie(nume, cale, corp, cheie): try: arata("POST /" + cale, cere("POST", cale, corp, cheie=cheie, timeout=TIMEOUT_LUNG)) except EroareApi as e: # 501 + `unsupported` = raspuns DEFINITIV. Ramificatia se face pe COD, nu pe mesaj. if e.cod == 501 or e.cod_eroare == "unsupported": print("\nNESUPORTAT de terminalul asta: %s" % e.mesaj) print("Asta NU se reincearca - nici acum, nici maine. Solutia este alt terminal, alt") print("model, sau alt contract cu banca.") if e.detalii: arata("ce aparat a refuzat", e.detalii) else: print("\n%s -> HTTP %s (%s): %s" % (nume, e.cod, e.cod_eroare or "-", e.mesaj)) def ex39_retur_card(): """banii inapoi la client, pe card""" titlu("39. Retur pe card") _card_operatie("Retur", "pos/refund", {"amount": 25.00}, cheie_demo("retur")) nota("""Returul scoate bani REALI din contul comerciantului, deci nu se face automat: il ceri cu o decizie in spate, ca la casa de marcat. E altceva decat anularea: returul merge oricand, chiar si dupa ce borderoul s-a inchis; anularea (exemplul 40) merge doar cat timp plata este in borderoul curent. Unele aparate POS bancare nu suporta astfel de metode - afla DINAINTE, din `can.refund` la /terminals (exemplul 35), ca sa nu descoperi asta dupa ce clientul a predat marfa. ATENTIE, sunt doua lucruri diferite: asta este returul PE TERMINAL (banii pe cardul clientului), pe cand exemplul 42 este returul PE BON (restituirea, marcarea bonului si procesul-verbal), iar acela le foloseste pe amandoua cand plata initiala a fost cu cardul.""") def ex40_anulare_card(): """anularea unei plati din borderoul curent""" titlu("40. Anularea unei plati cu cardul") _card_operatie("Anulare", "pos/void", {"stan": "000123", "amount": 25.00}, cheie_demo("anulare")) nota("""Are nevoie de STAN (numarul tranzactiei din terminal, primit in raspunsul platii) SI de suma exacta. Terminalele cer amandoua tocmai ca sa nu anulezi alta tranzactie. Pastreaza STAN-ul la fiecare plata, altfel nu mai ai cu ce anula.""") def ex41_settlement(): """inchiderea de zi a terminalului bancar""" titlu("41. Inchiderea de zi a terminalului (settlement)") _card_operatie("Settlement", "pos/settlement", {}, cheie_demo("settlement")) nota("""Trimite borderoul la banca si porneste decontarea. NU are legatura cu raportul Z al casei de marcat - sunt doua zile diferite, a terminalului si a aparatului fiscal. Nu o facem noi automat: are efect asupra banilor, deci o cere omul.""") # == 42-44: restituirea banilor pe un bon emis, si documentele ei ====== # aici nu se tipareste nimic pe casa in afara de retragerea de numerar; bonul fiscal initial ramane valabil, iar ce iese este HARTIA care justifica banii scosi din sertar def _ultimul_bon_tiparit(): """id-ul ultimului bon TIPARIT din evidenta - returul si declaratia lucreaza pe el""" for b in (cere("GET", "receipts?limit=10").get("receipts") or []): if b.get("status") == "ok": return b.get("id"), b return None, {} def ex42_retur(): """banii inapoi la client, pe un bon deja emis""" titlu("42. Retur pe un bon emis (restituire + proces-verbal)") bid, b = _ultimul_bon_tiparit() if not bid: print("(niciun bon tiparit in evidenta - ruleaza intai exemplul 5)") return print("bonul ales: %s nr. fiscal %s %s lei" % (bid, b.get("fiscal_no"), b.get("total"))) try: arata("POST /returns", cere("POST", "returns", {"receipt": bid, "reason": "produs defect, adus inapoi de client", "requested_by": "demo", "acknowledged": True}, timeout=TIMEOUT_LUNG)) except EroareApi as e: print("\nretur -> HTTP %s (%s): %s" % (e.cod, e.cod_eroare or "-", e.mesaj)) if e.detalii: arata("ce s-a inregistrat totusi", e.detalii) arata("GET /returns?limit=3", cere("GET", "returns?limit=3")) nota("""`acknowledged: true` si `reason` sunt OBLIGATORII: un retur scoate bani din sertar fara sa iasa marfa pe usa, deci nu se face din reflex si nu se face fara motiv scris. Pentru un singur produs trimite `items: [{"line_no": 1, "name": "...", "qty": 1, "amount": 40.00}]`, unde `line_no` este pozitia liniei in bon, de la 0. Acelasi lucru nu se poate returna de doua ori: se tine socoteala si pe bon, si pe linie. Daca plata initiala a fost cu cardul, se incearca virarea prin terminal; daca terminalul nu stie operatiunea, returul ramane `approved` (de reglat) si restituirea se face pe alta cale. Daca aparatul refuza retragerea, returul ramane `failed` si NU iese niciun document - o hartie de restituire pentru bani care n-au plecat ar fi o hartie mincinoasa. `document` din raspuns este calea procesului-verbal: se tipareste si se semneaza de gestionar SI de client.""") def ex43_declaratie(): """documentul pentru un bon emis gresit""" titlu("43. Declaratia de bon gresit intocmit") arata("GET /declaration-reasons", cere("GET", "declaration-reasons")) bid, b = _ultimul_bon_tiparit() if not bid: print("(niciun bon tiparit in evidenta - ruleaza intai exemplul 5)") return motive = [m.get("key") for m in (cere("GET", "declaration-reasons").get("reasons") or [])][:1] try: arata("POST /declarations", cere("POST", "declarations", {"receipt": bid, "reasons": motive, "operator": "demo", "cash_out": False, "acknowledged": True}, timeout=TIMEOUT_LUNG)) except EroareApi as e: print("\ndeclaratie -> HTTP %s (%s): %s" % (e.cod, e.cod_eroare or "-", e.mesaj)) nota("""NU anuleaza bonul - bonul fiscal ramane in memoria fiscala si in jurnalul electronic, si ramane acolo. Ce iese este documentul prin care gestionarul arata de ce vanzarea nu a fost buna; el il tipareste si il SEMNEAZA. Nu se cere parola si nici aprobare pe email (asa era pana pe 17.08.2026): cine semneaza, raspunde. `cash_out: true` doar daca banii chiar se scot din sertar. `reasons` sunt CHEILE din /declaration-reasons, nu textele. Tot ce s-a anulat si s-a returnat intr-o zi pleaca a doua zi dimineata, intr-un email catre contabilitate.""") def ex44_firma(): """datele care apar pe documente""" titlu("44. Datele firmei (fara ele, documentele ies incomplete)") r = cere("GET", "company") arata("GET /company", r) if r.get("missing"): print("\nLIPSESC: %s" % ", ".join(r["missing"]) + "\nCompleteaza-le cu POST /company, altfel procesul-verbal de retur si declaratia de bon gresit ies cu lipsa scrisa pe ele.") nota("""Denumirea, forma juridica, sediul, Registrul Comertului, CUI-ul si capitalul social sunt mentiunile pe care Legea 31/1990 (art. 74) le cere pe documentele societatii. Trimiti doar campurile pe care le schimbi. Cu `case` (NUI-ul casei) scrii datele DOAR pentru casa aceea: acelasi calculator poate deservi case ale unor firme diferite, iar un document iesit cu firma gresita este o problema reala, nu una de interfata.""") def ex45_setari(): """ce se poate schimba din afara, si ce nu""" titlu("45. Setarile statiei") r = cere("GET", "settings") print("se pot schimba: %s blocate: %s\n" % (r.get("writable"), r.get("locked"))) for s in (r.get("settings") or []): if s.get("writable"): print(" %-32s %-6s = %s" % (s.get("key"), s.get("type"), s.get("value"))) print("\nblocate (primele 5, cu motivul lor):") for s in [x for x in (r.get("settings") or []) if not x.get("writable")][:5]: print(" %-32s %s" % (s.get("key"), s.get("reason"))) # o schimbare buna si una refuzata, in ACEEASI cerere: asa se vede ca statia le trateaza separat try: arata("POST /settings", cere("POST", "settings", {"settings": {"bon.sertar_mereu": False, "api.port": "1"}})) except EroareApi as e: print("\nsetari -> HTTP %s (%s): %s" % (e.cod, e.cod_eroare or "-", e.mesaj)) if e.detalii: arata("ce a intrat si ce nu", e.detalii) # PRELUAREA DE PE SFTP, configurata din afara. Lasata COMENTATA dinadins: pornita, statia # incepe sa coboare fisiere de pe serverul tau si sa le tipareasca. Decomenteaz-o cand chiar # vrei asta, cu datele tale. # arata("POST /settings (SFTP)", cere("POST", "settings", {"settings": { # "sftp.on": True, "sftp.host": "sftp.exemplu.ro", "sftp.port": 22, # "sftp.user": "poleo", "sftp.key": "C:/chei/poleo_sftp", # "sftp.dir": "/home/poleo/bonuri", "sftp.pauza": 20}})) nota("""Se pot schimba 69 din cele 73 de setari ale aplicatiei, inclusiv preluarea de pe SFTP (`sftp.*`). Raman blocate PATRU: cele ale serviciului API insusi - pornit, asculta in retea, port, adrese permise din browser. Alea sunt chiar USA, iar un serviciu care isi poate schimba portul sau isi poate rescrie cheia si-ar administra singur accesul. Restul se scriu, inclusiv raportul Z automat, adresele de email, terminalul de card si backupul: cine are jetonul poate deja TIPARI BONURI FISCALE, operatiunea cea mai greu de desfacut din toate, deci a porni aplicatia sau a schimba o adresa e mai putin grav - si tocmai de aceea accesul nu e liber, ci autorizat. PAROLELE se scriu, dar nu se citesc niciodata inapoi: la citire primesti `value: null`, `secret: true` si `completata`. Se aplica TOT ce este bun si se raporteaza TOT ce nu este - nu ne oprim la prima greseala, altfel n-ai sti care dintre celelalte au intrat. Primesti 200 doar daca a intrat ceva; daca nimic n-a trecut, primesti 400 cu aceleasi doua liste in `details`. Aproape toate intra in vigoare pe loc; singura cu efect in afara bazei e pornirea cu Windows, unde rescriem chiar sarcina din sistem.""") # == catalogul ====== # (numar, functie, descriere, periculos); Numerele sunt STABILE: exemplele noi se adauga la coada, nu se renumeroteaza - ca sa poti spune "vezi exemplul 13" si peste un an sa fie tot acela EXEMPLE = [ (1, ex1_conectare, "Conectarea la API si descoperirea metodelor", False), (2, ex2_casele, "Casele configurate (de unde iei NUI-ul)", False), (3, ex3_stare, "Starea casei: conectata, monitorizata, ziua fiscala", False), (4, ex4_config, "Cotele de TVA si metodele de incasare, din casa", False), (5, ex5_bon_numerar, "Bon fiscal platit cu numerar", False), (6, ex6_citeste_bonul, "Citirea raspunsului: bonul din evidenta", False), (7, ex7_bon_card, "Bon fiscal platit cu cardul", False), (8, ex8_plati_card, "Citirea platilor cu cardul (si a celor incerte)", False), (9, ex9_bon_adaos, "Bon cu adaos (majorare)", False), (10, ex10_bon_reducere, "Bon cu reducere, pe articol si pe bon", False), (11, ex11_plati_multiple, "Bon cu mai multe metode de plata", False), (12, ex12_cote_multiple, "Bon cu mai multe cote de TVA", False), (13, ex13_bon_complet, "Bonul complet: de toate, ca in realitate", False), (14, ex14_storno, "Bon de storno (cantitati negative) - NU exista; vezi 42", False), (15, ex15_stil_coduri, "Text stilizat si coduri de bare pe bon", False), (16, ex16_copie, "Copia ultimului bon", False), (17, ex17_void, "Anularea bonului ramas deschis (deblocare)", False), (18, ex18_probleme, "Problemele active ale casei", False), (19, ex19_raport_x, "Tiparirea unui raport X", False), (20, ex20_nomenclator, "Rapoarte de nomenclator (verificarea maparii)", False), (21, ex21_depunere, "Depunerea de numerar", False), (22, ex22_retragere, "Retragerea de numerar", False), (23, ex23_sertar, "Soldul sertarului si plafonul", False), (24, ex24_totaluri, "Totalurile zilei si defalcarea pe cote", False), (25, ex25_anaf_stare, "Starea raportarii catre ANAF", False), (26, ex26_raport_periodic, "Raport periodic din memoria fiscala", False), (27, ex27_export_je, "Generarea exportului de jurnal electronic", False), (28, ex28_lista_je, "Salvarile de jurnal electronic", False), (29, ex29_descarca_je, "Descarcarea unui fisier semnat de jurnal", False), (30, ex30_raport_z, "Raport Z - INCHIDE ZIUA FISCALA, ireversibil", True), (31, ex31_idempotenta, "Cheia de operatiune, demonstrata", False), (32, ex32_erori, "Tratarea erorilor", False), (33, ex33_sertar_deschide, "Deschiderea sertarului", False), (34, ex34_pos_card, "Plata la terminalul de card (POS bancar)", False), (35, ex35_terminale, "Terminalele de card configurate", False), (36, ex36_licenta, "Licenta fiecarei case", False), (37, ex37_statie, "Amprenta statiei si casele ei", False), (38, ex38_restul_evidentei, "Restul evidentei: numerar, rapoarte, declaratii, posta", False), (39, ex39_retur_card, "Retur pe card", False), (40, ex40_anulare_card, "Anularea unei plati cu cardul", False), (41, ex41_settlement, "Inchiderea de zi a terminalului (settlement)", False), (42, ex42_retur, "Retur pe un bon emis (restituire + proces-verbal)", True), (43, ex43_declaratie, "Declaratia de bon gresit intocmit", True), (44, ex44_firma, "Datele firmei care apar pe documente", False), (45, ex45_setari, "Setarile statiei: ce se poate schimba din afara si ce nu", False), ] def lista(): print("Exemple disponibile (`python poleo_demo.py `):\n") for nr, _f, desc, periculos in EXEMPLE: print("%2d %s%s" % (nr, desc, " ! cere --da" if periculos else "")) print("\n toate ruleaza tot, mai putin cele marcate !") def main(): ap = argparse.ArgumentParser(description="Exemple de integrare cu Poleo Fiscal.", epilog="Configurare: POLEO_URL, POLEO_TOKEN, POLEO_CASA (variabile de mediu).") ap.add_argument("ce", nargs="*", help="numere de exemplu, sau 'toate'") ap.add_argument("--lista", action="store_true", help="arata catalogul si iese") ap.add_argument("--da", action="store_true", help="permite si exemplele ireversibile (raport Z)") a = ap.parse_args() if a.lista or not a.ce: lista() return 0 if TOKEN == "PUNE-TOKENUL-AICI": print("Nu ai pus tokenul. Ia-l din Poleo Fiscal -> Setari -> API -> 'Copiaza tokenul',") print("apoi: set POLEO_TOKEN=") return 2 dupa_numar = {nr: (f, d, p) for nr, f, d, p in EXEMPLE} if len(a.ce) == 1 and a.ce[0].lower() in ("toate", "all"): cerute = [nr for nr, _f, _d, p in EXEMPLE if not p or a.da] else: cerute = [] for x in a.ce: if not x.isdigit() or int(x) not in dupa_numar: print("Nu exista exemplul '%s'. Vezi `--lista`." % x) return 2 cerute.append(int(x)) print("Statie: %s casa: %s" % (BAZA, CASA or "(cea implicita a statiei)")) esecuri = 0 for nr in cerute: f, desc, periculos = dupa_numar[nr] if periculos and not a.da: print("\n[%d] %s - SARIT (adauga --da daca chiar vrei)" % (nr, desc)) continue try: f() except EroareApi as e: esecuri += 1 print("\n[%d] %s -> REFUZAT: HTTP %s - %s" % (nr, desc, e.cod, e.mesaj)) except Exception as e: # noqa: BLE001 esecuri += 1 print("\n[%d] %s -> a crapat: %s" % (nr, desc, e)) print("\n" + "-" * 74) print("Gata. %d exemple rulate, %d cu probleme." % (len(cerute), esecuri)) return 1 if esecuri else 0 if __name__ == "__main__": sys.exit(main())