openapi: 3.0.3
info:
  title: Trynx ERP API
  version: '1.0'
  description: |
    REST API for Trynx ERP system.

    ## Co tahle specifikace pokrývá

    **Popisuje novější JSON vrstvu API, ne celé API.** Část endpointů, které živí
    mobilní aplikace, tu chybí — jde o starší XML cestu (`BaseAPI::actionSet()` /
    `actionGet()`), která vrací XML místo JSON a nikdy sem dopsaná nebyla.
    Z 24 endpointů, které podle produkčního logu mobilní aplikace za srpen a září
    2026 skutečně volaly, jsou tu popsané **tři**. Chybí mimo jiné celý
    `/transport/*` (dvanáct endpointů), `/tasks/get-fund`, `/tasks/task-insert`,
    `/store/get-quantity`, čtyři přehledy z `/homepage/*`, `/helpdesk/set`
    a `/partners/set-worker-by-position`.

    Nedoplňují se zde vědomě: jejich jediní konzumenti jsou mobilní aplikace, jejichž
    autoři je znají, a pro integraci s externími nástroji se nepoužívají. Pokud
    hledáš endpoint, který tu není, zdrojem pravdy je kód v `app/APIModule/presenters/`.

    ## Authentication

    Tři způsoby, všechny se posílají **v těle POST požadavku** (fungují i jako query
    parametr, ale tam hodnota skončí v historii prohlížeče, v logu webserveru
    a u proxy):

    - **`bearer_token`** — session token z `POST /api/homepage/login`. Není bezstavový,
      klient musí držet cookies. Platnost 120 minut nečinnosti.
    - **`sync_token`** — dlouhoživotný firemní token, načítaný z QR kódu v Trynxu.
      Neurčuje uživatele, jen firmu, takže se u něj nevyhodnocují per-uživatelská
      práva ani omezení na úrovni záznamu. Dosáhne dál, než je tu popsané —
      kromě `/tasks/*`, `/invoicearrived/*` a `/partners/*` také na `/transport/*`,
      `/store/*`, `/homepage/*`, `/helpdesk/*`, `/cash/*`, `/notifications/*`
      a `/devices/*`, protože sync token obsluhují tři různé autentizační cesty.
    - **`api_token`** — integrační token pro externí reporting a BI nástroje, spravovaný
      v nastavení firmy. Jen ke čtení a jen u modulů faktur vydaných, přijatých
      a pokladny; jinde vrací 403. Podrobnosti u schématu `apiToken` níž.

    Neplatný token vrací 401.

    ## Request format
    All endpoints use **POST** method. Parameters are sent as POST body with
    `bearer_token` and optionally `dataJSON` (JSON-encoded string with request data).

servers:
  - url: /api
    description: API endpoint

tags:
  # --- Přihlášení ---
  - name: Authentication
    description: Login and session management
  - name: Mobile App
    description: Konfigurace mobilní aplikace Trynx-Info (viditelnost modulů per uživatel)
  # --- Úkoly ---
  - name: Tasks
    description: Task (úkol) CRUD operations
  - name: Task Statuses
    description: Task status CRUD operations (cl_status where status_use = task)
  - name: Task Categories
    description: Task category CRUD operations (cl_task_category)
  - name: Task Workers
    description: Pracovníci úkolu (cl_task_workers) – evidence práce a měsíční fond
  - name: Task Work Events
    description: |
      Práce helpdesku na úkolu – podřízené záznamy cl_partners_event pod rodičovskou
      helpdesk událostí úkolu (cl_task.cl_partners_event_id). Každá mutace přepočítá
      work_time/date_to/finished rodičovské události ze součtu dokončených podřízených záznamů.
  - name: Task Messages
    description: Chat zprávy úkolu (cl_chat s vazbou cl_task_id)
  - name: Task Files
    description: Přílohy úkolu (cl_files.cl_task_id)
  - name: Task Relations
    description: Související úkoly (cl_task_tasks) – vazby mezi úkoly a kopie úkolu
  - name: Task Forms
    description: Formulář 2 s verzemi (cl_task_forms + cl_task_forms_version)
  - name: Task AI
    description: AI funkce úkolu – shrnutí (ai_summary) a AI chaty navázané na úkol
  - name: Task Lookups
    description: Číselníky a statistiky pro modul úkolů (typy/způsoby událostí, projekty, statistiky partnera)
  # --- Zakázky ---
  - name: Commission
    description: Zakázky (cl_commission) – hlavička CRUD + vnořené položky/úkoly/práce/soubory
  - name: Commission Items
    description: Položky zakázky (cl_commission_items_sel)
  - name: Commission Tasks
    description: Úkoly zakázky (cl_commission_task)
  - name: Commission Work
    description: Práce na zakázce (cl_commission_work)
  - name: Commission Files
    description: Přílohy zakázky (cl_files.cl_commission_id)
  # --- Nabídky ---
  - name: Offer
    description: Nabídky (cl_offer) – hlavička CRUD + vnořené položky/úkoly/práce/soubory
  - name: Offer Items
    description: Položky nabídky (cl_offer_items)
  - name: Offer Tasks
    description: Úkoly nabídky (cl_offer_task)
  - name: Offer Work
    description: Práce na nabídce (cl_offer_work)
  - name: Offer Files
    description: Přílohy nabídky (cl_files.cl_offer_id)
  # --- Faktury přijaté ---
  - name: Invoice Arrived
    description: Přijaté faktury (cl_invoice_arrived) – hlavička CRUD, stav a úhrada
  - name: Invoice Arrived Payments
    description: Úhrady přijaté faktury (cl_invoice_arrived_payments)
  - name: Invoice Arrived Commissions
    description: Rozpuštění nákladu faktury na zakázky (cl_invoice_arrived_commission)
  - name: Invoice Arrived Files
    description: Přílohy přijaté faktury (cl_files.cl_invoice_arrived_id)
  - name: Invoice Arrived Lookups
    description: |
      Číselníky pro formulář přijaté faktury (stavy, formy úhrady, měny, střediska,
      druhy dokladů, sazby DPH)
  # --- Pokladna ---
  - name: Cash
    description: |
      Pokladní doklady (cl_cash) – hlavička CRUD, stav, zůstatky po pokladnách a PDF.
      Autorizace jen bearer_token (sync_token zde neplatí), zápis kontroluje práva proti
      Application:Cash a vrací 403.

      POZOR – bearer_token není bezstavový: server ho porovnává s hodnotou ve své
      session, která se identifikuje cookie (PHPSESSID, _nss). Klient musí cookies
      uchovávat a posílat s každým voláním a přebírat jejich aktualizace (Set-Cookie
      chodí u každé odpovědi). Samotný token na čistém spojení vrátí 401. Platnost
      120 minut se vztahuje na session a prodlužuje se každým voláním.

      dataJSON, pokud je posláno, musí být JSON objekt – jinak 400. Úplně vynechané
      dataJSON znamená "bez parametrů" a je v pořádku.
  - name: Cash Files
    description: Přílohy pokladního dokladu (cl_files.cl_cash_id)
  - name: Cash Lookups
    description: Číselníky pro pokladnu (pokladny, střediska, měny, číselné řady, stavy)
  # --- Faktury vydané ---
  - name: Invoice
    description: |
      Faktury vydané (cl_invoice) – hlavička CRUD, stav, přepočet kurzem, PDF a ISDOC.
      Autorizace jen bearer_token (sync_token zde neplatí), zápis kontroluje práva proti
      Application:Invoice a vrací 403.

      POZOR – bearer_token není bezstavový: server ho porovnává s hodnotou ve své
      session, která se identifikuje cookie (PHPSESSID, _nss). Klient musí cookies
      uchovávat a posílat s každým voláním a přebírat jejich aktualizace (Set-Cookie
      chodí u každé odpovědi). Samotný token na čistém spojení vrátí 401. Platnost
      120 minut se vztahuje na session a prodlužuje se každým voláním.

      dataJSON, pokud je posláno, musí být JSON objekt – jinak 400. Úplně vynechané
      dataJSON znamená "bez parametrů" a je v pořádku.

      Nad rámec firmy platí stejná omezení jako ve webu: pobočka uživatele (má-li ji
      vyplněnou) a role „Jen vlastní záznamy" – uplatňují se shodně u všech akcí
      modulu (hlavička, položky, úhrady, přílohy).
  - name: Invoice Items
    description: |
      Položky faktury – prodejní (cl_invoice_items) a vratné (cl_invoice_items_back).
      Uložení ceníkové položky při zapnutém invoice_to_store zakládá/aktualizuje
      skladový doklad (výdejku pro prodej, příjemku pro vratky) jako VEDLEJŠÍ EFEKT –
      appka o něj nežádá samostatně, endpoint pro založení skladového dokladu
      neexistuje. Validace rozlišuje kind="error" (položka se neuloží, HTTP 409) a
      kind="warning" (položka se uloží, hláška v poli warnings u HTTP 200).
  - name: Invoice Payments
    description: |
      Úhrady faktury (cl_invoice_payments) – hotovost, banka, čerpání zálohy, úhrada
      celé částky najednou. Na rozdíl od ostatních zápisových akcí modulu úhrady
      nekontrolují zámek/EET/uzávěrku DPH (měkký zámek – shodně s webem). Hotovostní
      úhrada zakládá pokladní doklad jako vedlejší efekt (pole cash_document v odpovědi).
  - name: Invoice Files
    description: Přílohy faktury (cl_files.cl_invoice_id)
  - name: Invoice Lookups
    description: |
      Číselníky pro formulář faktury vydané (stavy, typy dokladu, typy úhrady, měny,
      střediska, sazby DPH, číselné řady, bankovní účty, sklady, otevřené zakázky).
  # --- Partneři ---
  - name: Partners
    description: Adresář partnerů (cl_partners_book) – CRUD + vnořené kontakty/pobočky
  - name: Partner Contacts
    description: Kontaktní osoby partnera (cl_partners_book_workers)
  - name: Partner Branches
    description: Pobočky partnera (cl_partners_branch)
  # --- Znalostní databáze ---
  - name: KDB Articles
    description: Knowledge Base article CRUD operations
  - name: KDB Categories
    description: Knowledge Base category listing
  - name: KDB Files
    description: Přílohy KDB článků (cl_files.cl_kdb_id)
  # --- AI chat ---
  - name: AI Chat Embed
    description: |
      Server-to-server endpoint for embedding the Trynx AI chat into third-party desktop
      applications via WebView2. Authenticated with company `sync_token` (not `bearer_token`).
      Returns a one-shot URL with a session token that the host application loads into WebView2.
  # --- Sklad a ceník (BI) ---
  - name: Reporting
    description: Čtecí výpisy pro BI nástroje (sklad, ceník). Jen bearer_token nebo api_token, sync_token ne.
  # --- Mobilní pokladna: párování zařízení ---
  - name: Pos Devices
    description: |
      Párování pokladního zařízení (appka) párovacím kódem vygenerovaným v ERP a token
      zařízení pro následné volání. Bez uživatelského přihlášení - autorizace je párovací
      kód (`register-device`) nebo token zařízení (`unregister-device`, `deviceToken`).
  - name: Pos Sale
    description: |
      Prodej z mobilní pokladny (krok 3c) - `POST /api/sale/create`, autorizace jen
      tokenem zařízení (`deviceToken`). Obsluhu nese `cl_users_id` v těle, ověřuje ji
      `PosCashier`. Server prodej nikdy neodmítá kvůli skladu ani rozdílu součtů -
      obojí se vrátí jako varování v odpovědi. Vklad, výběr a rozdíl z uzávěrky jdou
      přes `/cash/create` (tag `Cash`), který token zařízení přijme taky.

  # --- Čtecí API modulů (bearer_token nebo api_token, sync_token ne) ---
  - name: COCCZ
    description: Exporty prodejů pro Coca-Cola HBC (jen čtení). Právo read k modulu Coca-Cola.
  - name: ORO
    description: Oznámení ORO pro celní správu (jen čtení). Právo read k modulu ORO.
  - name: Transports
    description: |
      Jízdy Dopravy (jen čtení, `/api/transports/*`). Nezaměňovat s `/api/transport/*` – to je
      API mobilní aplikace Doprava (sync_token), které tu dokumentované není.
  - name: Deliveries
    description: |
      Dodávky od dodavatelů (jen čtení, `/api/deliveries/*`). Nezaměňovat s `/api/delivery/*` –
      to je API mobilní aplikace Expedice.
  - name: Delivery Notes In
    description: Dodací listy přijaté (jen čtení, `/api/deliverynotein/*`).
  - name: Sales
    description: |
      Prodejky Prodejny (jen čtení, `/api/sales/*`). Zakládání prodejek z pokladny je
      `/api/sale/*` (tag Pos Sale, token zařízení).
  - name: Purchase Orders
    description: |
      Objednávky u dodavatelů (jen čtení, `/api/purchaseorders/*`). Nezaměňovat s legacy
      `/api/orders/*` (sync_token).

components:
  securitySchemes:
    bearerToken:
      type: apiKey
      in: query
      name: bearer_token
      description: |
        Session token z /api/homepage/login.

        **Posílá se v těle POST požadavku** (`application/x-www-form-urlencoded`),
        což je doporučená cesta; funguje i jako query parametr. `in: query` níž
        odráží jen tu druhou možnost - OpenAPI neumí vyjádřit apiKey v těle
        formuláře, jiná hodnota než query/header/cookie tam nejde zapsat.

        Pozor, jméno schématu mate: jde o `type: apiKey`, ne o HTTP bearer.
        Nepřejmenovává se, aby se nerozbili už vygenerovaní klienti.

        Na rozdíl od ostatních dvou tokenů **není bezstavový** - server ho
        porovnává s hodnotou v PHP session, takže klient musí držet cookies
        (`PHPSESSID`, `_nss`). Platnost 120 minut nečinnosti, každé volání ji
        posouvá.
    syncToken:
      type: apiKey
      in: query
      name: sync_token
      description: |
        Per-company integration token stored in `cl_company.sync_token`. Used for
        server-to-server B2B integrations (no user login required). Identifies the Trynx
        tenant on whose behalf the call is made.

        **Posílá se v těle POST požadavku** (`application/x-www-form-urlencoded`);
        funguje i jako query parametr. `in: query` odráží jen tu druhou možnost,
        viz poznámka u `bearerToken`.

        Neurčuje uživatele, jen firmu - per-uživatelská práva ani omezení na
        úrovni záznamu se u něj proto nevyhodnocují a platí plný firemní rozsah.
    apiToken:
      type: apiKey
      in: query
      name: api_token
      description: |
        Dlouhodobý integrační token pro externí reporting a BI nástroje, spravovaný
        v nastavení firmy.

        **Posílá se v těle POST požadavku** (`application/x-www-form-urlencoded`).
        Funguje i jako query parametr, ale tam se hodnota propíše do historie
        prohlížeče, do logu webserveru a k proxy - pro dlouhodobé tajemství to
        není vhodné. `in: query` odráží jen tu druhou možnost, viz poznámka
        u `bearerToken`.

        Podporovaný je zatím jen u modulů vystavených faktur
        (Invoice), přijatých faktur (Invoicearrived) a pokladny (Cash) – to jsou
        jediné presentery, které umí vyhodnotit uživatelská práva technického
        uživatele za tokenem. U ostatních modulů token vrátí 403 (`type`
        `https://example.com/probs/token-scope`).

        Token jedná za technického uživatele: platí práva na modul podle jeho role
        a omezení na firmu vždy; omezení na pobočku platí u Invoice a Cash (podle
        pobočky vyplněné u technického uživatele), Invoicearrived podle pobočky
        nefiltruje. Nastavení „jen vlastní záznamy" se vyhodnocuje stejně jako
        u přihlášeného uživatele.

        Zápis je dnes vždy zakázaný – nastavení firmy umí založit jen token
        s příznakem readonly a Base::readOnlyBlocked() ho vynucuje nezávisle na
        roli technického účtu (dvě nezávislé brány). Pokus o zápis proto vždy
        skončí 403, ale tvar odpovědi se liší podle toho, která brána zabrala
        dřív – ověřeno naživo, ne odvozeno:

        - technický účet má `readonly_access`, takže kontrola práv zamítne zápis
          ještě před prvním dotazem do databáze a vrátí
          `{"status":"error","message":"K této akci nemáte oprávnění"}`;
          tohle je odpověď, kterou dostane každý token vydaný nastavením firmy;
        - druhá brána (Base::readOnlyBlocked()) se projeví jen tehdy, když by
          kontrola práv zápis pustila, a vrací 403 v podobě problem+json
          s `type` `https://example.com/probs/read-only`.

        Posílá se jako parametr `api_token` – funguje v těle POST požadavku i jako GET
        parametr, ale doporučená cesta je POST: v GET by hodnota skončila v historii
        prohlížeče, v logu webserveru a u proxy.

        Limit 120 požadavků za minutu na token; při překročení odpověď 429 s hlavičkou
        `Retry-After`. Neplatný nebo zamítnutý token vrací 401 (`type`
        `https://example.com/probs/invalid-api-token`), překročený limit 429 (`type`
        `https://example.com/probs/rate-limit`).
    deviceToken:
      type: apiKey
      in: query
      name: device_token
      description: |
        Token pokladního zařízení, vydaný `/pos/register-device` (pole `device_token`
        v odpovědi). Nenese uživatele, jen firmu a konkrétní spárované zařízení -
        identifikuje appku, ne přihlášeného člověka.

        Neprůhledný řetězec (nyní 51 znaků, začíná `trx_pos_`, nejvýš 64 znaků) - appka jeho
        délku ani tvar nevaliduje, jen ho bere přesně tak, jak přišel v odpovědi
        `register-device`, a posílá zpátky beze změny.

        **Posílá se v těle POST požadavku** (`application/x-www-form-urlencoded`).
        `in: query` odráží jen druhou technickou možnost - OpenAPI neumí vyjádřit
        apiKey v těle formuláře, viz stejná poznámka u `bearerToken`.

        Je bezstavový (na rozdíl od `bearerToken`), platí dokud zařízení někdo
        neodvolá v ERP (`Prodej → Pokladní zařízení`) nebo appka nezavolá
        `/pos/unregister-device`. Neplatný, neznámý nebo odvolaný token vrací 401
        (`type` `https://example.com/probs/invalid-device-token`).

  schemas:
    KdbArticleList:
      type: object
      properties:
        id:
          type: integer
          example: 42
        kdb_number:
          type: string
          example: 'KB-0042'
        title:
          type: string
          example: 'How to configure invoices'
        ai_summary:
          type: string
          nullable: true
          example: 'Article about invoice configuration settings'
        category_id:
          type: integer
          example: 5
        category_name:
          type: string
          nullable: true
          example: 'Invoicing'
        created:
          type: string
          nullable: true
          example: '2025-01-15 10:30:00'
        create_by:
          type: string
          example: 'Admin'
        changed:
          type: string
          nullable: true
          example: '2025-02-01 14:00:00'
        change_by:
          type: string
          nullable: true
          example: 'Editor'

    KdbArticleDetail:
      allOf:
        - $ref: '#/components/schemas/KdbArticleList'
        - type: object
          properties:
            description:
              type: string
              description: Full article content (HTML or Markdown)
              example: '<p>Step by step guide for invoice configuration</p>'
            category_public:
              type: boolean
              example: true
            category_access_key:
              type: string
              nullable: true
              description: Access key for public sharing URL
              example: 'a1b2c3d4e5f6...'
            category_plain_text:
              type: boolean
              description: If true, content is Markdown instead of HTML
              example: false

    KdbCategory:
      type: object
      properties:
        id:
          type: integer
          example: 1
        name:
          type: string
          example: 'General'
        parent_id:
          type: integer
          nullable: true
          example: null
        public:
          type: boolean
          example: false
        plain_text:
          type: boolean
          example: false
        children:
          type: array
          items:
            $ref: '#/components/schemas/KdbCategory'

    TaskListItem:
      type: object
      properties:
        id:
          type: integer
          example: 123
        task_number:
          type: string
          example: 'T-0123'
        task_date:
          type: string
          nullable: true
          example: '2026-04-04'
        description:
          type: string
          example: '<p>Task description</p>'
        description2:
          type: string
          nullable: true
          example: '<p>Developer notes</p>'
        ai_summary:
          type: string
          nullable: true
          example: 'Summary of the task'
        version:
          type: string
          nullable: true
          example: '2026V793'
        priority:
          type: string
          nullable: true
          example: '1'
        finished:
          type: boolean
          example: false
        checked:
          type: boolean
          example: false
        payment:
          type: integer
          nullable: true
          description: '0 = unpaid, 1 = paid'
          example: 0
        invoice:
          type: boolean
          example: false
        use_fund:
          type: boolean
          example: true
        time_fund:
          type: number
          nullable: true
          example: 2.5
        target_date:
          type: string
          nullable: true
          example: '2026-04-15'
        end_date:
          type: string
          nullable: true
          example: '2026-04-10'
        date_end2:
          type: string
          nullable: true
          description: Development end date
          example: '2026-04-08'
        date_end3:
          type: string
          nullable: true
          description: Testing end date
          example: '2026-04-09'
        date_check:
          type: string
          nullable: true
          description: Next inspection date
          example: '2026-04-20'
        cl_partners_book_id:
          type: integer
          nullable: true
          example: 42
        partner_name:
          type: string
          nullable: true
          example: 'ACME s.r.o.'
        cl_partners_branch_id:
          type: integer
          nullable: true
        cl_partners_book_workers_id:
          type: integer
          nullable: true
        cl_project_id:
          type: integer
          nullable: true
        cl_task_category_id:
          type: integer
          nullable: true
          example: 3
        category_name:
          type: string
          nullable: true
          example: 'Bug'
        cl_status_id:
          type: integer
          nullable: true
          example: 5
        status_name:
          type: string
          nullable: true
          example: 'In progress'
        cl_users_id:
          type: integer
          nullable: true
          description: Main worker
        user_name:
          type: string
          nullable: true
          example: 'Jan Novák'
        cl_users2_id:
          type: integer
          nullable: true
          description: Controller
        cl_users3_id:
          type: integer
          nullable: true
          description: Communicator
        created:
          type: string
          nullable: true
          example: '2026-04-04 10:30:00'
        create_by:
          type: string
          example: 'Admin'
        changed:
          type: string
          nullable: true
          example: '2026-04-04 14:00:00'
        change_by:
          type: string
          nullable: true
          example: 'API'
        cust_descr1_sm:
          type: string
          description: 'Custom text field 1 (short). Fields cust_descr1_sm … cust_descr6_sm follow the same shape.'
          example: 'custom value'
        cust_descr1_bg:
          type: string
          description: 'Custom textarea 1. Also cust_descr2_bg.'
        cust_num1:
          type: number
          description: 'Custom number field 1. Fields cust_num1 … cust_num4 follow the same shape.'
          example: 42.5
        cust_option1:
          type: string
          description: 'Custom select field (value from configured option list).'
        unread_count:
          type: integer
          description: Počet nepřečtených cizích zpráv pro uživatele z unread_user_id (jen když byl parametr poslán)
          example: 3
        workers:
          type: array
          description: 'Worker plan (subset of TaskWorker) so the task card can render the plan without an extra get-workers request; ordered by work_start.'
          items:
            type: object
            properties:
              cl_users_id:
                type: integer
                example: 17
              user_name:
                type: string
                nullable: true
                example: 'Jan Novák'
              work_start:
                type: string
                nullable: true
                example: '2026-07-08 08:00:00'
              work_end:
                type: string
                nullable: true
                example: '2026-07-08 12:00:00'
              work_time:
                type: number
                description: Worked time in hours
                example: 2.5
              work_finished:
                type: boolean
                example: false

    TaskStatus:
      type: object
      properties:
        id:
          type: integer
          example: 5
        status_name:
          type: string
          example: 'In progress'
        description:
          type: string
          example: 'Task is being worked on'
        item_order:
          type: integer
          example: 3
        color_hex:
          type: string
          example: '#FF9900'
        color_ink_hex:
          type: string
          nullable: true
          example: '#FFFFFF'

    TaskCategory:
      type: object
      properties:
        id:
          type: integer
          example: 3
        label:
          type: string
          example: 'Bug'

    TaskWorker:
      type: object
      properties:
        id:
          type: integer
          example: 456
        cl_task_id:
          type: integer
          example: 123
        cl_users_id:
          type: integer
          example: 7
        user_name:
          type: string
          nullable: true
          example: 'Jan Novák'
        final_email:
          type: string
          description: Notification e-mail, auto-filled from the user account when empty
          example: 'jan.novak@firma.cz'
        work_start:
          type: string
          nullable: true
          example: '2026-04-04 08:00:00'
        work_end:
          type: string
          nullable: true
          example: '2026-04-04 12:00:00'
        work_time:
          type: number
          description: Worked time in hours
          example: 2.5
        work_summary:
          type: string
          nullable: true
          example: 'Implementace exportu'
        work_description:
          type: string
          nullable: true
        work_location:
          type: string
          nullable: true
        work_finished:
          type: boolean
          example: false
        google_event_id:
          type: string
          nullable: true
          description: Linked Google Calendar event (read-only, managed by the UI)
        created:
          type: string
          nullable: true
        create_by:
          type: string
        changed:
          type: string
          nullable: true
        change_by:
          type: string
          nullable: true

    TaskWorkEvent:
      type: object
      properties:
        id:
          type: integer
          example: 13374
        cl_partners_event_id:
          type: integer
          description: Parent helpdesk event ID (cl_task.cl_partners_event_id)
          example: 11754
        date:
          type: string
          nullable: true
          example: '2026-04-04 09:00:00'
        date_to:
          type: string
          nullable: true
        cl_users_id:
          type: integer
          nullable: true
        user_name:
          type: string
          nullable: true
          example: 'Jan Novák'
        cl_partners_event_type_id:
          type: integer
          nullable: true
        type_name:
          type: string
          nullable: true
          example: 'Technická podpora'
        cl_partners_event_method_id:
          type: integer
          nullable: true
        method_name:
          type: string
          nullable: true
          example: 'Telefonicky'
        work_time_hours:
          type: integer
          example: 1
        work_time_minutes:
          type: integer
          example: 15
        work_time:
          type: number
          description: Total time in minutes (hours*60 + minutes)
          example: 75
        work_label:
          type: string
        description:
          type: string
        description_original:
          type: string
        add_text:
          type: string
          description: Material and other costs
        public:
          type: boolean
          description: Visible to the customer
          example: false
        finished:
          type: boolean
          example: true
        created:
          type: string
          nullable: true
        create_by:
          type: string
        changed:
          type: string
          nullable: true
        change_by:
          type: string
          nullable: true

    TaskFile:
      type: object
      properties:
        id:
          type: integer
          example: 93000005
        cl_task_id:
          type: integer
          example: 123
        file_name:
          type: string
          description: Stored file name (may differ from the uploaded one on collision)
          example: 'zadani.pdf'
        label_name:
          type: string
          example: 'zadani.pdf'
        mime_type:
          type: string
          example: 'application/pdf'
        file_size:
          type: integer
          description: Size in bytes
          example: 52431
        description:
          type: string
        created:
          type: string
          nullable: true
        create_by:
          type: string

    TaskRelatedTask:
      type: object
      properties:
        link_id:
          type: integer
          description: cl_task_tasks row ID (use for unlink-task)
          example: 128
        cl_task_id:
          type: integer
          description: Linked task ID
          example: 456
        task_number:
          type: string
          example: 'T0407-26'
        task_date:
          type: string
          nullable: true
          example: '2026-04-04'
        finished:
          type: boolean
        partner_name:
          type: string
          nullable: true
          example: 'ACME s.r.o.'
        cl_status_id:
          type: integer
          nullable: true
        status_name:
          type: string
          nullable: true
        ai_summary:
          type: string
          nullable: true

    TaskFormsVersion:
      type: object
      properties:
        id:
          type: integer
          example: 136
        version_number:
          type: integer
          example: 1
        label:
          type: string
          nullable: true
          example: 'Nabídka v1'
        fields_count:
          type: integer
          example: 11
        created:
          type: string
          nullable: true
        create_by:
          type: string

    TaskFormField:
      type: object
      properties:
        id:
          type: integer
          example: 1532
        name:
          type: string
          example: 'Název požadavku'
        item_order:
          type: integer
          example: 1
        type:
          type: string
          enum: [text, number, formated_text, date, select, calculated]
          description: Field type derived from en_val_* flags; calculated fields are read-only
        value:
          nullable: true
          description: 'Current value: string, number, YYYY-MM-DD date or select option ID (null for calculated fields)'

    TaskAiChat:
      type: object
      properties:
        id:
          type: integer
          example: 300
        chat_topic:
          type: string
          example: 'Export CSV'
        assistant_name:
          type: string
          nullable: true
          example: 'Znalostní báze'
        feedback_rating:
          type: integer
          nullable: true
        created:
          type: string
          nullable: true
        create_by:
          type: string

    SuccessResponse:
      type: object
      properties:
        status:
          type: string
          enum: [ok]

    ErrorResponse:
      type: object
      properties:
        status:
          type: string
          enum: [error]
        message:
          type: string
          example: 'Article not found'

    PosProblem:
      type: object
      description: |
        Chybová odpověď pokladního zařízení (RFC 7807 problem+json), ne standardní
        `ErrorResponse` - zavedeno endpointy `/pos/*` a sdílené i `/sale/create`,
        `/sale/create-correction` a `/cash/create` s `device_token`
        (`PosProblemTrait::sendProblem()`). `missing` je jen u `eet_not_configured` a `number_series_missing`
        (`/pos/register-device`), `item_index` jen u chyb na konkrétní položce
        `/sale/create` a `/sale/create-correction`.
      properties:
        type:
          type: string
          format: uri
          example: 'https://example.com/probs/invalid-pairing-code'
        title:
          type: string
          example: Unauthorized
        status:
          type: integer
          example: 401
        detail:
          type: string
          example: 'Párovací kód je neplatný, použitý nebo prošlý.'
        instance:
          type: string
          example: '/api/pos/register-device'
        error:
          type: string
          enum:
            - invalid_input
            - invalid_pairing_code
            - module_not_licensed
            - eet_not_configured
            - number_series_missing
            - no_vs_prefix
            - pairing_conflict
            - server_error
            - invalid_device_token
            - invalid_var_symb
            - invalid_vat_rate
            - invalid_user
            - license_ended
            - request_key_conflict
            - forbidden
            - invalid_sale
            - already_cancelled
            - quantity_exceeded
          description: |
            `invalid_var_symb`, `invalid_vat_rate`, `invalid_user` a
            `request_key_conflict` vrací jen `/sale/create`, `/sale/create-correction`
            a `/cash/create` s `device_token`; `forbidden` je obecný mapovaný kód
            `/cash/create` v režimu zařízení pro ostatní 403
            (`CashPresenter::deviceProblem()`) - v praxi ho `create` samo nevyvolá,
            `PosCashier` hlásí přímo `invalid_user`. `license_ended` (403) vrací
            `/sale/create`, `/sale/create-correction` a `/cash/create` s `device_token`:
            obsluha bez platné licence Prodejny a doklad vznikl až po konci její
            platnosti (`PosCashier::guardEnded()`). `invalid_sale`,
            `already_cancelled` a `quantity_exceeded` vrací jen
            `/sale/create-correction` (`CorrectionCheck::decide()`).
        missing:
          type: array
          nullable: true
          description: |
            U `eet_not_configured` podmnožina `[eic, id_jednotky, id_pokl, certificate]`,
            u `number_series_missing` podmnožina `[sale_series, correction_series]`.
          items: { type: string, enum: [eic, id_jednotky, id_pokl, certificate, sale_series, correction_series] }
        item_index:
          type: integer
          nullable: true
          description: |
            U `/sale/create` - index položky v poslaném poli `items` (od 0).

            U `/sale/create-correction` má `item_index` **dva různé významy** podle
            chyby:
            - chyby tvaru vstupu (`invalid_input` - položka není objekt, chybí nebo
              je neplatný `item_index`/`quantity`): pořadí **v poslaném poli**
              `items` požadavku (od 0), stejně jako u `/sale/create`;
            - `invalid_input` s neznámým `item_index` nebo s řádkem se záporným či
              nulovým množstvím a `quantity_exceeded` (`CorrectionCheck::decide()`): hodnota `item_index`, tedy pořadí řádku
              (`item_order`) **v původní prodejce**, ne pozice v poslaném poli.
          example: 1

    PosCompanyLogo:
      type: object
      nullable: true
      description: |
        Logo firmy pro obrazovku obsluhy (zadání rev. 6, oddíl 17, E6). `null` = firma
        logo nemá. Zmenšenina nejvýš 512 px na delší straně (nikdy se nezvětšuje),
        vždy JPEG. `hash` je sha256 **odeslaných bajtů** (tedy zmenšeniny), appka podle
        něj stejné logo znovu neukládá. Změna loga v ERP posune `cl_company.changed`,
        takže vynutí plnou synchronizaci katalogu.
      properties:
        hash: { type: string, example: '9f2c4a1b…(64 hex)' }
        mime: { type: string, example: 'image/jpeg' }
        data_base64: { type: string, example: '/9j/4AAQSkZJRg…' }
    PosCompany:
      type: object
      description: |
        Údaje firmy pro účtenku a obrazovku obsluhy. V `register-device` vždy
        s `logo`; na první stránce `/pos/get-catalog` vždy bez ohledu na `full`,
        ale `logo` jen při `full: true` (v deltě klíč chybí = beze změny).
      properties:
        name: { type: string, example: 'Trynx s.r.o.' }
        ico: { type: string, example: '12345678' }
        dic: { type: string, example: 'CZ12345678' }
        address: { type: string, example: 'Ulice 1, 100 00 Praha' }
        logo:
          $ref: '#/components/schemas/PosCompanyLogo'
    PosDeviceConfig:
      type: object
      description: |
        Konfigurace pokladního zařízení - odpověď `/pos/register-device` (s
        `device_token`) sestavená `PosPairingService::deviceConfig()`.
      properties:
        device_id:
          type: integer
          example: 42
        device_token:
          type: string
          description: 'Jen v odpovědi `register-device` - vydává se jednou, appka ho uloží. Neprůhledný řetězec (nyní 51 znaků, `trx_pos_` + base64url, nejvýš 64 znaků) - délka se nevaliduje.'
          example: 'trx_pos_zkhbcS9j9ZE55dQ9IBp6lH4c9xaxjQlAOLRTjGgzmM4'
        company:
          $ref: '#/components/schemas/PosCompany'
        branch:
          type: object
          nullable: true
          properties:
            id: { type: integer, example: 3 }
            name: { type: string, example: 'Pobočka Praha' }
        storage:
          type: object
          nullable: true
          description: |
            Sklad, ze kterého zařízení vydává: sklad zařízení (jen je-li výslovně
            zvolen v párovacím kódu nebo v Prodejna / Pokladní zařízení), jinak sklad
            pobočky, jinak sklad z Nastavení / Prodejna. Dopočítává se při každém
            dotazu, změna nastavení se tedy projeví bez nového párování.
          properties:
            id: { type: integer, example: 1 }
            name: { type: string, example: 'Hlavní sklad' }
        cash_def:
          type: object
          nullable: true
          description: |
            Pokladna, do které jdou pokladní doklady prodejek zařízení: pokladna
            zařízení (jen je-li výslovně zvolena), jinak pokladna pobočky, jinak
            pokladna z Nastavení / Prodejna, jinak výchozí pokladna měny. Stejná
            kaskáda jako při prodeji (`Sale::updateSaleSum()`); dopočítává se při
            každém dotazu.
          properties:
            id: { type: integer, example: 1 }
            name: { type: string, example: 'Pokladna 1' }
        currency:
          type: object
          nullable: true
          properties:
            id: { type: integer, example: 1 }
            code: { type: string, example: CZK }
            decimal_places:
              type: integer
              description: Počet desetinných míst pro ceny.
              example: 2
            decimal_places_cash:
              type: integer
              description: Počet desetinných míst pro zaokrouhlení hotovosti.
              example: 0
        price_e_type:
          type: integer
          description: Typ ceny firmy (`cl_company.price_e_type`).
          example: 1
        vat_payer:
          type: boolean
          description: Firma je plátce DPH (`cl_company.platce_dph = 1`).
        offline:
          type: object
          description: Limity pro appku bez spojení se serverem.
          properties:
            warn_hours: { type: integer, example: 24 }
            block_hours: { type: integer, example: 40 }
        qr_payment:
          type: object
          nullable: true
          description: 'null, pokud firma nemá výchozí bankovní účet v měně pokladny.'
          properties:
            iban: { type: string, example: 'CZ6508000000192000145399' }
            vs_prefix: { type: string, example: '901' }
            vs_next:
              type: integer
              description: Další volný variabilní symbol s tímto prefixem.
              example: 9010001
        eet:
          type: object
          properties:
            active:
              type: boolean
              description: Firma/pobočka má aktivní podpisová data EET 2.0.
            test:
              type: boolean
              description: Testovací režim EET.
            eic:
              type: string
              nullable: true
            id_jednotky:
              type: integer
              nullable: true
            id_pokl:
              type: string
              nullable: true

    PosCatalog:
      type: object
      description: |
        Katalog pokladny - odpověď `/pos/get-catalog` (`PosCatalogService::catalog()`).

        Malé sekce (`pricelist_groups`, `shorts`, `vat_rates`, `payment_types`,
        `users`), údaje firmy `company` (s `logo` jen při `full: true`), konfigurace
        (`vat_payer`, `currency`, `price_e_type`, `offline`,
        `qr_payment`, `eet`), `deny_negative_stock`/`warn_negative_stock` (nastavení
        skladu zařízení na nejvyšší úrovni, ne u položek) a obsah `stock` chodí
        **jen na první stránce** (požadavek bez `after_id`, nebo s `after_id: 0`).
        Další stránky nesou `full`, `server_time`, `pricelist`, `next_after_id`,
        `deleted_ids` a `stock: []`. Malými sekcemi appka nahradí celý dosavadní obsah - `deleted_ids`
        se u nich neposílá, protože uživatel může z katalogu vypadnout změnou role
        nebo odebráním z firmy, aniž by se to projevilo na `changed`. Naproti tomu
        `deleted_ids.pricelist` chodí na **každé** stránce delty, ne jen první -
        položka může přestat vyhovovat filtru (deaktivace, přesun do jiné skupiny)
        i na pozdější stránce.

        `full` se může v sérii stránek změnit (konfigurace se změnila mezi
        stránkami): vrátí-li pozdější stránka delty `full: true`, appka sérii
        zahodí a začne znovu od první stránky bez `changed_since`.

        Položky v cizí měně jsou přepočtené pevným kurzem na měnu firmy (viz
        `PosCatalogItem`); položky v měně s denním kurzem (`fix_rate` 0) v katalogu
        nejsou - v deltě chodí v `deleted_ids.pricelist`, nejsou ani ve `stock`.

        Známá omezení: sekce `stock` v deltě **není stránkovaná** - po delší
        odpojené appce může být velká. Smazaný skladový pohyb (například smazaný
        přijatý dodací list) se v deltě `stock` neprojeví; hodnota se opraví až
        při dalším pohybu té položky, nebo při plné synchronizaci (vynechané
        `changed_since`). Appka by proto měla dělat plnou synchronizaci
        pravidelně (např. jednou denně), nejen po `full: true` z delty.
      properties:
        full:
          type: boolean
          description: 'true = appka nahradí celý katalog (plná synchronizace), false = delta.'
        server_time:
          type: string
          example: '2026-09-28 12:00:00'
          description: Čas serveru při zpracování - appka ho z PRVNÍHO volání série (první stránky) použije jako příští `changed_since`.
        pricelist:
          type: array
          description: Stránka položek ceníku (`id > after_id`, `ORDER BY id`, `LIMIT limit`).
          items: { $ref: '#/components/schemas/PosCatalogItem' }
        next_after_id:
          type: integer
          nullable: true
          description: id pro pokračování stránkování ceníku, `null` = konec. Prázdné `pricelist` konec neznamená.
        deleted_ids:
          type: object
          description: 'Jen v deltě (`full: false`), jinak `{"pricelist": []}`. Na KAŽDÉ stránce delty, ne jen na první.'
          properties:
            pricelist:
              type: array
              items: { type: integer }
              description: 'id smazaných (`cl_erased_sync`, jen na první stránce) a deaktivovaných/přesunutých mimo skupinu pobočky položek a položek v měně bez pevného kurzu (na dané stránce).'
        stock:
          type: array
          description: 'Jen v deltě a jen na první stránce (bez `after_id`), jinak `[]`. Není stránkovaná. Položky s pohybem na skladu zařízení od `changed_since`; platí-li položka zároveň v `pricelist` téže odpovědi, rozhoduje hodnota tam.'
          items:
            type: object
            properties:
              cl_pricelist_id: { type: integer }
              stock: { type: number, format: float }
        company:
          allOf:
            - $ref: '#/components/schemas/PosCompany'
          description: 'Jen na první stránce. Název, IČO, DIČ a adresa vždy (appka převezme změnu údajů na účtence); `logo` jen při `full: true`, v deltě klíč chybí.'
        deny_negative_stock:
          type: boolean
          nullable: true
          description: Jen na první stránce. `null` bez skladu zařízení.
        warn_negative_stock:
          type: boolean
          nullable: true
          description: Jen na první stránce. `null` bez skladu zařízení.
        pricelist_groups:
          type: array
          description: Jen na první stránce, vždy celé.
          items:
            type: object
            properties:
              id: { type: integer }
              name: { type: string }
              cl_pricelist_group_id: { type: integer, nullable: true, description: Nadřazená skupina. }
        shorts:
          type: array
          description: 'Jen na první stránce, vždy celé. Rychlé volby jako web (Listgrid): s pobočkou jen její, bez náhradních firemních; bez pobočky firemní. `cl_pricelist_id` může odkazovat na položku, která v katalogu není (neaktivní, jiná skupina, vyřazená kvůli měně) - appka takovou volbu ignoruje.'
          items:
            type: object
            properties:
              id: { type: integer }
              name: { type: string }
              color_hex: { type: string, example: '#ff9900' }
              cl_pricelist_id: { type: integer, nullable: true }
              cl_sale_shorts_id: { type: integer, nullable: true }
        vat_rates:
          type: array
          description: Jen na první stránce, vždy celé. Sazby platné dnes (`RatesVatManager::findAllValidStrict()`).
          items:
            type: object
            properties:
              id: { type: integer }
              rates: { type: number, format: float, example: 21 }
              code_name: { type: string }
              description: { type: string }
        payment_types:
          type: array
          description: 'Jen na první stránce, vždy celé. `use_for_sale = 1 AND not_active = 0`.'
          items:
            type: object
            properties:
              id: { type: integer }
              name: { type: string }
              payment_type: { type: integer }
              eet_send: { type: boolean }
        users:
          type: array
          description: |
            Jen na první stránce, vždy celé. Obsluha s neprázdným PIN, právem zápisu
            do Prodejny (`application_sale_write`, chybějící klíč = povoleno,
            `readonly_access` právo nemá) a platnou licencí modulu Prodejna:
            zaplacenou, nebo bez licence jen ve zkušební době firmy. Na tarifu
            zdarma Prodejna není (`Application:Sale` je placený modul,
            `TariffLimits::FREE_MODULES` obsahuje jen fakturaci). Licence se
            jen ověřuje, ne obsazuje - na rozdíl od `UserManager::trfModuleEnable()`
            samotná synchronizace katalogu neobsadí místo v licenci uživateli, který
            ho zatím nemá.
          items:
            type: object
            properties:
              id: { type: integer }
              name: { type: string }
              pin_hash:
                type: string
                description: '`hash_pbkdf2(''sha256'', PIN, pin_salt, 10000)`, hex, 64 znaků. PIN v čistém textu server nikdy nevrací ani nezaloguje.'
              pin_salt:
                type: string
                description: '`hash_hmac(''sha256'', ''pos-pin:'' . userId, token_hash zařízení)`, hex, 64 znaků. Stabilní mezi synchronizacemi, jiný pro každé zařízení a uživatele; po novém spárování (nový token) se změní.'
        vat_payer:
          type: boolean
          description: Jen na první stránce. Firma je plátce DPH.
        currency:
          type: object
          nullable: true
          description: Jen na první stránce.
          properties:
            id: { type: integer }
            code: { type: string, example: CZK }
            decimal_places: { type: integer }
            decimal_places_cash: { type: integer }
        price_e_type:
          type: integer
          description: Jen na první stránce. Typ ceny firmy (`cl_company.price_e_type`) - určuje, ze kterého pole je počítáno `price_e` položek.
        offline:
          type: object
          description: Jen na první stránce. Limity pro appku bez spojení se serverem.
          properties:
            warn_hours: { type: integer }
            block_hours: { type: integer }
        qr_payment:
          type: object
          nullable: true
          description: Jen na první stránce. `null`, pokud firma nemá výchozí bankovní účet v měně pokladny.
          properties:
            iban: { type: string }
            vs_prefix: { type: string }
            vs_next: { type: integer }
        eet:
          type: object
          description: Jen na první stránce.
          properties:
            active: { type: boolean }
            test: { type: boolean }
            eic: { type: string, nullable: true }
            id_jednotky: { type: integer, nullable: true }
            id_pokl: { type: string, nullable: true }

    PosCatalogItem:
      type: object
      description: Položka ceníku v odpovědi `/pos/get-catalog`.
      properties:
        id: { type: integer }
        identification: { type: string }
        item_label: { type: string }
        ean_code: { type: string }
        search_tag: { type: string }
        cl_pricelist_group_id: { type: integer, nullable: true }
        units: { type: string, description: 'Sloupec `cl_pricelist.unit`.' }
        vat: { type: number, format: float }
        price_e:
          type: number
          format: float
          description: |
            Cena, kterou pokladna účtuje - `price_vat` při `price_e_type = 1`, jinak `price` (podle nastavení firmy).
            Všechny tři ceny jsou v měně firmy: položka v cizí měně je přepočtená jako na webové Prodejně
            (`cena × fix_rate měny položky / fix_rate měny firmy`, zaokrouhleno na desetinná místa měny firmy,
            nejméně 2). Položka v měně bez pevného kurzu (`fix_rate` 0 = denní kurz ČNB) v katalogu není.
        price_e2: { type: number, format: float, description: 'Sloupec `price` (bez DPH), v měně firmy.' }
        price_e2_vat: { type: number, format: float, description: 'Sloupec `price_vat` (s DPH), v měně firmy, informativní.' }
        stock:
          type: number
          format: float
          nullable: true
          description: Orientační stav na skladu zařízení. `null` bez skladu zařízení, `0` bez pohybu.

    PosSaleItem:
      type: object
      description: |
        Jedna položka prodejky v požadavku `POST /sale/create` (`PosSaleInput::parse()`).
        Server řádek dopočítá z `price_e` stejným vzorcem jako webová prodejna
        (`SaleLine::compute()`) - `price_e2_vat` z požadavku je jen informativní a
        podle ceníku se ceny nepřepočítávají.
      required: [quantity, price_e, vat]
      properties:
        cl_pricelist_id:
          type: integer
          nullable: true
          description: |
            Id položky ceníku firmy. Neexistuje-li ve firmě (smazaná/přesunutá mezi
            synchronizacemi), řádek se **neodmítá** - uloží se jako volná položka bez
            skladového výdeje a odpověď nese varování `unknown_item`. `null` vždy
            znamená volnou položku (musí mít `item_label`).
        item_label:
          type: string
          nullable: true
          description: |
            Popis položky. **Povinný jen u položky bez `cl_pricelist_id`** (`null`) -
            chybí-li tam, 400 `invalid_input`. U `cl_pricelist_id`, které ve firmě
            neexistuje, je nepovinný: poslaný popisek se použije pro uloženou volnou
            položku (doporučeno ho posílat), jinak „Položka #<id>“. U nalezené
            položky ceníku se ignoruje - jméno se vezme z ceníku.
        quantity:
          type: number
          format: float
          example: 2
          description: |
            U položky s `cl_pricelist_id` musí být >= 0 - záporné množství by se do
            skladu nepropsalo, proto 400 `invalid_input` s `item_index`. Vrácení zboží
            patří do `/sale/create-correction`. Volná položka (`cl_pricelist_id: null`,
            sleva, záloha) záporná být smí.
        price_e:
          type: number
          format: float
          description: |
            Jednotková cena přesně podle `price_e_type` firmy (0 = bez DPH, 1 = s DPH) -
            konečná cena od appky, ceníková `price_s` se dosadí jen jako informace na
            řádek, výpočet z ní nevychází.
          example: 149.5
        vat:
          type: number
          format: float
          description: |
            Sazba DPH řádku. Musí být mezi platnými sazbami k datu `sale_time`
            (`RatesVatManager::findAllValidStrict()`), u neplátce jen `0` - jinak
            400 `invalid_vat_rate` s `item_index`.
          example: 21
        discount:
          type: number
          format: float
          default: 0
          description: Sleva na řádku v procentech. Nepovinné, výchozí 0.

    PosSaleCreate:
      type: object
      description: |
        `dataJSON` požadavku `POST /sale/create` (+ `device_token` v těle POST).
        Validuje a normalizuje `PosSaleInput::parse()` - server prodej nikdy neodmítá
        kvůli skladu ani rozdílu součtů, jen kvůli tvaru vstupu (viz chyby endpointu).
      required: [api_request_key, sale_time, cl_users_id, cl_payment_types_id, items]
      properties:
        api_request_key:
          type: string
          description: |
            Ochrana proti dvojímu založení a dvojí evidenci v EET - 1 až 64 znaků
            z písmen, číslic a `. _ : -`, jinak 400 `invalid_input`. Stejný klíč se
            stejným obsahem požadavku je opakování (200 + `Idempotent-Replayed`),
            se stejným klíčem a jiným obsahem 409 `request_key_conflict`. Na rozdíl
            od `/cash/create` je tu **povinný**.
          example: 'a1b2c3d4-1234-4a5b-8c9d-0e1f2a3b4c5d'
        sale_time:
          type: string
          description: |
            Čas prodeje `Y-m-d H:i:s`, jiný formát nebo víc než 5 minut v budoucnosti
            (čas serveru) -> 400 `invalid_input`. Starý čas (offline prodej) se nikdy
            neodmítá - použije se jako `inv_date`/`vat_date`/`dat_trzby` v EET a
            k němu se vybírají platné sazby DPH.
          example: '2026-09-28 14:32:07'
        cl_users_id:
          type: integer
          description: |
            Obsluha - ověří ji `PosCashier::check()` (člen firmy, aktivní, právo
            zápisu do Prodejny). Licence zápis nezamítá: místo se v transakci
            zápisu obsadí, je-li volné; bez místa v plné licenci se doklad přijme
            s varováním `license_exceeded`, bez platné licence s `license_expired`
            (viz `PosWarning`) - ale jen prodej z doby platnosti licence, po jejím konci
            403 `license_ended`. Neprojde z jiného než licenčního důvodu -> 403 `invalid_user`.
          example: 89
        cl_payment_types_id:
          type: integer
          description: |
            Forma úhrady firmy zařízení. Musí patřit firmě, jinak 400 `invalid_input`;
            **deaktivovaná forma (`not_active = 1`) se přijme** - prodej u pultu už
            proběhl.
          example: 1
        cash_rec:
          type: number
          format: float
          nullable: true
          description: Přijatá hotovost (zaokrouhlená). Nepovinné.
        client_total:
          type: number
          format: float
          nullable: true
          description: |
            Součet spočítaný appkou. Liší-li se od serverového součtu, rozdíl se
            uloží jako `price_correction` a odpověď nese varování `total_mismatch` -
            prodej se kvůli rozdílu neodmítá.
        discount:
          type: number
          format: float
          default: 0
          description: |
            Sleva na celý doklad v **procentech** (sloupec `cl_sale.discount`).
            Sleva pobočky (`cl_company_branch`) se u prodeje z pokladny neuplatní.
        var_symb:
          type: string
          nullable: true
          description: |
            Variabilní symbol pro QR platbu - přesně 10 číslic začínajících
            `vs_prefix` zařízení, jinak 400 `invalid_var_symb`. Jiná prodejka,
            faktura nebo záloha firmy se stejným VS nevede k odmítnutí, jen k
            varování `duplicate_var_symb`.
          example: '9010001235'
        items:
          type: array
          minItems: 1
          description: Neprázdný seznam položek, jinak 400 `invalid_input`. Pořadí v poli určuje `item_order` (0, 1, 2, …).
          items: { $ref: '#/components/schemas/PosSaleItem' }

    PosWarning:
      type: object
      description: |
        Jedno varování v poli `warnings` odpovědi `POST /sale/create` a
        `POST /sale/create-correction` (`PosSaleWarnings`). Server doklad kvůli
        žádnému z nich neodmítá - položky `cl_pricelist_id`, `stock_after`,
        `blocked`, `server_total`, `client_total`, `item_index`, `item_label`,
        `var_symb`, `cl_users_id` a `user_name` jsou u příslušného kódu, jinak chybí.

        **`license_exceeded` ani `license_expired` v odpovědi `POST /cash/create` nikdy nepřijde** -
        tahle akce varování v odpovědi vůbec nevrací (jen řádek dokladu). Zapíše
        se do `cl_pos_sale_warnings` stejně jako u prodeje a storna, a správce ho
        uvidí jen v denním souhrnu (`app:posnotify`, `docs/ODPOVED-backend-pokladna.md`
        oddíl N).
      required: [code, message]
      properties:
        code:
          type: string
          enum:
            - negative_stock
            - blocked_stock
            - total_mismatch
            - no_storage
            - unknown_item
            - duplicate_var_symb
            - eet_pending
            - eet_late
            - no_number_series
            - license_exceeded
            - license_expired
          description: |
            `no_number_series` - pobočka zařízení ani firma nemá číselnou řadu prodejek
            (`sale`): prodej je uložený s číslem `"0"` a **bez pokladního dokladu**;
            správce musí nastavit řadu prodejek pobočce nebo firmě. `eet_late` - tržba
            je po lhůtě pro evidenci (hlásí se u zaevidované i čekající tržby).

            `license_exceeded` - obsluha (`cl_users_id`) nemá místo v licenci
            Prodejny a volné místo v okamžiku zápisu už nebylo (`SaleLicense::claim()`
            = `full`) - doklad se přesto uložil, aby offline prodej u pultu
            neztratil evidenci v EET (spec `2026-09-29-pos-sale-license-seats-design.md`,
            rozhodnutí 4). Vrací ho `POST /sale/create` a `POST /sale/create-correction`
            v `warnings`; `POST /cash/create` ho do odpovědi nedává vůbec (viz výše u
            popisu schématu).

            `license_expired` - obsluha (`cl_users_id`) nemá platnou licenci Prodejny
            z jiného důvodu než plné licence (modul Prodejna v licenci chybí nebo mu
            prošel `exp`; licence prošla / není zaplacená a tarif ani zkušební doba
            Prodejnu nepovolí) - `PosCashier::check()` = `license_expired`. Doklad se
            přesto uložil, protože vznikl (`sale_time`, u `/cash/create` `inv_date`)
            ještě za platnosti licence - doklad po jejím konci dostane 403
            `license_ended` (`PosProblem`); místo v licenci se neobsazuje. Správce musí licenci obnovit nebo
            prodloužit. Vrací ho `POST /sale/create` a `POST /sale/create-correction`;
            u `POST /cash/create` jen v denním souhrnu (jako `license_exceeded`).
        message: { type: string }
        cl_pricelist_id:
          type: integer
          nullable: true
          description: 'U `negative_stock`, `blocked_stock` a `unknown_item`.'
        stock_after:
          type: number
          format: float
          nullable: true
          description: 'U `negative_stock` - stav skladu PO výdeji této položky (může být záporný).'
        blocked:
          type: number
          format: float
          nullable: true
          description: 'U `blocked_stock` - množství blokované pro zakázky.'
        server_total:
          type: number
          format: float
          nullable: true
          description: 'U `total_mismatch` - součet spočítaný serverem.'
        client_total:
          type: number
          format: float
          nullable: true
          description: 'U `total_mismatch` - součet poslaný appkou.'
        item_index:
          type: integer
          nullable: true
          description: 'U `unknown_item` - index položky v poslaném poli `items` (od 0).'
        item_label:
          type: string
          nullable: true
          description: 'U `unknown_item` - název, pod kterým se položka uložila na prodejku.'
        var_symb:
          type: string
          nullable: true
          description: 'U `duplicate_var_symb` - variabilní symbol, který se opakuje.'
        cl_users_id:
          type: integer
          nullable: true
          description: 'U `license_exceeded` a `license_expired` - id obsluhy (`cl_users.id`) bez místa / bez platné licence.'
        user_name:
          type: string
          nullable: true
          description: |
            U `license_exceeded` a `license_expired` - jméno obsluhy v okamžiku zápisu
            (`cl_users.name`), pro denní souhrn. Chybí, když by bylo prázdné
            (`PosSaleWarnings::licenseExceeded()`, `licenseExpired()`).

    PosSaleResult:
      type: object
      description: Odpověď `POST /sale/create` (`data`), sestavená `PosSaleService::response()`.
      properties:
        id: { type: integer, example: 31440 }
        sale_number:
          type: string
          nullable: true
          description: |
            Číslo prodejky z číselné řady **pobočky** zařízení, jinak z výchozí řady
            prodejek firmy (řada uložená u zařízení se pro číslování nepoužívá).
            Nemá-li pobočka ani firma řadu prodejek, je `"0"` a odpověď nese varování
            `no_number_series` (viz známé omezení u `/sale/create`).
          example: 'PR-261980'
        var_symb: { type: string, example: '9010001235' }
        price_e2_vat:
          type: number
          format: float
          description: Celkový součet dokladu s DPH v měně firmy - po případné korekci na `client_total`.
        price_correction:
          type: number
          format: float
          description: Rozdíl mezi serverovým součtem a `client_total` (`0`, pokud se shodují nebo `client_total` nepřišel).
        vat_summary:
          type: array
          description: Rekapitulace DPH podle sazeb hlavičky, jen nenulové základy.
          items:
            type: object
            properties:
              vat: { type: number, format: float, example: 21 }
              base: { type: number, format: float, example: 651.24 }
              vat_amount: { type: number, format: float, example: 136.76 }
        eet:
          type: object
          description: |
            Stav EET po odeslání (čerstvý prodej), nebo dočtený z `cl_eet`
            (opakovaný požadavek).
          properties:
            status:
              type: string
              enum: [registered, pending, not_applicable]
            pok: { type: string, nullable: true }
            error: { type: string, nullable: true }
        warnings:
          type: array
          description: |
            U opakovaného požadavku vždy prázdné - appka si varování z prvního
            zpracování drží sama, server je znovu neposílá (`ODPOVED-backend-pokladna.md` I2).
          items: { $ref: '#/components/schemas/PosWarning' }

    PosSaleCorrectionItem:
      type: object
      description: |
        Jedna vracená položka v požadavku `POST /sale/create-correction`
        (`PosCorrectionInput::parse()`). `item_index` neodkazuje na tuto položku v
        poli `items` požadavku - je to pořadí řádku (`item_order`) v **původní**
        prodejce `/sale/create`, začíná 0. Stejné `item_index` se smí v poli opakovat,
        `quantity` se sečte.
      required: [item_index, quantity]
      properties:
        item_index:
          type: integer
          minimum: 0
          description: 'Pořadí vraceného řádku v původní prodejce (0-based), ne pořadí v tomto poli.'
          example: 0
        quantity:
          type: number
          format: float
          description: Vracené množství, musí být kladné.
          example: 1

    PosSaleCorrection:
      type: object
      description: |
        `dataJSON` požadavku `POST /sale/create-correction` (+ `device_token` v těle
        POST) - storno prodejky odeslané pokladnou (krok 6). Validuje a normalizuje
        `PosCorrectionInput::parse()`.
      required: [api_request_key, cl_sale_id, cl_users_id, sale_time, items]
      properties:
        api_request_key:
          type: string
          description: |
            Stejný tvar a stejná role jako u `/sale/create` (storno je taky řádek
            `cl_sale`, sdílí unikátní index `(cl_company_id, api_request_key)`).
            Klíč použitý dřív pro prodej (nebo jiné storno) a teď pro toto storno
            vrátí 409 `request_key_conflict` (jiný otisk obsahu).
          example: 'b2c3d4e5-2345-4a5b-8c9d-0e1f2a3b4c5e'
        cl_sale_id:
          type: integer
          description: |
            Id stornované prodejky (`cl_sale`). Cizí firma, neexistující, `sale_type = 1`
            (storno storna), prodejka bez čísla nebo bez řádku s kladným množstvím
            -> 409 `invalid_sale`.
          example: 31474
        cl_users_id:
          type: integer
          description: Obsluha - ověří ji `PosCashier::check()` stejně jako `/sale/create`, včetně `license_exceeded` u plné licence a `license_expired` bez platné licence (jen doklad z doby platnosti licence, `inv_date` jako ten den, jinak 403 `license_ended`). Neprojde z jiného než licenčního důvodu -> 403 `invalid_user`.
          example: 89
        sale_time:
          type: string
          description: |
            Čas storna `Y-m-d H:i:s`, stejná pravidla jako `/sale/create`
            (nejvýš 5 minut v budoucnosti). Je to čas EET evidence storna (samostatná
            záporná tržba), ne čas původního prodeje.
          example: '2026-09-28 15:10:00'
        items:
          type: array
          minItems: 1
          description: |
            Neprázdný seznam vracených řádků, jinak 400 `invalid_input`. `item_index`
            mimo rozsah původní prodejky nebo na řádku se záporným/nulovým množstvím
            -> 400 `invalid_input` s `item_index`;
            součet dosud vrácených množství řádku (ze všech storen, i z jiných
            zařízení) a tohoto požadavku nad prodané množství -> 409 `quantity_exceeded`
            s `item_index`.
          items: { $ref: '#/components/schemas/PosSaleCorrectionItem' }
        client_total:
          type: number
          format: float
          nullable: true
          description: |
            Součet spočítaný appkou, musí být `<= 0`, jinak 400 `invalid_input`. Liší
            se od serverového součtu -> `price_correction` a varování `total_mismatch`,
            storno se přesto uloží. **Appka má posílat skutečně vrácenou částku** -
            `0` s nenulovým serverovým součtem se bere jako vyrovnání na `0` (ne
            "appka částku nezná"): serverový součet se přepíše na `0` a u formy úhrady
            hotovost vznikne záporný pokladní doklad na `0 Kč`, ne na skutečně vrácenou
            částku.
        var_symb:
          type: string
          nullable: true
          description: |
            **Nepřijímá se.** Pošle-li appka cokoli, tiše se ignoruje - storno má
            vždy prázdný `var_symb` (stejně jako webové storno), aby nekolidoval s VS
            původní prodejky při párování banky (viz `ODPOVED-backend-pokladna.md` L).

    AiChatEmbedAssistant:
      type: object
      properties:
        id:
          type: integer
          example: 3
        name:
          type: string
          example: 'Znalostní báze'
        service_type:
          type: string
          enum: [help, chat, kdb]
          description: |
            Type of the assistant. `task_analyst` and internal helpers (`summary`,
            `topic`, `kdb_selection`) are filtered out — they are not user-facing.
        model:
          type: string
          description: Underlying LLM model used by the assistant.
          example: 'claude-sonnet-4-20250514'
        kdb_category:
          type: object
          nullable: true
          description: |
            Knowledge base category scope. `null` for non-KDB assistants or KDB assistants
            without a category restriction (entire tenant KDB is searched).
          properties:
            id:
              type: integer
              example: 7
            name:
              type: string
              example: 'Doprava 4K - nápověda'
        kdb_public_domain:
          type: string
          nullable: true
          description: |
            Custom domain used when rewriting public KDB article links inside chat answers.
            `null` means the Trynx host domain is used as fallback.
          example: 'https://kb.firma.cz'
        theme:
          type: object
          description: |
            Lightweight theme preview the host application can use to render its
            assistant picker (primary color and logo). Full theme is applied only inside
            the embed window after `embed_url` is loaded.
          properties:
            primary:
              type: string
              nullable: true
              description: Hex color (`#RRGGBB`) or `null` if not configured.
              example: '#e93b57'
            logo_url:
              type: string
              nullable: true
              format: uri
              example: 'https://kb.firma.cz/logo.png'
        pricing:
          type: object
          description: Token pricing in the assistant's billing currency, per 1 000 000 tokens.
          properties:
            input_per_million:
              type: number
              format: float
              example: 3.0
            output_per_million:
              type: number
              format: float
              example: 15.0

    AiChatEmbedStartRequest:
      type: object
      required: [sync_token, assistant_id, external_user_name, external_user_ico]
      properties:
        sync_token:
          type: string
          description: Trynx tenant integration token from `cl_company.sync_token`.
        assistant_id:
          type: integer
          description: |
            ID of the AI assistant (`cl_ai_assistant.id`) that will handle the chat.
            Must be active and either global (`cl_company_id IS NULL`) or owned by the
            tenant. `task_analyst` service type is not supported in embed mode.
          example: 3
        external_user_name:
          type: string
          maxLength: 120
          description: |
            Display name of the end-user from the host desktop application. Stored on
            `cl_ai_chat.external_user_name` for audit and shown in the chat UI.
          example: 'Jan Novák'
        external_user_ico:
          type: string
          maxLength: 20
          description: |
            Czech business ID (IČ) of the end-customer's company. Trynx looks it up in
            `cl_partners_book` for the tenant; if found, the chat is linked to the partner
            via `cl_ai_chat.cl_partners_book_id`. The raw IČ is always stored in
            `cl_ai_chat.external_user_ico` regardless of match.
          example: '12345678'
        external_user_email:
          type: string
          format: email
          maxLength: 255
          nullable: true
          description: |
            Optional e-mail address of the end-user from the host desktop application.
            Validated as an e-mail when present and stored on
            `cl_ai_chat.external_user_email` for audit/contact.
          example: 'jan.novak@firma.cz'
        external_origin:
          type: string
          maxLength: 255
          nullable: true
          description: |
            Optional identifier of the host application (e.g. its name or domain).
            Stored on `cl_ai_chat.external_origin` for audit/reporting.
          example: 'Doprava4K-WPF/2.1'
        keywords:
          type: string
          maxLength: 8000
          nullable: true
          description: |
            Optional initial context from the host application (keywords, current screen,
            entity number, etc.). Stored on `cl_ai_chat.initial_keywords` and silently
            appended to the assistant's system prompt on every LLM call within this chat.
            The user does not see this text; the AI uses it to bias retrieval and answers.

            Example use cases:
            - Currently viewed invoice number: `"faktura 2024-001, nezaplacená, partner ABC s.r.o."`
            - Active screen context: `"obrazovka skladu, hledání položky"`
          example: 'faktura, splatnost, DPH'

    AiChatEmbedStartResponse:
      type: object
      properties:
        chat_id:
          type: integer
          description: ID of the freshly created `cl_ai_chat` row.
          example: 92
        token:
          type: string
          description: |
            64-character CSPRNG session token. Valid for 8 hours by default. Stored in
            `cl_ai_chat_external_session.token`. Embedded into `embed_url`.
          example: 'CxKuSjQlgQ3Cm7s5NhWBnBdmE6DcW55lD31lkwNVrCYVyvVFWgfAqtDOldB1KwiM'
        embed_url:
          type: string
          format: uri
          description: |
            Absolute URL the host desktop application loads into the WebView2 control.
            The URL is single-use within session lifetime and renders the standalone
            chat with the assistant's theme applied.
          example: 'https://trynx.example.com/application/ai-chat-embed/?token=CxKuS...'
        expires_at:
          type: string
          format: date-time
          description: ISO 8601 timestamp when the token expires.
          example: '2026-04-25T18:53:22+02:00'
        partner_matched:
          type: boolean
          description: |
            `true` if `external_user_ico` matched a record in `cl_partners_book` for the
            tenant and the chat was linked to that partner; `false` otherwise (chat is
            still created, just without a partner link).

    CommissionListItem:
      type: object
      properties:
        id: { type: integer, example: 1001 }
        cm_number: { type: string, example: 'Z-0042' }
        cm_title: { type: string, example: 'Výroba dílů' }
        cm_date: { type: string, nullable: true, example: '2026-06-01' }
        cl_partners_book_id: { type: integer, nullable: true }
        partner_name: { type: string, nullable: true }
        cl_status_id: { type: integer, nullable: true }
        status_name: { type: string, nullable: true }
        cl_users_id: { type: integer, nullable: true }
        price_e2: { type: number, nullable: true }
        price_e2_vat: { type: number, nullable: true }
        req_date: { type: string, nullable: true, description: 'Požadované dodání', example: '2026-10-01' }
        delivery_time_from: { type: string, nullable: true, description: 'Plánovaný čas závozu od (HH:MM); při vytvoření dodacího listu ze zakázky se přenese do dodacího listu', example: '08:00' }
        delivery_time_to: { type: string, nullable: true, description: 'Plánovaný čas závozu do (HH:MM)', example: '10:30' }
        total_weight_kg: { type: number, description: 'Celková hmotnost prodejních položek zakázky v kg (cl_pricelist.weight převedená na kg × množství); jen pro čtení', example: 12.5 }
        created: { type: string, nullable: true }
        create_by: { type: string }
        changed: { type: string, nullable: true }
        change_by: { type: string, nullable: true }

    CommissionItem:
      type: object
      properties:
        id: { type: integer }
        cl_commission_id: { type: integer }
        item_order: { type: integer }
        cl_pricelist_id: { type: integer, nullable: true }
        item_label: { type: string }
        quantity: { type: number }
        units: { type: string }
        price_s: { type: number }
        price_e: { type: number }
        price_e2: { type: number }
        vat: { type: number }
        discount: { type: number }

    CommissionTask:
      type: object
      properties:
        id: { type: integer }
        cl_commission_id: { type: integer }
        item_order: { type: integer }
        name: { type: string }
        description: { type: string }
        done: { type: integer }
        work_time: { type: number }
        cl_workplaces_id: { type: integer, nullable: true }

    CommissionWork:
      type: object
      properties:
        id: { type: integer }
        cl_commission_id: { type: integer }
        cl_commission_task_id: { type: integer, nullable: true }
        item_order: { type: integer }
        work_label: { type: string }
        work_date_s: { type: string, nullable: true }
        work_date_e: { type: string, nullable: true }
        work_time: { type: number }
        qty_ok: { type: integer, nullable: true }
        qty_nok: { type: integer, nullable: true }
        qty_repair: { type: integer, nullable: true }

    CommissionDetail:
      allOf:
        - $ref: '#/components/schemas/CommissionListItem'
        - type: object
          properties:
            items:
              type: array
              items: { $ref: '#/components/schemas/CommissionItem' }
            tasks:
              type: array
              items: { $ref: '#/components/schemas/CommissionTask' }
            work:
              type: array
              items: { $ref: '#/components/schemas/CommissionWork' }
            files:
              type: array
              items: { $ref: '#/components/schemas/FileItem' }

    OfferListItem:
      type: object
      properties:
        id: { type: integer, example: 2001 }
        cm_number: { type: string, example: 'N-0017' }
        cm_title: { type: string }
        offer_date: { type: string, nullable: true }
        validity_date: { type: string, nullable: true }
        cl_partners_book_id: { type: integer, nullable: true }
        partner_name: { type: string, nullable: true }
        cl_status_id: { type: integer, nullable: true }
        status_name: { type: string, nullable: true }
        cl_commission_id: { type: integer, nullable: true }
        price_e2: { type: number, nullable: true }
        created: { type: string, nullable: true }
        create_by: { type: string }
        changed: { type: string, nullable: true }
        change_by: { type: string, nullable: true }

    OfferItem:
      type: object
      properties:
        id: { type: integer }
        cl_offer_id: { type: integer }
        item_order: { type: integer }
        position: { type: integer, nullable: true }
        cl_pricelist_id: { type: integer, nullable: true }
        item_label: { type: string }
        quantity: { type: number }
        units: { type: string }
        price_e: { type: number }
        price_e2: { type: number }
        vat: { type: number }

    OfferTask:
      type: object
      properties:
        id: { type: integer }
        cl_offer_id: { type: integer }
        item_order: { type: integer }
        name: { type: string }
        description: { type: string }
        done: { type: integer }
        work_time: { type: number }

    OfferWork:
      type: object
      properties:
        id: { type: integer }
        cl_offer_id: { type: integer }
        item_order: { type: integer }
        work_label: { type: string }
        work_date_s: { type: string, nullable: true }
        work_date_e: { type: string, nullable: true }
        work_time: { type: number }

    OfferDetail:
      allOf:
        - $ref: '#/components/schemas/OfferListItem'
        - type: object
          properties:
            items:
              type: array
              items: { $ref: '#/components/schemas/OfferItem' }
            tasks:
              type: array
              items: { $ref: '#/components/schemas/OfferTask' }
            work:
              type: array
              items: { $ref: '#/components/schemas/OfferWork' }
            files:
              type: array
              items: { $ref: '#/components/schemas/FileItem' }

    InvoiceArrivedListItem:
      type: object
      description: |
        Hlavička přijaté faktury (cl_invoice_arrived) tak, jak ji vrací formatInvoiceRow().
        Datumová pole (inv_date, arv_date, vat_date, due_date, pay_date) mají formát `Y-m-d`,
        created/changed formát `Y-m-d H:i:s`.
      properties:
        id: { type: integer, example: 141444 }
        inv_number: { type: string, description: 'Účetní číslo faktury (z číselné řady, v rámci firmy unikátní)', example: 'F447' }
        rinv_number: { type: string, description: 'Číslo faktury dodavatele', example: 'API-TEST-1' }
        inv_title: { type: string, example: 'API test faktura' }
        inv_title2: { type: string }
        inv_memo: { type: string }
        inv_date: { type: string, nullable: true, description: 'Datum vystavení', example: '2026-07-23' }
        arv_date: { type: string, nullable: true, description: 'Datum přijetí', example: '2026-07-23' }
        vat_date: { type: string, nullable: true, description: 'DUZP', example: '2026-07-23' }
        due_date: { type: string, nullable: true, description: 'Datum splatnosti', example: '2026-08-06' }
        pay_date: { type: string, nullable: true, description: 'Datum úhrady; NULL = neuhrazeno (dopočítává paymentUpdate)', example: '2026-07-24' }
        var_symb: { type: string, example: '2026000447' }
        konst_symb: { type: string }
        spec_symb: { type: string }
        od_number: { type: string, description: 'Číslo objednávky' }
        delivery_number: { type: string, description: 'Číslo dodacího listu' }
        header_txt: { type: string }
        header_show: { type: integer, example: 0 }
        footer_txt: { type: string }
        footer_show: { type: integer, example: 0 }
        cl_partners_book_id: { type: integer, nullable: true, description: 'Dodavatel', example: 464305 }
        partner_name: { type: string, nullable: true, description: 'cl_partners_book.company dodavatele', example: 'ACME s.r.o.' }
        cl_partners_branch_id: { type: integer, nullable: true }
        cl_partners_book_workers_id: { type: integer, nullable: true }
        cl_partners_account_id: { type: integer, nullable: true, description: 'Navázaný účet dodavatele (cl_partners_account)', example: 5821 }
        partner_account:
          type: object
          nullable: true
          description: 'Navázaný účet dodavatele (dle cl_partners_account_id), obě formy dopočítané'
          properties:
            account_code: { type: string, description: 'předčíslí-číslo (bez kódu banky), nebo jen číslo', example: '2000145399' }
            bank_code: { type: string, example: '0800' }
            iban_code: { type: string, example: 'CZ7908000000002000145399' }
            swift_code: { type: string, nullable: true }
        cl_status_id: { type: integer, nullable: true }
        status_name: { type: string, nullable: true, description: 'cl_status.status_name (status_use = invoice_arrived)', example: 'Nová' }
        cl_currencies_id: { type: integer, nullable: true }
        currency_code: { type: string, nullable: true, description: 'cl_currencies.currency_code měny dokladu', example: 'CZK' }
        currency_rate: { type: number, description: 'Kurz měny dokladu (u create výchozí fix_rate měny, jinak 1)', example: 1 }
        cl_center_id: { type: integer, nullable: true }
        center_name: { type: string, nullable: true, description: 'cl_center.name střediska', example: 'Výroba' }
        cl_payment_types_id: { type: integer, nullable: true }
        payment_type_name: { type: string, nullable: true, description: 'cl_payment_types.name formy úhrady', example: 'Převodem' }
        cl_invoice_types_id: { type: integer, nullable: true }
        invoice_type_name: { type: string, nullable: true, description: 'cl_invoice_types.name druhu dokladu (inv_type = 4)', example: 'Faktura přijatá' }
        cl_users_id: { type: integer, nullable: true }
        user_name: { type: string, nullable: true, description: 'cl_users.name odpovědného uživatele', example: 'Jan Novák' }
        cl_company_branch_id: { type: integer, nullable: true, description: 'Pobočka firmy; používá-li firma pobočky, měl by ji klient u create posílat' }
        cl_commission_id: { type: integer, nullable: true, description: 'Zakázka navázaná na hlavičku faktury' }
        cl_number_series_id: { type: integer, nullable: true }
        cl_payment_order_id: { type: integer, nullable: true, description: 'Navázaný platební příkaz (přes update nelze měnit)' }
        payment_order_number: { type: string, nullable: true, description: 'cl_payment_order.po_number' }
        cl_collection_order_id: { type: integer, nullable: true, description: 'Navázaný inkasní příkaz (přes update nelze měnit)' }
        collection_order_number: { type: string, nullable: true, description: 'cl_collection_order.co_number' }
        price_base0: { type: number, description: 'Základ osvobozeno / bez DPH', example: 0 }
        price_base1: { type: number, description: 'Základ v sazbě vat1', example: 1000 }
        price_base2: { type: number, example: 0 }
        price_base3: { type: number, example: 0 }
        vat1: { type: number, description: 'Sazba DPH v %', example: 21 }
        vat2: { type: number, example: 0 }
        vat3: { type: number, example: 0 }
        price_vat1: { type: number, description: 'Daň = round(price_base1 * vat1 / 100, 2); dopočítává server', example: 210 }
        price_vat2: { type: number, example: 0 }
        price_vat3: { type: number, example: 0 }
        price_total1: { type: number, description: 'round(price_base1 + price_vat1, 2); dopočítává server', example: 1210 }
        price_total2: { type: number, example: 0 }
        price_total3: { type: number, example: 0 }
        price_correction: { type: number, description: 'Haléřové vyrovnání – vstupuje už do price_e2', example: -0.4 }
        price_e2: { type: number, description: 'Celkem bez DPH = round(price_base0..3 + price_correction, 2)', example: 999.6 }
        price_e2_vat: { type: number, description: 'Celkem s DPH = round(price_e2 + price_vat1..3, 2)', example: 1209.6 }
        price_payed: { type: number, description: 'Uhrazeno – součet úhrad, přepočítává paymentUpdate (přes update nelze měnit)', example: 500 }
        advance_payed: { type: number, description: 'Uhrazená záloha; do price_remaining se NEpromítá (přes update nelze měnit)', example: 0 }
        price_on_commission: { type: number, description: 'Rozpuštěno na zakázky – součet cl_invoice_arrived_commission.amount (přes update nelze měnit)', example: 400 }
        price_remaining:
          type: number
          description: |
            Zbývá uhradit. U plátce DPH `price_e2_vat − price_payed`, u neplátce
            `price_e2 − price_payed` (plátcovství se bere z nastavení autorizované firmy).
            Záloha `advance_payed` se do zbytku NEpromítá. Definice je shodná
            s InvoiceArrivedManager::makePayment() — co ukáže `price_remaining`,
            to doplatí endpoint `pay-full`.
          example: 709.6
        pdp: { type: integer, description: 'Přenesená daňová povinnost', example: 0 }
        import: { type: integer, example: 0 }
        recalc_disabled: { type: integer, description: '1 = „Nepočítat" – server přeskočí výpočet price_vat*/price_total*, součty price_e2/price_e2_vat počítá dál', example: 0 }
        locked: { type: integer, description: '1 = zamčená faktura, zápisové operace vrací 409; přes update nelze nastavit ani zrušit', example: 0 }
        s_eml: { type: integer, example: 0 }
        created: { type: string, nullable: true, example: '2026-07-23 09:12:44' }
        create_by: { type: string, example: 'API' }
        changed: { type: string, nullable: true, example: '2026-07-23 10:03:11' }
        change_by: { type: string, nullable: true, example: 'API' }

    InvoiceArrivedPayment:
      type: object
      description: Úhrada přijaté faktury (cl_invoice_arrived_payments) dle formatPaymentRow().
      properties:
        id: { type: integer, example: 114905 }
        cl_invoice_arrived_id: { type: integer, example: 141444 }
        item_order: { type: integer, description: 'Pořadí úhrady (server dosazuje MAX+1)', example: 1 }
        pay_date: { type: string, nullable: true, description: 'Datum úhrady (Y-m-d), výchozí dnes', example: '2026-07-24' }
        pay_price: { type: number, description: 'Uhrazená částka', example: 500 }
        pay_doc: { type: string, description: 'Doklad úhrady', example: 'VB 2026/114' }
        pay_type: { type: integer, example: 0 }
        pay_vat: { type: integer, description: 'Úhrada včetně DPH', example: 0 }
        vat: { type: number, example: 0 }
        cl_payment_types_id: { type: integer, nullable: true, description: 'Výchozí = forma úhrady faktury; u hotovostní formy vzniká pokladní doklad' }
        payment_type_name: { type: string, nullable: true, example: 'Hotově' }
        cl_currencies_id: { type: integer, nullable: true, description: 'Výchozí = měna faktury' }
        currency_code: { type: string, nullable: true, example: 'CZK' }
        cl_cash_id: { type: integer, nullable: true, description: 'Navázaný pokladní doklad' }
        cl_bank_trans_id: { type: integer, nullable: true, description: 'Navázaný bankovní pohyb' }
        created: { type: string, nullable: true, example: '2026-07-24 08:30:00' }
        create_by: { type: string, example: 'API' }
        changed: { type: string, nullable: true }
        change_by: { type: string, nullable: true }

    InvoiceArrivedCommission:
      type: object
      description: |
        Rozpuštění nákladu faktury na zakázku (cl_invoice_arrived_commission)
        dle formatAllocationRow(). Každá mutace přepočítá price_on_commission na hlavičce.
      properties:
        id: { type: integer, example: 8801 }
        cl_invoice_arrived_id: { type: integer, example: 141444 }
        cl_commission_id: { type: integer, example: 1001 }
        commission_number: { type: string, nullable: true, description: 'cl_commission.cm_number', example: 'Z-0042' }
        commission_title: { type: string, nullable: true, description: 'cl_commission.cm_title', example: 'Výroba dílů' }
        item_order: { type: integer, description: 'Pořadí (server dosazuje MAX+1)', example: 1 }
        amount: { type: number, description: 'Rozpuštěná částka', example: 400 }
        into_costs: { type: integer, description: '1 = zahrnout do nákladů zakázky', example: 0 }
        note: { type: string }
        created: { type: string, nullable: true }
        create_by: { type: string, example: 'API' }
        changed: { type: string, nullable: true }
        change_by: { type: string, nullable: true }

    InvoiceArrivedDetail:
      allOf:
        - $ref: '#/components/schemas/InvoiceArrivedListItem'
        - type: object
          properties:
            payments:
              type: array
              items: { $ref: '#/components/schemas/InvoiceArrivedPayment' }
            commissions:
              type: array
              items: { $ref: '#/components/schemas/InvoiceArrivedCommission' }
            files:
              type: array
              items: { $ref: '#/components/schemas/FileItem' }

    InvoiceArrivedStatus:
      type: object
      description: Stav přijaté faktury (cl_status, status_use = invoice_arrived).
      properties:
        id: { type: integer, example: 12 }
        status_name: { type: string, example: 'Nová' }
        s_new: { type: integer, description: '1 = výchozí stav nové faktury', example: 1 }
        s_work: { type: integer, example: 0 }
        s_fin: { type: integer, description: '1 = finální stav (pay-full na něj fakturu přepne)', example: 0 }
        s_storno: { type: integer, example: 0 }
        color_hex: { type: string, nullable: true, example: '#FF9900' }
        color_ink_hex: { type: string, nullable: true, example: '#FFFFFF' }

    PaymentType:
      type: object
      description: Forma úhrady (cl_payment_types, jen not_active = 0).
      properties:
        id: { type: integer, example: 3 }
        name: { type: string, example: 'Převodem' }
        short_desc: { type: string, nullable: true, example: 'PŘ' }
        payment_type: { type: integer, description: '0 = převod, 1 = hotovost, 2 = dobírka, 3 = karta', example: 0 }

    Currency:
      type: object
      description: Měna (cl_currencies).
      properties:
        id: { type: integer, example: 1 }
        currency_code: { type: string, example: 'CZK' }
        currency_name: { type: string, example: 'Koruna česká' }
        rate: { type: number, description: 'Aktuální kurz', example: 1 }
        fix_rate: { type: number, description: 'Pevný kurz – dosazuje se do currency_rate faktury', example: 1 }
        amount: { type: integer, description: 'Množství měny, ke kterému se kurz vztahuje', example: 1 }
        decimal_places: { type: integer, example: 2 }
        dtm_rate: { type: string, nullable: true, description: 'Datum a čas kurzu (Y-m-d H:i:s)', example: '2026-07-23 09:00:00' }
        is_default: { type: boolean, description: 'true = výchozí měna firmy', example: true }

    Center:
      type: object
      description: Středisko (cl_center).
      properties:
        id: { type: integer, example: 4 }
        name: { type: string, example: 'Výroba' }
        short_desc: { type: string, nullable: true, example: 'VYR' }
        default_center: { type: integer, description: '1 = výchozí středisko', example: 0 }

    InvoiceType:
      type: object
      description: Druh dokladu přijaté faktury (cl_invoice_types, inv_type = 4).
      properties:
        id: { type: integer, example: 7 }
        name: { type: string, example: 'Faktura přijatá' }
        default_type: { type: integer, description: '1 = výchozí druh dokladu', example: 1 }
        cl_number_series_id: { type: integer, nullable: true, description: 'Číselná řada navázaná na druh dokladu' }

    VatRate:
      type: object
      description: Sazba DPH platná k datu pro zemi firmy (cl_rates_vat).
      properties:
        id: { type: integer, example: 2 }
        code_name: { type: string, description: 'high / low / third / zero', example: 'high' }
        rates: { type: number, description: 'Sazba v %', example: 21 }
        description: { type: string, nullable: true, example: 'Základní sazba' }
        valid_from: { type: string, nullable: true, example: '2024-01-01' }
        valid_to: { type: string, nullable: true, example: null }

    FileItem:
      type: object
      properties:
        id: { type: integer, example: 555 }
        file_name: { type: string, example: 'vykres-01.pdf' }
        label_name: { type: string, example: 'výkres.pdf' }
        mime_type: { type: string, example: 'application/pdf' }
        file_size: { type: integer, example: 84213 }
        created: { type: string, nullable: true }
        create_by: { type: string }

    FileUploadInput:
      type: object
      required: [name, type, size, dataUrl]
      properties:
        name: { type: string, example: 'výkres.pdf' }
        type: { type: string, example: 'application/pdf' }
        size: { type: integer, example: 84213 }
        dataUrl:
          type: string
          description: 'Base64 data URL: data:<mime>;base64,<...>'
          example: 'data:application/pdf;base64,JVBERi0xLjQ...'

    CashListItem:
      type: object
      description: |
        Hlavička pokladního dokladu (cl_cash) tak, jak ji vrací formatCashRow().
        inv_date má formát `Y-m-d`, created/changed formát `Y-m-d H:i:s`.
      properties:
        id: { type: integer, example: 9911 }
        cash_number: { type: string, description: 'Číslo dokladu z číselné řady cash_in/cash_out', example: 'V26/0042' }
        direction:
          type: string
          enum: [in, out]
          description: 'Odvozeno ze znaménka cash (cash >= 0 → in), není to samostatný uložený sloupec'
          example: out
        cash: { type: number, description: 'Částka; záporná u výdeje, kladná u příjmu', example: -500 }
        title: { type: string, example: 'Nákup kancelářských potřeb' }
        description_txt: { type: string }
        inv_date: { type: string, nullable: true, description: 'Datum dokladu', example: '2026-08-05' }
        locked:
          type: integer
          description: |
            1 = zamčený doklad. API ho nikdy nenastavuje ani nekontroluje – zamyká jen tisk PDF
            u firem s lock_ap=1. Ochrana zápisu se váže na provázanost dokladu, ne na tento příznak.
          example: 0
        eet_relevant:
          type: integer
          enum: [0, 1]
          description: |
            1 = doklad je označený k evidenci tržby do EET. Podle tohoto příznaku se
            při create i update rozhoduje, jestli se tržba odešle finanční správě
            (`EetCashService::shouldSend()`). Vrací se proto i v odpovědi – jinak by klient
            neměl jak ověřit, s jakým fiskálním příznakem doklad skutečně uložil.
          example: 1
        cl_cash_def_id: { type: integer, nullable: true, description: 'Pokladna' }
        cash_def_name: { type: string, nullable: true, description: 'cl_cash_def.name', example: 'Hlavní pokladna' }
        cl_partners_book_id: { type: integer, nullable: true }
        partner_name: { type: string, nullable: true, description: 'cl_partners_book.company', example: 'ACME s.r.o.' }
        cl_center_id: { type: integer, nullable: true }
        center_name: { type: string, nullable: true, example: 'Výroba' }
        cl_currencies_id: { type: integer, nullable: true }
        currency_code: { type: string, nullable: true, example: 'CZK' }
        currency_rate: { type: number, description: 'Kurz měny dokladu (u create výchozí fix_rate měny, jinak 1)', example: 1 }
        cl_status_id: { type: integer, nullable: true }
        status_name: { type: string, nullable: true, description: 'cl_status.status_name (status_use = cash)', example: 'Zaplaceno' }
        cl_invoice_types_id: { type: integer, nullable: true }
        invoice_type_name: { type: string, nullable: true, example: 'Výdajový pokladní doklad' }
        cl_users_id: { type: integer, nullable: true, description: 'Uživatel firmy (ověřuje se přes cl_access_company)' }
        cl_number_series_id: { type: integer, nullable: true, description: 'Číselná řada; nastavuje se jen při create, update ji neumí měnit' }
        cl_company_branch_id: { type: integer, nullable: true }
        cl_partners_book_workers_id: { type: integer, nullable: true, description: 'Kontaktní osoba partnera' }
        created: { type: string, nullable: true, example: '2026-08-05 09:12:44' }
        create_by:
          type: string
          description: '"API" u dokladu založeného přes toto API; u dokladu založeného ve webu jméno přihlášeného uživatele'
          example: 'API'
        changed: { type: string, nullable: true }
        change_by:
          type: string
          description: |
            NOT NULL varchar, nikdy null. Po create prázdný řetězec (create() ho nenastavuje).
            Při update a set-status se zapíše JMÉNO PŘIHLÁŠENÉHO UŽIVATELE, ne "API":
            Base::update() s příznakem $mark přebíjí change_by jménem z identity, kdykoli je
            uživatel přihlášený – a u bearer tokenu přihlášený je, identita i token sdílejí
            jednu session. Hodnota "API" zbyde jen v okrajovém případě, kdy identita v session
            vyprší dřív než bearer token.
          example: 'Jan Novák'

    CashPairedDoc:
      type: object
      description: |
        Doklad spárovaný s pokladním dokladem přes cl_paired_docs.
      properties:
        type:
          type: string
          description: |
            Druh navázaného dokladu. Hodnota NENÍ z uzavřeného číselníku – odvozuje se
            za běhu z názvu vazebního sloupce tabulky cl_paired_docs (cl_invoice_id → invoice,
            correction_cl_sale_id → correction_sale). Přibude-li v programu nová vazba,
            objeví se ve výstupu sama, bez zásahu do API i do tohoto schématu.

            Klient proto MUSÍ umět neznámý type bezpečně ignorovat (typicky nenabídnout
            proklik) a NESMÍ hodnotu validovat proti pevnému výčtu – enum tu záměrně není,
            aby generovaný klient nespadl na platné odpovědi.

            K 5. 8. 2026 tabulka obsahuje 20 vazebních sloupců, tedy tyto hodnoty
            (informativní snímek, ne uzavřený seznam): commission, offer, invoice,
            invoice_advance, invoice_internal, store_docs, order, invoice_arrived,
            delivery_note, delivery_note_in, sale, transport, b2b_order, task,
            partners_event, payment_order, version_changelist, collection_order,
            delivery, correction_sale.
          example: invoice
        id: { type: integer }

    CashDetail:
      allOf:
        - $ref: '#/components/schemas/CashListItem'
        - type: object
          properties:
            files:
              type: array
              items: { $ref: '#/components/schemas/FileItem' }
            paired_docs:
              type: array
              items: { $ref: '#/components/schemas/CashPairedDoc' }

    CashBalanceItem:
      type: object
      description: Zůstatek jedné pokladny (get-balance).
      properties:
        cl_cash_def_id: { type: integer, example: 1 }
        name: { type: string, example: 'Hlavní pokladna' }
        short_name: { type: string, nullable: true, example: 'HP' }
        balance: { type: number, description: 'Součet cash k datu (nepovinný filtr date)', example: 12345.5 }
        currency_code: { type: string, nullable: true, example: 'CZK' }

    CashRegister:
      type: object
      description: Pokladna (cl_cash_def).
      properties:
        id: { type: integer, example: 1 }
        name: { type: string, example: 'Hlavní pokladna' }
        short_name: { type: string, nullable: true, example: 'HP' }
        def_cash: { type: integer, description: '1 = výchozí pokladna', example: 1 }
        cl_currencies_id: { type: integer, nullable: true }
        currency_code: { type: string, nullable: true, example: 'CZK' }
        cl_number_series_id_cashin: { type: integer, nullable: true, description: 'Číselná řada pro příjem; null = řada pobočky nebo výchozí řada firmy' }
        cl_number_series_id_cashout: { type: integer, nullable: true, description: 'Číselná řada pro výdej; null = řada pobočky nebo výchozí řada firmy' }

    CashCurrency:
      type: object
      description: Měna (cl_currencies) pro pokladnu.
      properties:
        id: { type: integer, example: 1 }
        currency_code: { type: string, example: 'CZK' }
        fix_rate: { type: number, example: 1 }
        decimal_places: { type: integer, example: 2 }

    CashNumberSeries:
      type: object
      description: Číselná řada pokladních dokladů (getSeriesForUse).
      properties:
        id: { type: integer, example: 3 }
        name: { type: string, example: 'Výdajové pokladní doklady' }
        default: { type: integer, description: '(int) cl_number_series.form_default – 1 = výchozí řada pro dané use, 0 jinak; nikdy JSON boolean', example: 1 }
        use: { type: string, enum: [cash_in, cash_out] }

    CashStatus:
      type: object
      description: Stav pokladního dokladu (cl_status, status_use = cash).
      properties:
        id: { type: integer, example: 21 }
        name: { type: string, description: 'cl_status.status_name', example: 'Zaplaceno' }
        s_new: { type: integer, description: '1 = výchozí stav nového dokladu', example: 1 }
        s_fin: { type: integer, example: 0 }
        color_hex: { type: string, nullable: true, example: '#00AA00' }

    InvoiceListItem:
      type: object
      description: |
        Hlavička faktury vydané (cl_invoice) tak, jak ji vrací formatInvoiceRow().
        inv_date/vat_date/due_date/pay_date mají formát `Y-m-d`, created/changed
        formát `Y-m-d H:i:s`.
      properties:
        id: { type: integer, example: 141500 }
        inv_number: { type: string, description: 'Číslo dokladu z číselné řady form_use=invoice', example: '26F0087' }
        var_symb: { type: string, nullable: true }
        spec_symb: { type: string, nullable: true, description: 'Přepočítává se automaticky přes apply-partner' }
        konst_symb: { type: string, nullable: true }
        inv_title: { type: string, nullable: true }
        od_number: { type: string, nullable: true }
        inv_date: { type: string, nullable: true, example: '2026-08-08' }
        vat_date: { type: string, nullable: true }
        due_date: { type: string, nullable: true }
        pay_date: { type: string, nullable: true }
        cl_partners_book_id: { type: integer, nullable: true, description: 'Povinné při create, nelze odebrat (null) při update' }
        partner_name: { type: string, nullable: true, description: 'cl_partners_book.company' }
        cl_currencies_id: { type: integer, nullable: true }
        currency_code: { type: string, nullable: true, example: 'CZK' }
        currency_rate: { type: number, example: 1 }
        cl_status_id: { type: integer, nullable: true }
        status_name: { type: string, nullable: true, description: 'cl_status.status_name (status_use = invoice)' }
        cl_invoice_types_id: { type: integer, nullable: true }
        invoice_type_name: { type: string, nullable: true }
        cl_payment_types_id: { type: integer, nullable: true }
        payment_type_name: { type: string, nullable: true }
        cl_bank_accounts_id: { type: integer, nullable: true }
        cl_center_id: { type: integer, nullable: true }
        center_name: { type: string, nullable: true }
        cl_commission_id:
          type: integer
          nullable: true
          description: 'Zakázka hlavičky – položka může mít vlastní cl_commission_id, který má u blokace přednost'
        cl_number_series_id: { type: integer, nullable: true, description: 'Nastavuje se jen při create, update ji neumí měnit' }
        cl_company_branch_id: { type: integer, nullable: true }
        cl_users_id: { type: integer, nullable: true, description: 'Ověřuje se přes cl_access_company (členství), ne cl_users.cl_company_id' }
        price_e2: { type: number, description: 'Základ (bez DPH), dopočítává server' }
        price_e2_vat: { type: number, description: 'Celkem s DPH, dopočítává server' }
        price_payed: { type: number, description: 'Zaplaceno, dopočítává se z úhrad' }
        price_e2_used: { type: number, description: 'U zálohové faktury – kolik už bylo vyčerpáno' }
        price_correction:
          type: number
          description: 'Bez tohoto pole appka nespočítá „Celkem s DPH" na tiskopisu (viz vatTotal.latte)'
        advance_payed: { type: number }
        base_payed0: { type: number }
        base_payed1: { type: number }
        base_payed2: { type: number }
        base_payed3: { type: number }
        vat_active: { type: integer, description: '0/1 – uzavřená množina, jinak 400' }
        price_e_type: { type: integer }
        pdp: { type: integer }
        export: { type: integer }
        locked:
          type: integer
          description: |
            1 = zamčeno. API ho samo nikdy nenastavuje NA HLAVIČCE, kromě vedlejšího
            efektu tisku PDF u firem s cl_company.lock_ap=1 (pdf/{id} fakturu zamkne).
          example: 0
        storno: { type: integer }
        s_eml: { type: integer }
        reminder_count: { type: integer }
        created: { type: string, nullable: true }
        create_by: { type: string, nullable: true }
        changed: { type: string, nullable: true }
        change_by:
          type: string
          description: 'Jméno přihlášeného uživatele (identita a bearer token sdílí session), ne "API"'

    InvoicePairedDoc:
      type: object
      description: |
        Doklad spárovaný s fakturou přes cl_paired_docs. Stejný mechanismus jako
        CashPairedDoc – type se odvozuje za běhu z názvu vazebního sloupce tabulky
        (NENÍ z uzavřeného číselníku, enum tu záměrně chybí). Klient musí neznámý
        type bezpečně ignorovat a nevalidovat proti pevnému výčtu.
      properties:
        type: { type: string, example: invoice_advance }
        id: { type: integer }

    InvoiceVatBreakdownItem:
      type: object
      description: Jeden slot rozpisu DPH podle sazby (get-one). Sloty s nulovým základem se vynechávají.
      properties:
        rate: { type: number, example: 21 }
        base: { type: number }
        vat: { type: number }
        correction: { type: number, description: 'correction_baseN – korekce ze storna/dobropisu v daném slotu' }

    InvoiceItem:
      type: object
      description: |
        Řádek prodejní (cl_invoice_items) nebo vratné (cl_invoice_items_back) položky
        faktury, formátovaný formatInvoiceItemRow(). Server price_e2/price_e2_vat/vat
        VŽDY sám dopočítá (InvoiceItemService::computeItemTotals()) – klientem poslané
        hotové částky se přepočítají, neuloží se tak, jak přišly.
      properties:
        id: { type: integer, example: 55012 }
        item_order: { type: integer }
        cl_pricelist_id: { type: integer, nullable: true, description: 'null u volné textové položky (doprava, práce...)' }
        item_label: { type: string }
        quantity: { type: number }
        units: { type: string, nullable: true }
        price_e: { type: number }
        price_e_type: { type: integer, description: '0/1 – řídí, zda je price_e s DPH, nebo bez' }
        discount: { type: number }
        price_e2: { type: number }
        vat: { type: number }
        price_e2_vat: { type: number }
        cl_storage_id: { type: integer, nullable: true }
        cl_store_move_id: { type: integer, nullable: true, description: 'Navázaný skladový pohyb, vyplní se po uložení' }
        description1: { type: string, nullable: true }
        description2: { type: string, nullable: true }

    InvoiceItemStoreInfo:
      type: object
      description: Informace o skladovém dokladu založeném/použitém při uložení položky (vedlejší efekt, appka o něj nežádá zvlášť).
      properties:
        cl_store_docs_id: { type: integer, nullable: true }
        cl_store_move_id: { type: integer, nullable: true }
        doc_number: { type: string, nullable: true, example: 'V26/0311' }
        stock_after: { type: number, nullable: true, description: 'Stav skladu dané ceníkové položky po uložení' }

    InvoiceItemSaveResponse:
      type: object
      properties:
        item: { $ref: '#/components/schemas/InvoiceItem' }
        store: { $ref: '#/components/schemas/InvoiceItemStoreInfo' }
        bonus_items:
          type: array
          items: { $ref: '#/components/schemas/InvoiceItem' }
          description: Dárky/bonusy rozgenerované z ceníkové vazby (cl_pricelist_bonds), stejný target jako uložená položka.
        warnings:
          type: array
          items: { type: string }
          description: Měkká (kind=warning) upozornění – položka se přesto uložila.
        invoice: { $ref: '#/components/schemas/InvoiceListItem' }

    InvoicePayment:
      type: object
      description: Úhrada faktury (cl_invoice_payments), formatInvoicePaymentRow().
      properties:
        id: { type: integer, example: 9001 }
        cl_invoice_id: { type: integer }
        pay_date: { type: string, example: '2026-08-08' }
        pay_price: { type: number }
        pay_doc: { type: string, nullable: true }
        pay_type: { type: integer, description: '1 = hotovost (zakládá pokladní doklad), 0 = jinak' }
        pay_vat: { type: integer }
        vat: { type: number }
        cl_currencies_id: { type: integer, nullable: true }
        currency_code: { type: string, nullable: true }
        cl_payment_types_id: { type: integer, nullable: true }
        payment_type_name: { type: string, nullable: true }
        used_cl_invoice_id:
          type: integer
          nullable: true
          description: 'Vyplněno u čerpání zálohy – ukazuje na fakturu z daňové zálohové řady (form_use=invoice_tax)'
        cl_users_id: { type: integer, nullable: true }
        cl_cash_id: { type: integer, nullable: true, description: 'Vyplněno, vznikl-li pokladní doklad' }
        cl_bank_trans_id: { type: integer, nullable: true }
        created: { type: string, nullable: true }
        create_by: { type: string, nullable: true }
        changed: { type: string, nullable: true }
        change_by: { type: string, nullable: true }

    InvoiceCashDocumentRef:
      type: object
      nullable: true
      description: Pokladní doklad založený jako vedlejší efekt hotovostní úhrady, nebo null.
      properties:
        id: { type: integer }
        cash_number: { type: string, example: 'P26/0042' }

    DeletedRecord:
      type: object
      description: Záznam o smazání. Obsah smazaného řádku se nevrací.
      properties:
        table: { type: string, example: cl_invoice_payments }
        id: { type: integer, example: 12345 }
        deleted_at: { type: string, nullable: true, example: '2026-09-22 14:03:11' }

    ReportingPage:
      type: object
      description: Obálka stránkovaného výpisu /reporting/*.
      properties:
        status: { type: string, enum: [ok] }
        total: { type: integer, nullable: true, description: 'Počet řádků odpovídajících filtru. Jen bez after_id, s kurzorem null.' }
        next_after_id: { type: integer, nullable: true, description: 'Hodnota after_id pro další stránku; null = konec dat.' }
    ReportingStoreMove:
      type: object
      properties:
        id: { type: integer }
        cl_store_docs_id: { type: integer, nullable: true }
        doc_date: { type: string, nullable: true, description: 'Datum skladového dokladu (Y-m-d)' }
        doc_type: { type: integer, nullable: true, description: '0 = příjem, 1 = výdej' }
        cl_store_id: { type: integer, nullable: true }
        cl_pricelist_id: { type: integer, nullable: true }
        cl_storage_id: { type: integer, nullable: true }
        item_order: { type: integer }
        s_in: { type: number }
        s_out: { type: number }
        s_end: { type: number }
        price_s: { type: number, description: 'Nákupní (skladová) cena za jednotku' }
        price_in: { type: number }
        price_e: { type: number }
        discount: { type: number }
        price_e2: { type: number, description: 'Prodejní cena bez DPH' }
        price_e2_vat: { type: number }
        vat: { type: number }
        profit: { type: number }
        batch: { type: string, nullable: true }
        exp_date: { type: string, nullable: true }
        cl_invoice_items_id: { type: integer, nullable: true }
        cl_invoice_items_back_id: { type: integer, nullable: true }
        cl_delivery_note_items_id: { type: integer, nullable: true }
        import: { type: number }
        import_fin: { type: number }
        rollup: { type: integer, description: '1 = souhrnný pohyb z archivace skladu, viz popis get-store-moves' }
        created: { type: string, nullable: true }
        changed: { type: string, nullable: true }
    ReportingStoreDoc:
      type: object
      properties:
        id: { type: integer }
        doc_type: { type: integer, description: '0 = příjem, 1 = výdej' }
        doc_number: { type: string, nullable: true }
        doc_date: { type: string, nullable: true }
        doc_title: { type: string, nullable: true }
        cl_partners_book_id: { type: integer, nullable: true }
        cl_storage_id: { type: integer, nullable: true }
        cl_company_branch_id: { type: integer, nullable: true }
        cl_center_id: { type: integer, nullable: true }
        cl_currencies_id: { type: integer, nullable: true }
        currency_rate: { type: number }
        price_in: { type: number }
        price_s: { type: number }
        price_e2: { type: number }
        price_e2_vat: { type: number }
        profit: { type: number }
        cl_invoice_id: { type: integer, nullable: true }
        cl_invoice_arrived_id: { type: integer, nullable: true }
        cl_delivery_note_id: { type: integer, nullable: true }
        cl_delivery_note_in_id: { type: integer, nullable: true }
        cl_commission_id: { type: integer, nullable: true }
        cl_order_id: { type: integer, nullable: true }
        cl_sale_id: { type: integer, nullable: true }
        created: { type: string, nullable: true }
        changed: { type: string, nullable: true }
    ReportingStock:
      type: object
      properties:
        id: { type: integer }
        cl_pricelist_id: { type: integer, nullable: true }
        cl_storage_id: { type: integer, nullable: true }
        batch: { type: string, nullable: true }
        exp_date: { type: string, nullable: true }
        quantity: { type: number }
        quantity_min: { type: number }
        quantity_req: { type: number }
        price_s: { type: number }
        created: { type: string, nullable: true }
        changed: { type: string, nullable: true }
    ReportingPricelistItem:
      type: object
      properties:
        id: { type: integer }
        identification: { type: string, nullable: true }
        ean_code: { type: string, nullable: true }
        item_label: { type: string, nullable: true }
        cl_pricelist_group_id: { type: integer, nullable: true }
        unit: { type: string, nullable: true }
        price_s: { type: number }
        price: { type: number }
        price_vat: { type: number }
        vat: { type: number }
        cl_currencies_id: { type: integer, nullable: true }
        weight: { type: number }
        not_active: { type: integer, description: '1 = neaktivní položka (vrací se kvůli historickým pohybům)' }
        created: { type: string, nullable: true }
        changed: { type: string, nullable: true }
    ReportingPricelistGroup:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        cl_pricelist_group_id: { type: integer, nullable: true, description: 'Nadřazená skupina' }
    ReportingStorage:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        description: { type: string, nullable: true }
        cl_storage_id: { type: integer, nullable: true, description: 'Nadřazený sklad' }
    ReportingCommission:
      type: object
      properties:
        id: { type: integer }
        cm_number: { type: string, nullable: true }
        cm_date: { type: string, nullable: true, description: 'Datum zakázky (Y-m-d)' }
        cm_title: { type: string, nullable: true }
        cm_order: { type: string, nullable: true }
        cl_partners_book_id: { type: integer, nullable: true }
        cl_partners_branch_id: { type: integer, nullable: true }
        cl_status_id: { type: integer, nullable: true }
        cl_center_id: { type: integer, nullable: true }
        cl_storage_id: { type: integer, nullable: true }
        cl_company_branch_id: { type: integer, nullable: true }
        cl_users_id: { type: integer, nullable: true }
        cl_users_id2: { type: integer, nullable: true, description: 'Druhý řešitel zakázky' }
        cl_currencies_id: { type: integer, nullable: true }
        currency_rate: { type: number }
        price_s: { type: number, description: 'Nákupní (skladová) cena' }
        price_e2: { type: number, description: 'Prodejní cena bez DPH' }
        price_e2_vat: { type: number }
        price_w: { type: number }
        price_w2: { type: number }
        profit: { type: number }
        profit_abs: { type: number }
        profit_items: { type: number, description: 'Zisk z prodejních položek (cl_commission_items_sel)' }
        profit_works: { type: number, description: 'Zisk z práce (cl_commission_work)' }
        delivery_date: { type: string, nullable: true, description: 'Y-m-d' }
        req_date: { type: string, nullable: true, description: 'Y-m-d' }
        start_date: { type: string, nullable: true, description: 'Y-m-d' }
        cl_invoice_id: { type: integer, nullable: true }
        cl_invoice_advance_id: { type: integer, nullable: true }
        cl_store_docs_id: { type: integer, nullable: true }
        storno: { type: integer, description: '1 = stornováno' }
        locked: { type: integer, description: '1 = uzamčeno' }
        created: { type: string, nullable: true }
        changed: { type: string, nullable: true }
    ReportingCommissionItem:
      type: object
      description: 'Prodejní položka zakázky (cl_commission_items_sel). Nákladové položky (cl_commission_items) tento výpis nevrací.'
      properties:
        id: { type: integer }
        cl_commission_id: { type: integer, nullable: true }
        item_order: { type: integer }
        cl_pricelist_id: { type: integer, nullable: true }
        item_label: { type: string, nullable: true }
        quantity: { type: number }
        units: { type: string, nullable: true }
        price_s: { type: number }
        price_e: { type: number }
        price_e_type: { type: integer }
        discount: { type: number }
        price_e2: { type: number }
        vat: { type: number }
        price_e2_vat: { type: number }
        profit: { type: number }
        cl_storage_id: { type: integer, nullable: true }
        cl_center_id: { type: integer, nullable: true }
        cl_invoice_id: { type: integer, nullable: true }
        cl_invoice_items_id: { type: integer, nullable: true }
        cl_store_move_id: { type: integer, nullable: true }
        created: { type: string, nullable: true }
        changed: { type: string, nullable: true }
    ReportingCommissionWork:
      type: object
      properties:
        id: { type: integer }
        cl_commission_id: { type: integer, nullable: true }
        item_order: { type: integer }
        work_label: { type: string, nullable: true }
        work_date_s: { type: string, nullable: true, description: 'Y-m-d H:i:s' }
        work_date_e: { type: string, nullable: true, description: 'Y-m-d H:i:s' }
        work_time: { type: number }
        work_rate: { type: number }
        profit: { type: number }
        cl_users_id: { type: integer, nullable: true }
        cl_commission_task_id: { type: integer, nullable: true }
        cl_center_id: { type: integer, nullable: true }
        cl_invoice_id: { type: integer, nullable: true }
        cl_invoice_items_id: { type: integer, nullable: true }
        created: { type: string, nullable: true }
        changed: { type: string, nullable: true }
    ReportingOffer:
      type: object
      properties:
        id: { type: integer }
        cm_number: { type: string, nullable: true }
        offer_date: { type: string, nullable: true, description: 'Y-m-d' }
        cm_title: { type: string, nullable: true }
        cl_partners_book_id: { type: integer, nullable: true }
        cl_partners_branch_id: { type: integer, nullable: true }
        cl_status_id: { type: integer, nullable: true }
        cl_center_id: { type: integer, nullable: true }
        cl_company_branch_id: { type: integer, nullable: true }
        cl_users_id: { type: integer, nullable: true }
        cl_currencies_id: { type: integer, nullable: true }
        currency_rate: { type: number }
        price_s: { type: number }
        price_e2: { type: number }
        price_e2_vat: { type: number }
        validity_date: { type: string, nullable: true, description: 'Y-m-d' }
        req_date: { type: string, nullable: true, description: 'Y-m-d' }
        cl_commission_id: { type: integer, nullable: true, description: 'Zakázka vytvořená z nabídky' }
        cl_invoice_id: { type: integer, nullable: true }
        locked: { type: integer, description: '1 = uzamčeno' }
        created: { type: string, nullable: true }
        changed: { type: string, nullable: true }
    ReportingOfferItem:
      type: object
      properties:
        id: { type: integer }
        cl_offer_id: { type: integer, nullable: true }
        item_order: { type: integer }
        cl_pricelist_id: { type: integer, nullable: true }
        item_label: { type: string, nullable: true }
        quantity: { type: number }
        units: { type: string, nullable: true }
        price_s: { type: number }
        price_e: { type: number }
        price_e_type: { type: integer }
        discount: { type: number }
        price_e2: { type: number }
        vat: { type: number }
        price_e2_vat: { type: number }
        profit: { type: number }
        cl_commission_id: { type: integer, nullable: true }
        created: { type: string, nullable: true }
        changed: { type: string, nullable: true }
    ReportingDeliveryNote:
      type: object
      properties:
        id: { type: integer }
        dn_number: { type: string, nullable: true }
        issue_date: { type: string, nullable: true, description: 'Y-m-d' }
        delivery_date: { type: string, nullable: true, description: 'Y-m-d' }
        due_date: { type: string, nullable: true, description: 'Y-m-d' }
        dn_title: { type: string, nullable: true }
        cl_partners_book_id: { type: integer, nullable: true }
        cl_partners_branch_id: { type: integer, nullable: true }
        cl_status_id: { type: integer, nullable: true }
        cl_center_id: { type: integer, nullable: true }
        cl_storage_id: { type: integer, nullable: true }
        cl_company_branch_id: { type: integer, nullable: true }
        cl_users_id: { type: integer, nullable: true }
        cl_currencies_id: { type: integer, nullable: true }
        currency_rate: { type: number }
        price_e2: { type: number }
        price_e2_vat: { type: number }
        price_payed: { type: number }
        pay_date: { type: string, nullable: true, description: 'Y-m-d' }
        cl_invoice_id: { type: integer, nullable: true }
        cl_store_docs_id: { type: integer, nullable: true }
        storno: { type: integer, description: '1 = stornováno' }
        locked: { type: integer, description: '1 = uzamčeno' }
        created: { type: string, nullable: true }
        changed: { type: string, nullable: true }
    ReportingDeliveryNoteItem:
      type: object
      properties:
        id: { type: integer }
        cl_delivery_note_id: { type: integer, nullable: true }
        item_order: { type: integer }
        cl_pricelist_id: { type: integer, nullable: true }
        item_label: { type: string, nullable: true }
        quantity: { type: number }
        units: { type: string, nullable: true }
        price_s: { type: number }
        price_e: { type: number }
        price_e_type: { type: integer }
        discount: { type: number }
        price_e2: { type: number }
        vat: { type: number }
        price_e2_vat: { type: number }
        cl_storage_id: { type: integer, nullable: true }
        cl_store_move_id: { type: integer, nullable: true }
        cl_invoice_id: { type: integer, nullable: true }
        cl_invoice_items_id: { type: integer, nullable: true }
        created: { type: string, nullable: true }
        changed: { type: string, nullable: true }
    ReportingDeliveryNoteItemBack:
      type: object
      description: 'Vratka dodacího listu — stejný tvar jako položka DL, ale vlastní řada id (viz get-delivery-note-items-back); místo cl_invoice_items_id nese cl_invoice_items_back_id.'
      properties:
        id: { type: integer }
        cl_delivery_note_id: { type: integer, nullable: true }
        item_order: { type: integer }
        cl_pricelist_id: { type: integer, nullable: true }
        item_label: { type: string, nullable: true }
        quantity: { type: number }
        units: { type: string, nullable: true }
        price_s: { type: number }
        price_e: { type: number }
        price_e_type: { type: integer }
        discount: { type: number }
        price_e2: { type: number }
        vat: { type: number }
        price_e2_vat: { type: number }
        cl_storage_id: { type: integer, nullable: true }
        cl_store_move_id: { type: integer, nullable: true }
        cl_invoice_id: { type: integer, nullable: true }
        cl_invoice_items_back_id: { type: integer, nullable: true }
        created: { type: string, nullable: true }
        changed: { type: string, nullable: true }
    ReportingPartner:
      type: object
      description: 'Kontakty (e-mail, telefon, kontaktní osoba), bankovní údaje, komentář a přístupové klíče se záměrně nevracejí.'
      properties:
        id: { type: integer }
        company: { type: string, nullable: true }
        street: { type: string, nullable: true }
        zip: { type: string, nullable: true }
        city: { type: string, nullable: true }
        cl_countries_id: { type: integer, nullable: true }
        ico: { type: string, nullable: true }
        dic: { type: string, nullable: true }
        icdph: { type: string, nullable: true }
        platce_dph: { type: integer, description: '1 = plátce DPH' }
        cl_partners_category_id: { type: integer, nullable: true }
        cl_partners_groups_id: { type: integer, nullable: true }
        cl_regions_id: { type: integer, nullable: true }
        cl_center_id: { type: integer, nullable: true }
        customer: { type: integer, description: '1 = odběratel' }
        supplier: { type: integer, description: '1 = dodavatel' }
        producer: { type: integer, description: '1 = výrobce' }
        active: { type: integer, description: '0 = neaktivní partner; vrací se kvůli historickým dokladům' }
        deleted: { type: integer, description: '1 = smazaný partner; vrací se kvůli historickým dokladům' }
        cl_currencies_id: { type: integer, nullable: true }
        cl_payment_types_id: { type: integer, nullable: true }
        due_date: { type: integer, description: 'Splatnost ve dnech' }
        partner_code: { type: string, nullable: true }
        created: { type: string, nullable: true }
        changed: { type: string, nullable: true }
    ReportingPartnerBranch:
      type: object
      description: 'Kontakty pobočky (b_phone, b_email, b_person) se záměrně nevracejí.'
      properties:
        id: { type: integer }
        cl_partners_book_id: { type: integer, nullable: true }
        b_type: { type: integer }
        b_name: { type: string, nullable: true }
        b_title: { type: string, nullable: true, description: 'Označení pobočky na dokladech (místo názvu, je-li vyplněno)' }
        b_street: { type: string, nullable: true }
        b_city: { type: string, nullable: true }
        b_zip: { type: string, nullable: true }
        cl_countries_id: { type: integer, nullable: true }
        b_ico: { type: string, nullable: true }
        b_dic: { type: string, nullable: true }
        use_as_main: { type: integer, description: '1 = hlavní pobočka' }
        created: { type: string, nullable: true }
        changed: { type: string, nullable: true, description: 'cl_partners_branch.changed je sloupec typu date — vrací se jako Y-m-d 00:00:00' }

    InvoicePaymentSaveResponse:
      type: object
      properties:
        payment: { $ref: '#/components/schemas/InvoicePayment' }
        cash_document: { $ref: '#/components/schemas/InvoiceCashDocumentRef' }
        invoice: { $ref: '#/components/schemas/InvoiceListItem' }

    InvoiceDetail:
      allOf:
        - $ref: '#/components/schemas/InvoiceListItem'
        - type: object
          properties:
            items:
              type: array
              items: { $ref: '#/components/schemas/InvoiceItem' }
            items_back:
              type: array
              items: { $ref: '#/components/schemas/InvoiceItem' }
            payments:
              type: array
              items: { $ref: '#/components/schemas/InvoicePayment' }
            files:
              type: array
              items: { $ref: '#/components/schemas/FileItem' }
            paired_docs:
              type: array
              items: { $ref: '#/components/schemas/InvoicePairedDoc' }
            vat_breakdown:
              type: array
              items: { $ref: '#/components/schemas/InvoiceVatBreakdownItem' }

    InvoicePricelistSearchItem:
      type: object
      description: Řádek výsledku search-pricelist – cena podle partnera, stav skladu a blokace.
      properties:
        id: { type: integer }
        identification: { type: string, nullable: true }
        item_label: { type: string }
        ean_code: { type: string, nullable: true }
        unit: { type: string, nullable: true }
        vat: { type: number }
        price: { type: number }
        price_vat: { type: number }
        price_e2: { type: number }
        price_e2_vat: { type: number }
        discount: { type: number }
        cl_currencies_id: { type: integer, nullable: true }
        quantity_stock: { type: number, description: 'Fyzický stav skladu' }
        blocked: { type: number, description: 'Množství blokované jinými zakázkami (vlastní zakázka řádku/hlavičky se vylučuje)' }
        quantity: { type: number, description: 'Dostupné množství = max(0, quantity_stock - blocked)' }

    InvoiceAvailability:
      type: object
      properties:
        cl_pricelist_id: { type: integer }
        cl_storage_id: { type: integer }
        quantity_stock: { type: number }
        blocked: { type: number }
        quantity: { type: number }

    InvoiceStatus:
      type: object
      description: Stav faktury (cl_status, status_use = invoice). Opravné doklady (jiný status_use) se zde nevrací.
      properties:
        id: { type: integer }
        name: { type: string, description: 'cl_status.status_name' }
        s_new: { type: integer }
        s_fin: { type: integer }
        s_storno: { type: integer }
        s_tax_invoice: { type: integer }
        color_hex: { type: string, nullable: true }

    InvoiceOutgoingType:
      type: object
      description: Typ dokladu faktury vydané (cl_invoice_types, inv_type=1 – jen faktury vydané, ne přijaté/opravné/zálohové).
      properties:
        id: { type: integer }
        name: { type: string }
        inv_type: { type: integer, enum: [1] }
        cl_number_series_id: { type: integer, nullable: true }
        default_type: { type: integer, nullable: true }

    InvoicePaymentType:
      type: object
      description: Typ úhrady (cl_payment_types). Nefiltruje use_for_sale ani not_active, shodně s webem pro fakturu.
      properties:
        id: { type: integer }
        name: { type: string }
        payment_type: { type: string, nullable: true }
        not_active: { type: integer }

    InvoiceCurrency:
      type: object
      properties:
        id: { type: integer }
        currency_code: { type: string, example: 'CZK' }
        fix_rate: { type: number }
        decimal_places: { type: integer }

    InvoiceCenter:
      type: object
      properties:
        id: { type: integer }
        name: { type: string }
        short_desc: { type: string, nullable: true }
        default_center: { type: integer, nullable: true }

    InvoiceVatRate:
      type: object
      description: 'Sazba DPH platná k datu (bez parametru date k dnešku, jinak k datu dokladu). valid_to se záměrně ignoruje, shodně s programem.'
      properties:
        id: { type: integer }
        code_name: { type: string, nullable: true }
        rates: { type: number, example: 21 }
        description: { type: string, nullable: true }

    InvoiceNumberSeries:
      type: object
      description: Číselná řada faktur (form_use = invoice).
      properties:
        id: { type: integer }
        form_name: { type: string }
        formula: { type: string, nullable: true }
        form_default: { type: integer, nullable: true }
        last_number: { type: integer, nullable: true }

    InvoiceBankAccount:
      type: object
      properties:
        id: { type: integer }
        bank_name: { type: string, nullable: true }
        account_number: { type: string, nullable: true }
        bank_code: { type: string, nullable: true }
        iban_code: { type: string, nullable: true }
        swift_code: { type: string, nullable: true }
        default_account: { type: integer, nullable: true }
        cl_currencies_id: { type: integer, nullable: true }

    InvoiceStorage:
      type: object
      description: Sklad (cl_storage), plochý seznam – appka si strom sestaví sama přes cl_storage_id.
      properties:
        id: { type: integer }
        name: { type: string }
        description: { type: string, nullable: true }
        cl_storage_id: { type: integer, nullable: true, description: 'Nadřazený sklad (hierarchie)' }
        public: { type: integer, nullable: true }

    InvoiceCommission:
      type: object
      description: Otevřená zakázka pro přiřazení k faktuře/položce (get-commissions). include_closed=1 vypne filtr otevřenosti.
      properties:
        id: { type: integer }
        cm_number: { type: string }
        cm_title: { type: string, nullable: true }
        storno: { type: integer, description: 'Vrací se v datech, ale NENÍ použit jako filtr otevřenosti (jen 10 řádků v celé DB, viz s_fin/s_storno stavu)' }

    PartnerListItem:
      type: object
      properties:
        id: { type: integer, example: 42 }
        company: { type: string, example: 'ACME s.r.o.' }
        ico: { type: string, example: '12345678' }
        dic: { type: string, nullable: true }
        street: { type: string }
        city: { type: string }
        zip: { type: string }
        email: { type: string }
        phone: { type: string }
        person: { type: string }
        cl_partners_category_id: { type: integer, nullable: true }
        category_name: { type: string, nullable: true }
        supplier: { type: integer, nullable: true }
        customer: { type: integer, nullable: true }
        producer: { type: integer, nullable: true }
        active: { type: integer, nullable: true }
        deleted: { type: integer, example: 0 }
        created: { type: string, nullable: true }
        create_by: { type: string }

    PartnerContact:
      type: object
      properties:
        id: { type: integer }
        cl_partners_book_id: { type: integer }
        cl_partners_branch_id: { type: integer, nullable: true }
        item_order: { type: integer }
        worker_name: { type: string }
        worker_position: { type: string }
        worker_email: { type: string }
        worker_phone: { type: string }

    PartnerBranch:
      type: object
      properties:
        id: { type: integer }
        cl_partners_book_id: { type: integer }
        item_order: { type: integer }
        b_name: { type: string }
        b_street: { type: string }
        b_city: { type: string }
        b_zip: { type: string }
        b_ico: { type: string }
        use_as_main: { type: integer, nullable: true }

    PartnerDetail:
      allOf:
        - $ref: '#/components/schemas/PartnerListItem'
        - type: object
          properties:
            contacts:
              type: array
              items: { $ref: '#/components/schemas/PartnerContact' }
            branches:
              type: array
              items: { $ref: '#/components/schemas/PartnerBranch' }

    TaskMessage:
      type: object
      properties:
        id: { type: integer, example: 5001 }
        cl_task_id: { type: integer, example: 123 }
        cl_chat_id: { type: integer, nullable: true, description: 'ID nadřízené zprávy (vlákno/odpověď)' }
        cl_status_id: { type: integer, nullable: true }
        message: { type: string, example: 'Prosím o upřesnění zadání.' }
        cl_users_id: { type: integer, nullable: true }
        user_name: { type: string, nullable: true }
        sender_name: { type: string, nullable: true }
        sender_email: { type: string, nullable: true }
        sent_partner: { type: integer, example: 0 }
        sent_to_email: { type: string, nullable: true, description: 'E-mail(y) příjemce, na které byla zpráva odeslána klientovi (sent_partner=1); více adres odděleno čárkou a mezerou' }
        sent_at: { type: string, nullable: true, description: 'Čas skutečného odeslání e-mailu klientovi (YYYY-MM-DD HH:MM:SS)' }
        created: { type: string, nullable: true }
        create_by: { type: string }
        changed: { type: string, nullable: true }
        change_by: { type: string, nullable: true }

    DocumentStatus:
      type: object
      nullable: true
      description: Stav dokladu z číselníku stavů; null = doklad stav nemá.
      properties:
        id: { type: integer, example: 12 }
        name: { type: string, example: 'Hotovo' }
        done: { type: boolean, description: 'Stav znamená hotovo (s_fin)' }
        storno: { type: boolean, description: 'Stav znamená storno (s_storno; u DL přijatých i vlastní příznak storno)' }

    StatusListItem:
      type: object
      properties:
        id: { type: integer, example: 12 }
        name: { type: string, example: 'Nová' }
        done: { type: boolean }
        storno: { type: boolean }
        is_new: { type: boolean, description: 'Výchozí stav nového dokladu' }

    OroListItem:
      type: object
      properties:
        id: { type: integer, example: 812 }
        id_oznameni: { type: string, example: 'ORO26100901' }
        oznameni_za_den: { type: string, format: date, example: '2026-10-09' }
        typ_podani: { type: string, example: 'R' }
        odesilatel_dic: { type: string, example: 'CZ12345678' }
        description: { type: string }
        status: { $ref: '#/components/schemas/DocumentStatus' }
        created: { type: string, nullable: true, example: '2026-10-09 10:00:00' }
        changed: { type: string, nullable: true }

    OroItem:
      type: object
      properties:
        id: { type: integer }
        item_order: { type: integer }
        odberatel: { type: string, description: 'DIČ nebo identifikace odběratele' }
        partner_id: { type: integer, nullable: true }
        typ: { type: integer }
        id_vyrobku: { type: string }
        pricelist_id: { type: integer, nullable: true }
        pocet: { type: integer }

    OroProduct:
      type: object
      properties:
        id: { type: integer }
        item_order: { type: integer }
        pricelist_id: { type: integer, nullable: true }
        id_vyrobku: { type: string }
        vyrobce: { type: string }
        nazev: { type: string }
        objem: { type: number, example: 0.7 }
        ean: { type: string }
        sarze: { type: string }
        procento_lihu: { type: number, example: 37.5 }

    OroDetail:
      allOf:
        - $ref: '#/components/schemas/OroListItem'
        - type: object
          properties:
            items: { type: array, items: { $ref: '#/components/schemas/OroItem' } }
            products: { type: array, items: { $ref: '#/components/schemas/OroProduct' } }

    CocczListItem:
      type: object
      properties:
        id: { type: integer, example: 41 }
        export_date: { type: string, format: date, example: '2026-10-05' }
        producer_id: { type: string }
        producer_name: { type: string }
        branch_id: { type: string, description: 'Kód pobočky distributora u Coca-Coly (ne pobočka firmy)' }
        generated_at: { type: string, nullable: true, example: '2026-10-05 06:00:00' }
        sent_at: { type: string, nullable: true }
        error_text: { type: string }
        description: { type: string }
        status: { $ref: '#/components/schemas/DocumentStatus' }
        items_count: { type: integer, example: 18450 }
        created: { type: string, nullable: true }
        changed: { type: string, nullable: true }

    CocczDetail:
      allOf:
        - $ref: '#/components/schemas/CocczListItem'
        - type: object
          properties:
            summary:
              type: object
              properties:
                rows_count: { type: integer }
                quantity: { type: number }
                quantity_vol: { type: number }
                customers: { type: integer, description: 'Počet různých customer_id' }
                products: { type: integer, description: 'Počet různých product_id' }

    CocczItem:
      type: object
      properties:
        id: { type: integer }
        item_order: { type: integer }
        item_no: { type: integer }
        source_doc_type: { type: string }
        source_doc_id: { type: integer, nullable: true }
        partner_id: { type: integer, nullable: true }
        customer_id: { type: string }
        type_sale_id: { type: string }
        pricelist_id: { type: integer, nullable: true }
        product_id: { type: string }
        measure_unit_id: { type: string }
        quantity: { type: number }
        quantity_vol: { type: number }

    TransportListItem:
      type: object
      properties:
        id: { type: integer, example: 310 }
        tn_number: { type: string, example: 'DP2600310' }
        transport_date: { type: string, format: date, example: '2026-10-10' }
        transport_end_date: { type: string, format: date, nullable: true }
        transport_type_id: { type: integer, nullable: true }
        transport_type_name: { type: string, nullable: true }
        branch_id: { type: integer, nullable: true }
        currency_code: { type: string, nullable: true, example: 'CZK' }
        given_cash: { type: number, description: 'Hotovost vydaná řidiči' }
        recieved_cash: { type: number, description: 'Hotovost přijatá od řidiče' }
        description: { type: string }
        status: { $ref: '#/components/schemas/DocumentStatus' }
        docs_count: { type: integer, description: 'Počet dodacích listů v jízdě' }
        created: { type: string, nullable: true }
        changed: { type: string, nullable: true }

    TransportDoc:
      type: object
      properties:
        id: { type: integer }
        item_order: { type: integer }
        delivery_note_id: { type: integer, nullable: true }
        dn_number: { type: string, nullable: true }
        partner_id: { type: integer, nullable: true }
        partner_name: { type: string, nullable: true }
        delivered: { type: boolean }
        payed: { type: boolean }
        only_for_pay: { type: boolean, description: 'DL jen k vybrání platby, nevozí se' }
        main_dn: { type: boolean }
        price_payed: { type: number }
        price_e2_vat_back: { type: number, description: 'Vratky s DPH' }
        package_count: { type: integer }
        package_type: { type: integer }
        package_descr: { type: string }
        weight: { type: number, description: 'kg' }
        note: { type: string }
        cash_id: { type: integer, nullable: true, description: 'Pokladní doklad úhrady' }

    TransportDetail:
      allOf:
        - $ref: '#/components/schemas/TransportListItem'
        - type: object
          properties:
            docs: { type: array, items: { $ref: '#/components/schemas/TransportDoc' } }

    DeliveryListItem:
      type: object
      properties:
        id: { type: integer, example: 77 }
        de_number: { type: string, example: 'DO2600077' }
        delivery_date: { type: string, format: date }
        partner_id: { type: integer, nullable: true }
        partner_name: { type: string, nullable: true, description: 'Název dodavatele z dodávky, jinak z adresáře' }
        delivery_note_in_id: { type: integer, nullable: true, description: 'Navázaný DL přijatý' }
        done: { type: boolean }
        description: { type: string }
        status: { $ref: '#/components/schemas/DocumentStatus' }
        items_count: { type: integer }
        created: { type: string, nullable: true }
        changed: { type: string, nullable: true }

    DeliveryItem:
      type: object
      properties:
        id: { type: integer }
        item_order: { type: integer }
        pricelist_id: { type: integer, nullable: true }
        identification: { type: string, nullable: true, description: 'Kód položky ceníku' }
        item_label: { type: string }
        quantity: { type: number, description: 'Objednané/dodané množství' }
        quantity_checked: { type: number, description: 'Zkontrolované množství' }
        difference: { type: number, description: 'quantity_checked − quantity' }
        units: { type: string }
        exp_date: { type: string, format: date, nullable: true }

    DeliveryDetail:
      allOf:
        - $ref: '#/components/schemas/DeliveryListItem'
        - type: object
          properties:
            items: { type: array, items: { $ref: '#/components/schemas/DeliveryItem' } }

    DeliveryNoteInListItem:
      type: object
      properties:
        id: { type: integer, example: 3015 }
        dn_number: { type: string, example: 'DLP3015-26' }
        rdn_number: { type: string, description: 'Číslo DL dodavatele' }
        rinv_number: { type: string, description: 'Číslo faktury dodavatele' }
        od_number: { type: string, description: 'Číslo objednávky' }
        dn_title: { type: string }
        issue_date: { type: string, format: date }
        delivery_date: { type: string, format: date, nullable: true }
        due_date: { type: string, format: date, nullable: true }
        partner_id: { type: integer, nullable: true }
        partner_name: { type: string, nullable: true }
        currency_code: { type: string, nullable: true }
        currency_rate: { type: number }
        price_e2: { type: number, description: 'Celkem bez DPH v měně dokladu' }
        price_e2_vat: { type: number, description: 'Celkem s DPH v měně dokladu' }
        price_payed: { type: number }
        pay_date: { type: string, format: date, nullable: true }
        storage_id: { type: integer, nullable: true }
        store_docs_id_in: { type: integer, nullable: true, description: 'Příjemka' }
        invoice_arrived_id: { type: integer, nullable: true, description: 'Faktura přijatá' }
        storno: { type: boolean }
        locked: { type: boolean }
        status: { $ref: '#/components/schemas/DocumentStatus' }
        created: { type: string, nullable: true }
        changed: { type: string, nullable: true }

    DeliveryNoteInItem:
      type: object
      description: Nákupní ceny (price_in, price_in_vat, price_s, profit) jen uživateli s právem vidět nákupní ceny; jinak klíče chybí.
      properties:
        id: { type: integer }
        item_order: { type: integer }
        pricelist_id: { type: integer, nullable: true }
        item_label: { type: string }
        quantity: { type: number }
        units: { type: string }
        price_e: { type: number, description: 'Cena za jednotku' }
        price_e2: { type: number, description: 'Řádek bez DPH' }
        vat: { type: number, description: 'Sazba DPH %' }
        price_e2_vat: { type: number, description: 'Řádek s DPH' }
        discount: { type: number }
        batch: { type: string }
        exp_date: { type: string, format: date, nullable: true }
        storage_id: { type: integer, nullable: true }
        store_done: { type: boolean, description: 'Naskladněno' }
        order_number: { type: string }
        price_in: { type: number }
        price_in_vat: { type: number }
        price_s: { type: number }
        profit: { type: number }

    DeliveryNoteInItemBack:
      type: object
      properties:
        id: { type: integer }
        item_order: { type: integer }
        pricelist_id: { type: integer, nullable: true }
        item_label: { type: string }
        quantity: { type: number }
        units: { type: string }
        price_e2: { type: number }
        vat: { type: number }
        price_e2_vat: { type: number }
        store_done: { type: boolean }

    VatBreakdown:
      type: object
      properties:
        price_base0: { type: number }
        price_base1: { type: number }
        price_base2: { type: number }
        price_base3: { type: number }
        price_vat1: { type: number }
        price_vat2: { type: number }
        price_vat3: { type: number }
        vat1: { type: number, description: 'Sazba 1 v %' }
        vat2: { type: number }
        vat3: { type: number }

    DeliveryNoteInDetail:
      allOf:
        - $ref: '#/components/schemas/DeliveryNoteInListItem'
        - $ref: '#/components/schemas/VatBreakdown'
        - type: object
          properties:
            items: { type: array, items: { $ref: '#/components/schemas/DeliveryNoteInItem' } }
            items_back: { type: array, items: { $ref: '#/components/schemas/DeliveryNoteInItemBack' } }

    SaleListItem:
      type: object
      properties:
        id: { type: integer, example: 52011 }
        sale_number: { type: string, example: 'P2652011' }
        sale_type: { type: string, enum: [sale, correction] }
        inv_date: { type: string, format: date }
        vat_date: { type: string, format: date, nullable: true }
        partner_id: { type: integer, nullable: true }
        partner_name: { type: string, nullable: true }
        branch_id: { type: integer, nullable: true }
        storage_id: { type: integer, nullable: true }
        payment_type_id: { type: integer, nullable: true }
        payment_type_name: { type: string, nullable: true }
        cash_id: { type: integer, nullable: true, description: 'Pokladna (definice pokladny) – stejná hodnota jako filtr cash_id' }
        cash_document_id: { type: integer, nullable: true, description: 'Pokladní doklad prodejky' }
        currency_code: { type: string, nullable: true }
        currency_rate: { type: number }
        price_e2: { type: number }
        price_e2_vat: { type: number }
        price_payed: { type: number }
        pay_date: { type: string, format: date, nullable: true }
        var_symb: { type: string }
        correction_sale_id: { type: integer, nullable: true, description: 'Opravovaná prodejka (u correction)' }
        eet_id: { type: integer, nullable: true }
        status: { $ref: '#/components/schemas/DocumentStatus' }
        created: { type: string, nullable: true }
        changed: { type: string, nullable: true }

    SaleItem:
      type: object
      description: price_s (nákupní cena) jen uživateli s právem vidět nákupní ceny; jinak klíč chybí.
      properties:
        id: { type: integer }
        item_order: { type: integer }
        pricelist_id: { type: integer, nullable: true }
        item_label: { type: string }
        quantity: { type: number }
        quantity_back: { type: number, description: 'Vráceno opravnou prodejkou' }
        units: { type: string }
        price_e: { type: number }
        price_e2: { type: number }
        vat: { type: number }
        price_e2_vat: { type: number }
        discount: { type: number }
        storage_id: { type: integer, nullable: true }
        note: { type: string }
        price_s: { type: number }

    SalePayment:
      type: object
      properties:
        id: { type: integer }
        pay_date: { type: string, format: date, nullable: true }
        pay_price: { type: number }
        payment_type_id: { type: integer, nullable: true }
        payment_type_name: { type: string, nullable: true }
        currency_code: { type: string, nullable: true }
        pay_doc: { type: string }
        pay_type: { type: integer }

    SaleDetail:
      allOf:
        - $ref: '#/components/schemas/SaleListItem'
        - $ref: '#/components/schemas/VatBreakdown'
        - type: object
          properties:
            items: { type: array, items: { $ref: '#/components/schemas/SaleItem' } }
            payments: { type: array, items: { $ref: '#/components/schemas/SalePayment' } }

    PurchaseOrderListItem:
      type: object
      properties:
        id: { type: integer, example: 902 }
        od_number: { type: string, example: 'OBJ2600902' }
        od_title: { type: string }
        od_date: { type: string, format: date }
        req_date: { type: string, format: date, nullable: true, description: 'Požadované datum dodání' }
        rea_date: { type: string, format: date, nullable: true, description: 'Skutečné datum dodání' }
        partner_id: { type: integer, nullable: true }
        partner_name: { type: string, nullable: true }
        branch_id: { type: integer, nullable: true }
        storage_id: { type: integer, nullable: true }
        currency_code: { type: string, nullable: true }
        currency_rate: { type: number }
        price_e2: { type: number }
        price_e2_vat: { type: number }
        price_e2_rcv: { type: number, description: 'Přijato bez DPH' }
        price_e2_vat_rcv: { type: number, description: 'Přijato s DPH' }
        on_store: { type: boolean, description: 'Naskladněno' }
        store_docs_id_in: { type: integer, nullable: true, description: 'Příjemka' }
        status: { $ref: '#/components/schemas/DocumentStatus' }
        created: { type: string, nullable: true }
        changed: { type: string, nullable: true }

    PurchaseOrderItem:
      type: object
      description: price_s (nákupní cena) jen uživateli s právem vidět nákupní ceny; jinak klíč chybí.
      properties:
        id: { type: integer }
        item_order: { type: integer }
        pricelist_id: { type: integer, nullable: true }
        item_label: { type: string }
        quantity: { type: number }
        quantity_rcv: { type: number, description: 'Přijaté množství' }
        units: { type: string }
        rea_date: { type: string, format: date, nullable: true }
        price_e: { type: number }
        price_e2: { type: number }
        vat: { type: number }
        price_e2_vat: { type: number }
        note: { type: string }
        storage_id: { type: integer, nullable: true }
        price_s: { type: number }

    PurchaseOrderDetail:
      allOf:
        - $ref: '#/components/schemas/PurchaseOrderListItem'
        - type: object
          properties:
            delivery_place: { type: string }
            delivery_method: { type: string }
            description: { type: string }
            items: { type: array, items: { $ref: '#/components/schemas/PurchaseOrderItem' } }

  requestBodies:
    ReadOneRequest:
      description: Ověření; dataJSON se neposílá.
      content:
        application/x-www-form-urlencoded:
          schema:
            type: object
            properties:
              api_token: { type: string, description: 'Integrační token (alternativa k bearer_token)' }
              bearer_token: { type: string, description: 'Token z /homepage/login' }
    ReadDeletedRequest:
      content:
        application/x-www-form-urlencoded:
          schema:
            type: object
            properties:
              api_token: { type: string }
              bearer_token: { type: string }
              dataJSON:
                type: string
                description: 'JSON: since (povinné, YYYY-MM-DD HH:MM:SS), limit (1–500, výchozí 50), offset'
                example: '{"since":"2026-10-01 00:00:00","limit":500}'

  responses:
    ReadBadRequest:
      description: Neplatný vstup – dataJSON není platný JSON nebo objekt, neplatné datum, neznámá hodnota výčtu, date_from po date_to.
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    ReadUnauthorized:
      description: Chybí nebo je neplatný bearer_token / api_token (problem+json).
    ReadForbidden:
      description: Bez práva read k modulu, modul není v licenci firmy nebo není uživateli přidělený, přihlášení B2B účtem, nebo volání se sync_tokenem (neurčuje uživatele).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    ReadNotFound:
      description: Doklad neexistuje, patří jiné firmě, nebo je mimo rozsah uživatele (jen vlastní záznamy, pobočka).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }
    ReadServerError:
      description: Chyba serveru (zalogováno, kanál api).
      content:
        application/json:
          schema: { $ref: '#/components/schemas/ErrorResponse' }

security:
  - bearerToken: []

paths:
  /homepage/login:
    post:
      tags: [Authentication]
      summary: Login and obtain bearer token
      security: []
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [username, password]
              properties:
                username:
                  type: string
                  description: User email
                password:
                  type: string
                  format: password
      responses:
        '200':
          description: Login successful
          content:
            application/json:
              schema:
                type: object
                properties:
                  Token:
                    type: string
                    description: Bearer token for subsequent requests
                  Role:
                    type: string
                    enum: [user, b2b]
                  cl_users_id:
                    type: integer
                  Company:
                    type: string
                  Username:
                    type: string

  /homepage/get-modules:
    post:
      tags: [Mobile App]
      summary: Mobile app modules visible to a user
      security: []
      description: |
        Returns which mobile app (Trynx-Info) modules are enabled for the given user.
        Configured in Trynx on the user card, "Mobilní aplikace" tab
        (cl_users.mobile_modules). Authenticated by the long-lived company `sync_token`.

        Without `cl_users_id`, or when the user does not belong to the sync token's
        company or never saved the tab, **all modules are returned** (default = everything on).
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [sync_token]
              properties:
                sync_token:
                  type: string
                  description: Long-lived company sync token (scanned from a QR code in Trynx)
                cl_users_id:
                  type: integer
                  description: User whose module visibility to return; may be sent as a plain param or inside dataJSON
                  example: 17
                dataJSON:
                  type: string
                  description: |
                    Alternative to the plain `cl_users_id` param — JSON-encoded parameters:
                    - `cl_users_id` (int) - user whose module visibility to return
                  example: '{"cl_users_id": 17}'
      responses:
        '200':
          description: Enabled modules
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      modules:
                        type: array
                        description: Keys of modules the app should show
                        items:
                          type: string
                          enum: [info, tasks, doklady]
                        example: [info, tasks, doklady]

  /devices/register:
    post:
      tags: [Mobile App]
      summary: Register mobile device for push notifications
      description: |
        Upsert registrace zařízení (unikátní device_id × firma). Appka volá po přihlášení,
        přidání firmy nebo změně nastavení notifikací. Server ukládá client_profile_id
        a vrací ho v datech notifikace - appka podle něj při tapu přepne profil/firmu.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [sync_token, dataJSON]
              properties:
                sync_token:
                  type: string
                  description: Long-lived company sync token
                dataJSON:
                  type: string
                  description: |
                    JSON:
                    - `device_id` (string, required) - stabilní id instalace
                    - `client_profile_id` (string) - lokální id profilu v appce
                    - `platform` (string, required) - `ios` | `android`
                    - `expo_push_token` (string, required) - `ExponentPushToken[...]`
                    - `cl_users_id` (int, required) - uživatel, musí být aktivní člen firmy
                    - `notify_messages`, `notify_status`, `notify_plan`, `notify_summary` (0|1, default 1)
                  example: '{"device_id":"a1b2","client_profile_id":"p17","platform":"ios","expo_push_token":"ExponentPushToken[xyz]","cl_users_id":17,"notify_messages":1,"notify_status":1,"notify_plan":1,"notify_summary":1}'
      responses:
        '200':
          description: Registered / updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: object
                    properties:
                      id: { type: integer, description: cl_devices row ID }
        '401':
          description: Invalid sync token
        '403':
          description: cl_users_id is not an active member of the company

  /devices/unregister:
    post:
      tags: [Mobile App]
      summary: Unregister mobile device
      description: Smaže registraci zařízení pro firmu danou sync_tokenem (odhlášení / odebrání firmy).
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [sync_token, dataJSON]
              properties:
                sync_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: `device_id` (string, required)'
                  example: '{"device_id":"a1b2"}'
      responses:
        '200':
          description: Unregistered (idempotent)
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }

  /tasks/get-all:
    post:
      tags: [Tasks]
      summary: List tasks
      description: |
        Returns a paginated list of tasks with optional filters.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    JSON-encoded filter parameters:
                    - `cl_partners_book_id` (int) - filter by client
                    - `cl_status_id` (int) - filter by status
                    - `exclude_closed` (1) - hide tasks whose status is final or cancelled (cl_status.s_fin = 1 OR s_storno = 1); tasks without a status stay visible
                    - `cl_users_id` (int) - filter by assigned worker
                    - `cl_project_id` (int) - filter by project
                    - `cl_task_category_id` (int) - filter by category
                    - `finished` (0|1) - filter by completion
                    - `payment` (0|1) - filter by paid flag
                    - `invoice` (0|1) - filter by invoiced flag
                    - `unassigned` (1) - only tasks without a main worker (cl_users_id IS NULL)
                    - `changed_since` (string, YYYY-MM-DD HH:MM:SS) - "live tasks": task created/changed or any of its chat messages created/changed since the given moment
                    - `search` (string) - fulltext in task_number, description, description2, ai_summary and chat messages
                    - `date_from` (string, YYYY-MM-DD) - tasks from date
                    - `date_to` (string, YYYY-MM-DD) - tasks to date
                    - `worker_user_id` (int) - only tasks having a worker record (cl_task_workers) for the given user; combine with planned_date_from/to for the "Today" screen
                    - `planned_date_from` (string, YYYY-MM-DD) - only tasks whose worker plan interval work_start-work_end overlaps the given range; NULL work_end = single day of work_start (DATE(work_start) <= planned_date_to AND COALESCE(DATE(work_end), DATE(work_start)) >= planned_date_from)
                    - `planned_date_to` (string, YYYY-MM-DD) - see planned_date_from
                    - `unread_user_id` (int) - vrátí `unread_count` (počet nepřečtených cizích zpráv) pro daného uživatele v každé položce
                    - `limit` (int, default 50) - page size
                    - `offset` (int, default 0) - pagination offset
                  example: '{"worker_user_id": 17, "planned_date_from": "2026-07-08", "planned_date_to": "2026-07-08"}'
      responses:
        '200':
          description: List of tasks
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TaskListItem'
                  total:
                    type: integer
                    description: Total number of matching tasks (before pagination)
                    example: 156
        '401':
          description: Unauthorized
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/get-one/{id}:
    post:
      tags: [Tasks]
      summary: Get task detail
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Task ID
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    Optional JSON string:
                    - `unread_user_id` (int) - vrátí `unread_count` (počet nepřečtených cizích zpráv) pro daného uživatele; stejná sémantika jako v get-all, bez parametru se klíč nevrací
                  example: '{"unread_user_id": 89}'
      responses:
        '200':
          description: Task detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    $ref: '#/components/schemas/TaskListItem'
        '404':
          description: Task not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/create:
    post:
      tags: [Tasks]
      summary: Create new task
      description: |
        Creates a new task. Task number is auto-generated from number series.
        AI summary is generated automatically from description.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    JSON-encoded task data:
                    - `description` (string, **required**) - task description (HTML)
                    - `description2` (string) - developer notes
                    - `task_date` (string, YYYY-MM-DD) - task date, defaults to today
                    - `cl_task_category_id` (int) - category ID
                    - `cl_status_id` (int) - status ID
                    - `cl_partners_book_id` (int) - client ID
                    - `cl_partners_branch_id` (int) - client branch ID
                    - `cl_partners_book_workers_id` (int) - client contact ID
                    - `cl_project_id` (int) - project ID
                    - `cl_users_id` (int) - main worker ID
                    - `cl_users2_id` (int) - controller ID
                    - `cl_users3_id` (int) - communicator ID
                    - `priority` (string) - priority (1-9)
                    - `version` (string) - version label
                    - `payment` (int) - 0=unpaid, 1=paid
                    - `use_fund` (bool) - use helpdesk fund
                    - `time_fund` (number) - fund time in hours
                    - `target_date` (string, YYYY-MM-DD) - customer deadline
                    - custom fields: `cust_descr1_sm` … `cust_descr6_sm`, `cust_descr1_bg`, `cust_descr2_bg`, `cust_num1` … `cust_num4`, `cust_option1`
                  example: '{"description": "<p>Fix login bug</p>", "cl_task_category_id": 3, "cl_status_id": 1, "priority": "1"}'
      responses:
        '200':
          description: Task created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 456
                      task_number:
                        type: string
                        example: 'T-0456'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                missingDescription:
                  value: { status: error, message: 'Description is required' }
                invalidCategory:
                  value: { status: error, message: 'Invalid category' }
                invalidStatus:
                  value: { status: error, message: 'Invalid status' }

  /tasks/update:
    post:
      tags: [Tasks]
      summary: Update existing task
      description: |
        Updates an existing task. Only provided fields are updated (partial update).
        If description changes, AI summary is regenerated automatically.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    JSON-encoded update data:
                    - `id` (int, **required**) - task ID to update
                    - Any field from create can be updated individually (incl. custom `cust_*` fields)
                    - Date fields: `task_date`, `target_date`, `end_date`, `date_end2`, `date_end3`, `date_check`
                    - Boolean fields: `finished`, `checked`, `invoice`, `use_fund`
                    - `change_by` (string) - defaults to "API"

                    Setting `finished` to 1 on a task whose status is not final automatically
                    switches the status to the one flagged `s_fin = 1` (same as the edit form in the UI).
                  example: '{"id": 456, "cl_status_id": 5, "finished": true}'
      responses:
        '200':
          description: Task updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 456
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Task not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/delete/{id}:
    post:
      tags: [Tasks]
      summary: Delete task
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Task ID to delete
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: Task deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '404':
          description: Task not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/get-statuses:
    post:
      tags: [Task Statuses]
      summary: List task statuses
      description: Returns all statuses with status_use = 'task', ordered by item_order.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: List of statuses
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TaskStatus'

  /tasks/get-status/{id}:
    post:
      tags: [Task Statuses]
      summary: Get status detail
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: Status detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    $ref: '#/components/schemas/TaskStatus'
        '404':
          description: Status not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/create-status:
    post:
      tags: [Task Statuses]
      summary: Create new task status
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    JSON-encoded status data:
                    - `status_name` (string, **required**)
                    - `description` (string, default "")
                    - `item_order` (int, default 0) - display order
                    - `color_hex` (string, default "#FFFFFF") - background color
                    - `color_ink_hex` (string, nullable) - text color
                  example: '{"status_name": "New status", "item_order": 5, "color_hex": "#FF9900"}'
      responses:
        '200':
          description: Status created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/update-status:
    post:
      tags: [Task Statuses]
      summary: Update existing task status
      description: Partial update - only provided fields are changed.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    JSON-encoded update data:
                    - `id` (int, **required**)
                    - `status_name` (string)
                    - `description` (string)
                    - `item_order` (int)
                    - `color_hex` (string)
                    - `color_ink_hex` (string)
                  example: '{"id": 5, "status_name": "Updated name"}'
      responses:
        '200':
          description: Status updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
        '404':
          description: Status not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/delete-status/{id}:
    post:
      tags: [Task Statuses]
      summary: Delete task status
      description: Fails if status is used by any task (409 Conflict).
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: Status deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '404':
          description: Status not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Status is in use
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/get-categories:
    post:
      tags: [Task Categories]
      summary: List task categories
      description: Returns all task categories ordered by label.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: List of categories
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TaskCategory'

  /tasks/get-category/{id}:
    post:
      tags: [Task Categories]
      summary: Get category detail
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: Category detail (includes audit fields)
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    allOf:
                      - $ref: '#/components/schemas/TaskCategory'
                      - type: object
                        properties:
                          created:
                            type: string
                            nullable: true
                          create_by:
                            type: string
                          changed:
                            type: string
                            nullable: true
                          change_by:
                            type: string
                            nullable: true
        '404':
          description: Category not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/create-category:
    post:
      tags: [Task Categories]
      summary: Create new task category
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    JSON-encoded category data:
                    - `label` (string, **required**) - category name
                  example: '{"label": "New category"}'
      responses:
        '200':
          description: Category created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/update-category:
    post:
      tags: [Task Categories]
      summary: Update existing task category
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    JSON-encoded update data:
                    - `id` (int, **required**)
                    - `label` (string)
                  example: '{"id": 3, "label": "Updated name"}'
      responses:
        '200':
          description: Category updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
        '404':
          description: Category not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/delete-category/{id}:
    post:
      tags: [Task Categories]
      summary: Delete task category
      description: Fails if category is used by any task (409 Conflict).
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: Category deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '404':
          description: Category not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Category is in use
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/get-workers/{id}:
    post:
      tags: [Task Workers]
      summary: List task workers
      description: Returns all worker records of the task, ordered by work_start descending.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Task ID
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: List of workers
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TaskWorker'
        '404':
          description: Task not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/create-worker:
    post:
      tags: [Task Workers]
      summary: Add worker to task
      description: |
        Creates a worker record. When `final_email` is empty, it is auto-filled
        from the user account e-mail (same as in the UI).
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    JSON-encoded worker data:
                    - `cl_task_id` (int, **required**) - task ID
                    - `cl_users_id` (int, **required**) - user ID (must belong to the company)
                    - `work_start` (string, datetime) - defaults to now
                    - `work_end` (string, datetime)
                    - `work_time` (number) - worked hours, default 0
                    - `work_summary` (string)
                    - `work_description` (string)
                    - `work_location` (string)
                    - `work_finished` (0|1)
                    - `final_email` (string) - notification e-mail
                  example: '{"cl_task_id": 123, "cl_users_id": 7, "work_start": "2026-04-04 08:00:00", "work_time": 2.5}'
      responses:
        '200':
          description: Worker created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
        '400':
          description: Validation error (missing fields, invalid user)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Task not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/update-worker:
    post:
      tags: [Task Workers]
      summary: Update task worker
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    JSON-encoded update data:
                    - `id` (int, **required**) - worker record ID
                    - Any field from create-worker can be updated individually
                  example: '{"id": 456, "work_time": 3, "work_finished": 1}'
      responses:
        '200':
          description: Worker updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Worker not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/delete-worker/{id}:
    post:
      tags: [Task Workers]
      summary: Delete task worker
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Worker record ID
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: Worker deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '404':
          description: Worker not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/get-work-time-remaining:
    post:
      tags: [Task Workers]
      summary: Remaining monthly work fund of a user
      description: |
        Returns the remaining monthly work fund in hours for the given user and month:
        working days × user work fund − hours already logged in cl_task_workers − monthly operative.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    - `cl_users_id` (int, **required**) - user ID
                    - `date` (string, YYYY-MM-DD) - any day of the month, defaults to today
                  example: '{"cl_users_id": 7, "date": "2026-04-15"}'
      responses:
        '200':
          description: Remaining fund
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      cl_users_id:
                        type: integer
                      date:
                        type: string
                      work_time_remaining:
                        type: number
                        description: Remaining hours (can be negative)
                        example: 42.5
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/get-work-events/{id}:
    post:
      tags: [Task Work Events]
      summary: List helpdesk work events of a task
      description: |
        Returns child cl_partners_event records under the task's parent helpdesk event,
        ordered by date. The task must have a linked helpdesk event (cl_partners_event_id).
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Task ID
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: List of work events
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TaskWorkEvent'
        '404':
          description: Task or linked helpdesk event not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Task has no linked helpdesk event
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/create-work-event:
    post:
      tags: [Task Work Events]
      summary: Add helpdesk work record to task
      description: |
        Creates a child work event under the task's parent helpdesk event. `work_time`
        is computed as hours×60+minutes, partner/branch/contact are inherited from
        the parent event, and the parent work_time/date_to/finished are recalculated.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    JSON-encoded work event data:
                    - `cl_task_id` (int, **required**) - task ID (must have a linked helpdesk event)
                    - `date` (string, datetime) - defaults to now
                    - `date_to` (string, datetime)
                    - `cl_users_id` (int) - author (must belong to the company)
                    - `cl_partners_event_type_id` (int) - event type (see get-event-types)
                    - `cl_partners_event_method_id` (int) - event method (see get-event-methods)
                    - `work_time_hours` (int) - default 0
                    - `work_time_minutes` (int) - default 0
                    - `public` (0|1) - visible to customer, default 0
                    - `finished` (0|1) - default 0; only finished records count into the parent work_time
                    - `description` (string)
                    - `add_text` (string) - material and other costs
                    - `work_label` (string)
                  example: '{"cl_task_id": 123, "cl_users_id": 7, "work_time_hours": 1, "work_time_minutes": 15, "finished": 1, "description": "Konzultace"}'
      responses:
        '200':
          description: Work event created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
        '400':
          description: Validation error (missing task, invalid user/type/method)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Task not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Task has no linked helpdesk event
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/update-work-event:
    post:
      tags: [Task Work Events]
      summary: Update helpdesk work record
      description: |
        Updates a child work event. When hours/minutes change, `work_time` is recomputed
        and the parent event totals are recalculated.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    JSON-encoded update data:
                    - `id` (int, **required**) - work event ID
                    - Any field from create-work-event can be updated individually
                  example: '{"id": 13374, "work_time_minutes": 45, "finished": 1}'
      responses:
        '200':
          description: Work event updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Work event not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/delete-work-event/{id}:
    post:
      tags: [Task Work Events]
      summary: Delete helpdesk work record
      description: Deletes a child work event and recalculates the parent event totals.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Work event ID
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: Work event deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '404':
          description: Work event not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/get-files/{id}:
    post:
      tags: [Task Files]
      summary: List task files
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Task ID
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: List of file metadata
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TaskFile'
        '404':
          description: Task not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/upload-file:
    post:
      tags: [Task Files]
      summary: Upload file to task
      description: |
        Stores a base64-encoded file into the company data folder and links it to the task.
        On file name collision the stored name gets a numeric suffix (`report.pdf` → `report-1.pdf`).
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    JSON-encoded file data:
                    - `cl_task_id` (int, **required**) - task ID
                    - `file_name` (string, **required**) - file name (path components are stripped)
                    - `content` (string) - plain base64 content, **required** unless dataUrl is given
                    - `dataUrl` (string) - alternative to content, data URL format (`data:...;base64,...`)
                    - `mime_type` (string) - defaults to application/octet-stream
                    - `label_name` (string) - display name, defaults to file_name
                    - `description` (string)
                  example: '{"cl_task_id": 123, "file_name": "zadani.pdf", "mime_type": "application/pdf", "content": "JVBERi0xLjQ..."}'
      responses:
        '200':
          description: File stored
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                      file_name:
                        type: string
                        description: Actually stored file name (after collision rename)
        '400':
          description: Validation error (missing fields, invalid base64)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Task not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/download-file/{id}:
    post:
      tags: [Task Files]
      summary: Download task file
      description: Returns the file content base64-encoded together with its metadata.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: File ID (cl_files)
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: File content
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    allOf:
                      - $ref: '#/components/schemas/TaskFile'
                      - type: object
                        properties:
                          content:
                            type: string
                            description: Base64-encoded file content
        '404':
          description: File not found (or missing on the server)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/delete-file/{id}:
    post:
      tags: [Task Files]
      summary: Delete task file
      description: Deletes both the database record and the physical file on the server.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: File ID (cl_files)
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: File deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '404':
          description: File not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/get-related-tasks/{id}:
    post:
      tags: [Task Relations]
      summary: List related tasks
      description: Returns tasks linked to the given task (cl_task_tasks where the task is the parent).
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Task ID
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: List of linked tasks
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TaskRelatedTask'
        '404':
          description: Task not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/link-task:
    post:
      tags: [Task Relations]
      summary: Link two tasks
      description: |
        Creates a relation between two tasks. Idempotent – an existing link is returned
        instead of duplicating. With `bidirectional: 1` the reverse link is created too
        (same as creating a subtask in the UI).
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    - `cl_task_id_parent` (int, **required**) - parent task ID
                    - `cl_task_id` (int, **required**) - linked task ID
                    - `bidirectional` (0|1) - also create the reverse link, default 0
                  example: '{"cl_task_id_parent": 123, "cl_task_id": 456, "bidirectional": 1}'
      responses:
        '200':
          description: Link(s) created or already existing
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      link_ids:
                        type: array
                        items:
                          type: integer
                        example: [128, 129]
        '400':
          description: Validation error (missing IDs, self-link)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: One of the tasks not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/unlink-task/{id}:
    post:
      tags: [Task Relations]
      summary: Remove task link
      description: Deletes one cl_task_tasks row by its ID (see link_id in get-related-tasks).
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Link ID (cl_task_tasks)
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: Link removed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '404':
          description: Link not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/copy-task/{id}:
    post:
      tags: [Task Relations]
      summary: Copy task
      description: |
        Creates a copy of the task with a new number from the number series and the status
        flagged `s_new = 1` (or none). Both tasks are linked together in both directions,
        same as the copy/subtask action in the UI.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Source task ID
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: Task copied
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        description: New task ID
                      task_number:
                        type: string
                        example: 'T0407-26'
        '404':
          description: Task not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/get-forms-versions/{id}:
    post:
      tags: [Task Forms]
      summary: List Forms 2 versions of a task
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Task ID
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: List of versions
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TaskFormsVersion'
        '404':
          description: Task not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/create-forms-version:
    post:
      tags: [Task Forms]
      summary: Create Forms 2 version
      description: |
        Creates a new form version. With `source_version_id` the given version is copied,
        otherwise the latest version of the task is copied. When the task has no version yet,
        the fields are created from the task category template (the task must have a category
        with a non-empty template).
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    - `cl_task_id` (int, **required**) - task ID
                    - `source_version_id` (int) - version to copy, defaults to the latest one
                    - `label` (string) - version label
                  example: '{"cl_task_id": 123, "label": "Nabídka v2"}'
      responses:
        '200':
          description: Version created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                      version_number:
                        type: integer
                      fields_count:
                        type: integer
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Task or source version not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Task has no category or the category has no form template
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/delete-forms-version/{id}:
    post:
      tags: [Task Forms]
      summary: Delete Forms 2 version
      description: Deletes the version including its field rows (FK cascade).
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Version ID
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: Version deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '404':
          description: Version not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/get-forms-values/{id}:
    post:
      tags: [Task Forms]
      summary: List form fields of a version
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Version ID
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: List of form fields with values
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TaskFormField'
        '404':
          description: Version not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/update-form-value:
    post:
      tags: [Task Forms]
      summary: Update form field value
      description: |
        Writes the value into the column matching the field type (text, number,
        formated_text, date YYYY-MM-DD, or select option ID). Calculated fields
        cannot be updated (409).
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    - `id` (int, **required**) - form field ID (see get-forms-values)
                    - `value` (**required**) - new value; type depends on the field type
                  example: '{"id": 1532, "value": "Export CSV na kartě klienta"}'
      responses:
        '200':
          description: Value updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Form field not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Calculated field cannot be updated
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/regenerate-summary/{id}:
    post:
      tags: [Task AI]
      summary: Regenerate AI summary
      description: |
        Regenerates `ai_summary` from the task description using the configured AI service
        and stores it on the task. Requires AI services enabled for the company.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Task ID
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: Summary regenerated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                      ai_summary:
                        type: string
                        example: 'Přidat tlačítko pro export dat do CSV.'
        '404':
          description: Task not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Task has no description, or AI services are not enabled / generation failed
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/get-ai-chats/{id}:
    post:
      tags: [Task AI]
      summary: List AI chats of a task
      description: Returns non-deleted AI chats linked to the task, newest first.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Task ID
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: List of AI chats
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TaskAiChat'
        '404':
          description: Task not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/create-ai-chat:
    post:
      tags: [Task AI]
      summary: Create AI chat for task
      description: |
        Creates a new AI chat linked to the task (files of the task are copied into the chat).
        Returns the chat ID and a prefill text built from the task description – send it
        as the first user message. Requires AI services enabled for the company.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    - `cl_task_id` (int, **required**) - task ID
                  example: '{"cl_task_id": 123}'
      responses:
        '200':
          description: AI chat created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      chat_id:
                        type: integer
                        example: 300
                      prefill_text:
                        type: string
                        description: Plain-text task description for the first message
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Task not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: AI services are not enabled
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/get-event-types:
    post:
      tags: [Task Lookups]
      summary: List helpdesk event types
      description: Returns event types (cl_partners_event_type) of the company, ordered by event_order.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: List of event types
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        type_name:
                          type: string
                          example: 'Technická podpora'
                        event_order:
                          type: integer
                        default_event:
                          type: boolean

  /tasks/get-event-methods:
    post:
      tags: [Task Lookups]
      summary: List helpdesk event methods
      description: Returns event methods (cl_partners_event_method) of the company, ordered by method_order.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: List of event methods
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        method_name:
                          type: string
                          example: 'Telefonicky'
                        method_order:
                          type: integer
                        default_method:
                          type: boolean

  /tasks/get-projects:
    post:
      tags: [Task Lookups]
      summary: List projects
      description: Returns projects (cl_project) of the company. By default only active ones (dtm_end IS NULL).
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    - `all` (0|1) - include finished projects, default 0
                  example: '{"all": 1}'
      responses:
        '200':
          description: List of projects
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: array
                    items:
                      type: object
                      properties:
                        id:
                          type: integer
                        label:
                          type: string
                          example: 'Mobilní aplikace'
                        dtm_start:
                          type: string
                          nullable: true
                        dtm_end:
                          type: string
                          nullable: true
                        pr_finished:
                          type: boolean

  /tasks/get-partner-stats/{id}:
    post:
      tags: [Task Lookups]
      summary: Task statistics of a partner
      description: |
        Returns the task statistics panel of a partner: last task, task count in the
        last 12 months, rejected tasks (status s_storno), unfinished tasks and the
        helpdesk hour fund usage.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Partner ID (cl_partners_book)
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: Partner statistics
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      partner_name:
                        type: string
                        example: 'ACME s.r.o.'
                      last_task:
                        type: object
                        nullable: true
                        properties:
                          id:
                            type: integer
                          task_number:
                            type: string
                          task_date:
                            type: string
                            nullable: true
                      tasks_12_months:
                        type: integer
                        example: 5
                      tasks_storno:
                        type: integer
                        example: 0
                      tasks_unfinished:
                        type: integer
                        example: 2
                      fund:
                        type: object
                        properties:
                          result:
                            type: string
                            description: HTML fragment with the fund summary
                          work_time:
                            type: number
                            description: Hours used from the fund
                          helpdesk_fund:
                            type: number
                            description: Fund size in hours (0 = not configured)
        '404':
          description: Partner not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /kdb/get-all:
    post:
      tags: [KDB Articles]
      summary: List KDB articles
      description: |
        Returns a paginated list of KDB articles. Description field is excluded
        to keep payload small. Use get-one for full article content.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    JSON-encoded filter parameters:
                    - `category_id` (int) - filter by category
                    - `search` (string) - fulltext search in title, description, ai_summary
                    - `limit` (int, default 50) - page size
                    - `offset` (int, default 0) - pagination offset
                  example: '{"category_id": 5, "search": "invoice", "limit": 20, "offset": 0}'
      responses:
        '200':
          description: List of articles
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/KdbArticleList'
                  total:
                    type: integer
                    description: Total number of matching articles (before pagination)
                    example: 42
        '401':
          description: Unauthorized - invalid or expired bearer_token
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /kdb/get-one/{id}:
    post:
      tags: [KDB Articles]
      summary: Get article detail
      description: Returns full article including description and category metadata.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Article ID
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: Article detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    $ref: '#/components/schemas/KdbArticleDetail'
        '404':
          description: Article not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /kdb/create:
    post:
      tags: [KDB Articles]
      summary: Create new article
      description: |
        Creates a new KDB article. Article number is auto-generated from number series.
        If description is provided, an AI summary is generated automatically.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    JSON-encoded article data:
                    - `title` (string, required)
                    - `description` (string, required) - HTML or Markdown content
                    - `cl_kdb_category_id` (int, required) - category ID
                  example: '{"title": "New article", "description": "<p>Content</p>", "cl_kdb_category_id": 5}'
      responses:
        '200':
          description: Article created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 124
                      kdb_number:
                        type: string
                        example: 'KB-0124'
        '400':
          description: Validation error (missing title, category, or invalid category)
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                missingTitle:
                  value: { status: error, message: 'Title is required' }
                missingCategory:
                  value: { status: error, message: 'Category is required' }
                invalidCategory:
                  value: { status: error, message: 'Invalid category' }

  /kdb/update:
    post:
      tags: [KDB Articles]
      summary: Update existing article
      description: |
        Updates an existing KDB article. Only provided fields are updated.
        If description changes, AI summary is regenerated automatically.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    JSON-encoded update data:
                    - `id` (int, required) - article ID to update
                    - `title` (string, optional)
                    - `description` (string, optional)
                    - `cl_kdb_category_id` (int, optional)
                    - `changed` (string, optional) - last known change timestamp for conflict detection
                    - `change_by` (string, optional) - defaults to "API"
                  example: '{"id": 42, "title": "Updated title", "description": "<p>New content</p>"}'
      responses:
        '200':
          description: Article updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 42
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '404':
          description: Article not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /kdb/delete/{id}:
    post:
      tags: [KDB Articles]
      summary: Delete article
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Article ID to delete
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: Article deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '404':
          description: Article not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /kdb/get-categories:
    post:
      tags: [KDB Categories]
      summary: List categories (hierarchical tree)
      description: |
        Returns all KDB categories as a hierarchical tree.
        Parent categories contain a `children` array with their subcategories.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: Category tree
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/KdbCategory'
              example:
                status: ok
                data:
                  - id: 1
                    name: 'General'
                    parent_id: null
                    public: true
                    plain_text: false
                    children:
                      - id: 2
                        name: 'FAQ'
                        parent_id: 1
                        public: false
                        plain_text: true
                        children: []

  /kdb/get-category/{id}:
    post:
      tags: [KDB Categories]
      summary: Get category detail
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Category ID
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: Category detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                      name:
                        type: string
                      parent_id:
                        type: integer
                        nullable: true
                      public:
                        type: boolean
                      plain_text:
                        type: boolean
                      access_key:
                        type: string
                        nullable: true
                      created:
                        type: string
                        nullable: true
                      create_by:
                        type: string
                      changed:
                        type: string
                        nullable: true
                      change_by:
                        type: string
                        nullable: true
        '404':
          description: Category not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /kdb/create-category:
    post:
      tags: [KDB Categories]
      summary: Create new category
      description: |
        Creates a new KDB category. If `public` is true, a random 64-char
        access key is generated automatically for public article sharing.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    JSON-encoded category data:
                    - `name` (string, required, max 50 chars)
                    - `cl_kdb_category_id` (int, optional) - parent category ID
                    - `public` (boolean, optional, default false) - enable public sharing
                    - `plain_text` (boolean, optional, default false) - Markdown mode
                  example: '{"name": "FAQ", "cl_kdb_category_id": 1, "public": true, "plain_text": false}'
      responses:
        '200':
          description: Category created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 10
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                missingName:
                  value: { status: error, message: 'Name is required' }
                nameTooLong:
                  value: { status: error, message: 'Name must be 50 characters or less' }
                invalidParent:
                  value: { status: error, message: 'Invalid parent category' }

  /kdb/update-category:
    post:
      tags: [KDB Categories]
      summary: Update existing category
      description: |
        Updates an existing KDB category. Only provided fields are updated.
        When setting `public` to true, an access key is auto-generated if not already present.
        When setting `public` to false, the access key is cleared.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    JSON-encoded update data:
                    - `id` (int, required) - category ID to update
                    - `name` (string, optional, max 50 chars)
                    - `cl_kdb_category_id` (int|null, optional) - parent category ID
                    - `public` (boolean, optional)
                    - `plain_text` (boolean, optional)
                  example: '{"id": 5, "name": "Updated FAQ", "public": true}'
      responses:
        '200':
          description: Category updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                selfParent:
                  value: { status: error, message: 'Category cannot be its own parent' }
        '404':
          description: Category not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /kdb/delete-category/{id}:
    post:
      tags: [KDB Categories]
      summary: Delete category
      description: |
        Deletes a KDB category. Fails if the category contains articles or subcategories.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Category ID to delete
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: Category deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '404':
          description: Category not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
        '409':
          description: Category has articles or subcategories
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                hasArticles:
                  value: { status: error, message: 'Cannot delete category with articles (3)' }
                hasChildren:
                  value: { status: error, message: 'Cannot delete category with subcategories (2)' }

  /kdb/get-files/{id}:
    post:
      tags: [KDB Files]
      summary: List files attached to a KDB article
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer }, description: KDB article ID }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token: { type: string }
      responses:
        '200':
          description: File list
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/FileItem' }
        '404':
          description: Article not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /kdb/file-upload:
    post:
      tags: [KDB Files]
      summary: Upload a file to a KDB article (base64 data URL)
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    JSON objekt: { cl_kdb_id, file: FileUploadInput }.
                    file.dataUrl = data:<mime>;base64,<...> (viz schema FileUploadInput)
      responses:
        '200':
          description: Uploaded
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: object
                    properties:
                      id: { type: integer }
                      file_name: { type: string }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Article not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /kdb/file-download/{id}:
    get:
      tags: [KDB Files]
      summary: Download a KDB article file (binary)
      description: >-
        Veřejné stažení přílohy KDB článku přes GET jen podle id souboru — bez bearer_token.
        Funguje pouze pro soubory navázané na KDB článek (cl_kdb_id); soubor se dohledá jen podle id, bez omezení na firmu.
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer }, description: File ID }
      responses:
        '200':
          description: Binary file content
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        '404':
          description: File not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /kdb/file-delete/{id}:
    post:
      tags: [KDB Files]
      summary: Delete a KDB article file
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer }, description: File ID }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token: { type: string }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessResponse' }
        '404':
          description: File not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /ai-chat-embed/list-assistants:
    post:
      tags: [AI Chat Embed]
      summary: List active embed-capable AI assistants for the tenant
      security:
        - syncToken: []
      description: |
        Returns assistants the host desktop application can choose from before calling
        `/ai-chat-embed/start`. Filter applied:

        - `cl_ai_assistant.active = 1`
        - `cl_company_id IS NULL OR cl_company_id = tenant`
        - `service_type IN ('help', 'chat', 'kdb')`

        Internal-only types (`task_analyst`, `summary`, `topic`, `kdb_selection`) are
        excluded. If `cl_company.ai_enabled = 0`, the response contains an empty list and
        an explanatory `message`.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [sync_token]
              properties:
                sync_token:
                  type: string
          application/json:
            schema:
              type: object
              required: [sync_token]
              properties:
                sync_token:
                  type: string
      responses:
        '200':
          description: List of available assistants (may be empty if AI is disabled or no assistants match).
          content:
            application/json:
              schema:
                type: object
                properties:
                  assistants:
                    type: array
                    items:
                      $ref: '#/components/schemas/AiChatEmbedAssistant'
                  message:
                    type: string
                    nullable: true
                    description: Present only when AI is disabled for the tenant.
                    example: 'AI služby nejsou pro tuto firmu povoleny.'

  /ai-chat-embed/start:
    post:
      tags: [AI Chat Embed]
      summary: Initialize an embed AI chat session for WebView2
      security:
        - syncToken: []
      description: |
        Server-to-server endpoint. The host desktop application calls this from its own
        backend with its tenant `sync_token`, the end-customer's IČ, and the end-user's
        display name. Trynx:

        1. Validates the `sync_token` and resolves the tenant `cl_company_id`.
        2. Validates the asistent (must be active, owned by tenant or global, not `task_analyst`).
        3. Looks up `cl_partners_book` by IČ within the tenant — optional partner link.
        4. Creates a `cl_ai_chat` row (`cl_users_id = NULL`, external_* metadata filled).
        5. Issues a 64-char session token in `cl_ai_chat_external_session` (TTL 8 h).
        6. Returns an absolute `embed_url` containing the token.

        The host application then loads `embed_url` into its WebView2 control. The chat
        UI is fully isolated (own layout, theme from the assistant, no Trynx menu/sidebar)
        and uses the token instead of cookies for authentication. AI usage is billed to
        the tenant that owns the `sync_token`.

        Requires `cl_company.ai_enabled = 1`.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              $ref: '#/components/schemas/AiChatEmbedStartRequest'
          application/json:
            schema:
              $ref: '#/components/schemas/AiChatEmbedStartRequest'
      responses:
        '200':
          description: Chat session created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/AiChatEmbedStartResponse'
              examples:
                matched:
                  summary: Partner matched by IČ
                  value:
                    chat_id: 92
                    token: 'CxKuSjQlgQ3Cm7s5NhWBnBdmE6DcW55lD31lkwNVrCYVyvVFWgfAqtDOldB1KwiM'
                    embed_url: 'https://trynx.example.com/application/ai-chat-embed/?token=CxKuS...'
                    expires_at: '2026-04-25T18:53:22+02:00'
                    partner_matched: true
                noMatch:
                  summary: No partner found in cl_partners_book — chat is still created
                  value:
                    chat_id: 93
                    token: 'a7Vn9Q...'
                    embed_url: 'https://trynx.example.com/application/ai-chat-embed/?token=a7Vn9Q...'
                    expires_at: '2026-04-25T19:00:00+02:00'
                    partner_matched: false
        '400':
          description: Validation error (missing or invalid input fields).
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
              examples:
                missingAssistant:
                  value: { error: 'Chybí nebo neplatný assistant_id.' }
                taskAnalyst:
                  value: { error: 'Task analyst režim není v embed podporován.' }
        '403':
          description: AI services disabled for the tenant.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: 'AI služby nejsou pro tuto firmu povoleny.'
        '404':
          description: Assistant not found or inactive for this tenant.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: 'Asistent nenalezen nebo není pro tuto firmu aktivní.'
        '500':
          description: Internal error while creating the chat.
          content:
            application/json:
              schema:
                type: object
                properties:
                  error:
                    type: string
                    example: 'Nepodařilo se založit chat.'

  /commission/get-all:
    post:
      tags: [Commission]
      summary: List commissions
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    Filtry: cl_partners_book_id, cl_status_id, cl_users_id, search,
                    date_from, date_to (cm_date), limit (50), offset (0)
      responses:
        '200':
          description: List
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/CommissionListItem' }
                  total: { type: integer }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /commission/get-one/{id}:
    post:
      tags: [Commission]
      summary: Commission detail with nested items/tasks/work/files
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token: { type: string }
      responses:
        '200':
          description: Detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/CommissionDetail' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /commission/create:
    post:
      tags: [Commission]
      summary: Create commission (cm_number auto-generated)
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    cm_title, cm_date, cl_partners_book_id, cl_status_id (status_use=commission),
                    cl_users_id, cl_currencies_id, header_txt, footer_txt, description_txt,
                    req_date, delivery_time_from, delivery_time_to (HH:MM, prázdné = bez času;
                    jiný formát vrátí 400), ...
      responses:
        '200':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: object
                    properties:
                      id: { type: integer }
                      cm_number: { type: string }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /commission/update:
    post:
      tags: [Commission]
      summary: Update commission (partial)
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                dataJSON: { type: string, description: 'id (required) + měněná pole; delivery_time_from/delivery_time_to ve formátu HH:MM, prázdná hodnota čas smaže. Změněný čas se přenese i do všech dodacích listů zakázky (stejně jako uložení karty zakázky).' }
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: object, properties: { id: { type: integer } } }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /commission/delete/{id}:
    post:
      tags: [Commission]
      summary: Delete commission (409 if has items/work/files)
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token: { type: string }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessResponse' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: Has linked data
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /commission/create-item:
    post:
      tags: [Commission Items]
      summary: Create commission item
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    cl_commission_id (required) + pole položky: item_label, quantity, units,
                    price_s, price_e, vat, discount, cl_pricelist_id, ...
      responses:
        '200':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: object, properties: { id: { type: integer } } }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Commission not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /commission/update-item:
    post:
      tags: [Commission Items]
      summary: Update commission item (partial)
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                dataJSON: { type: string, description: 'id (required) + měněná pole' }
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: object, properties: { id: { type: integer } } }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /commission/delete-item/{id}:
    post:
      tags: [Commission Items]
      summary: Delete commission item
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token: { type: string }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessResponse' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /commission/create-task:
    post:
      tags: [Commission Tasks]
      summary: Create commission task
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    cl_commission_id (required) + pole úkolu: name, description, done,
                    work_time, units, work_rate, cl_workplaces_id, ...
      responses:
        '200':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: object, properties: { id: { type: integer } } }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Commission not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /commission/update-task:
    post:
      tags: [Commission Tasks]
      summary: Update commission task (partial)
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                dataJSON: { type: string, description: 'id (required) + měněná pole' }
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: object, properties: { id: { type: integer } } }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /commission/delete-task/{id}:
    post:
      tags: [Commission Tasks]
      summary: Delete commission task
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token: { type: string }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessResponse' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /commission/create-work:
    post:
      tags: [Commission Work]
      summary: Create commission work record
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    cl_commission_id (required) + pole práce: work_label, work_date_s,
                    work_date_e, work_time, work_rate, qty_ok, qty_nok, cl_workplaces_id, ...
      responses:
        '200':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: object, properties: { id: { type: integer } } }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Commission not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /commission/update-work:
    post:
      tags: [Commission Work]
      summary: Update commission work record (partial)
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                dataJSON: { type: string, description: 'id (required) + měněná pole' }
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: object, properties: { id: { type: integer } } }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /commission/delete-work/{id}:
    post:
      tags: [Commission Work]
      summary: Delete commission work record
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token: { type: string }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessResponse' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /commission/get-files/{id}:
    post:
      tags: [Commission Files]
      summary: List commission files
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token: { type: string }
      responses:
        '200':
          description: File list
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/FileItem' }
        '404':
          description: Commission not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /commission/file-upload:
    post:
      tags: [Commission Files]
      summary: Upload a commission file (base64 data URL)
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    JSON objekt: { cl_commission_id, file: FileUploadInput }.
                    file.dataUrl = data:<mime>;base64,<...> (viz schema FileUploadInput)
      responses:
        '200':
          description: Uploaded
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: object
                    properties:
                      id: { type: integer }
                      file_name: { type: string }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Commission not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /commission/file-download/{id}:
    post:
      tags: [Commission Files]
      summary: Download a commission file (binary)
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token: { type: string }
      responses:
        '200':
          description: Binary file content
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        '404':
          description: File not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /commission/file-delete/{id}:
    post:
      tags: [Commission Files]
      summary: Delete a commission file
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token: { type: string }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessResponse' }
        '404':
          description: File not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /offer/get-all:
    post:
      tags: [Offer]
      summary: List offers
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    Filtry: cl_partners_book_id, cl_status_id, cl_users_id, search,
                    date_from, date_to (offer_date), limit (50), offset (0)
      responses:
        '200':
          description: List
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/OfferListItem' }
                  total: { type: integer }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /offer/get-one/{id}:
    post:
      tags: [Offer]
      summary: Offer detail with nested items/tasks/work/files
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token: { type: string }
      responses:
        '200':
          description: Detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/OfferDetail' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /offer/create:
    post:
      tags: [Offer]
      summary: Create offer (cm_number auto-generated)
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    cm_title, offer_date, cl_partners_book_id, cl_status_id (status_use=offer),
                    cl_users_id, cl_currencies_id, cl_commission_id, header_txt, footer_txt,
                    description_txt, ...
      responses:
        '200':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: object
                    properties:
                      id: { type: integer }
                      cm_number: { type: string }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /offer/update:
    post:
      tags: [Offer]
      summary: Update offer (partial)
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                dataJSON: { type: string, description: 'id (required) + měněná pole' }
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: object, properties: { id: { type: integer } } }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /offer/delete/{id}:
    post:
      tags: [Offer]
      summary: Delete offer (409 if has items/work/files)
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token: { type: string }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessResponse' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: Has linked data
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /offer/create-item:
    post:
      tags: [Offer Items]
      summary: Create offer item
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    cl_offer_id (required) + pole položky: item_label, quantity, units,
                    price_e, vat, cl_pricelist_id, ...
      responses:
        '200':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: object, properties: { id: { type: integer } } }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Offer not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /offer/update-item:
    post:
      tags: [Offer Items]
      summary: Update offer item (partial)
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                dataJSON: { type: string, description: 'id (required) + měněná pole' }
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: object, properties: { id: { type: integer } } }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /offer/delete-item/{id}:
    post:
      tags: [Offer Items]
      summary: Delete offer item
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token: { type: string }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessResponse' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /offer/create-task:
    post:
      tags: [Offer Tasks]
      summary: Create offer task
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    cl_offer_id (required) + pole úkolu: name, description, done,
                    work_time, units, work_rate, ...
      responses:
        '200':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: object, properties: { id: { type: integer } } }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Offer not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /offer/update-task:
    post:
      tags: [Offer Tasks]
      summary: Update offer task (partial)
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                dataJSON: { type: string, description: 'id (required) + měněná pole' }
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: object, properties: { id: { type: integer } } }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /offer/delete-task/{id}:
    post:
      tags: [Offer Tasks]
      summary: Delete offer task
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token: { type: string }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessResponse' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /offer/create-work:
    post:
      tags: [Offer Work]
      summary: Create offer work record
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    cl_offer_id (required) + pole práce: work_label, work_date_s,
                    work_date_e, work_time, work_rate, ...
      responses:
        '200':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: object, properties: { id: { type: integer } } }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Offer not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /offer/update-work:
    post:
      tags: [Offer Work]
      summary: Update offer work record (partial)
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                dataJSON: { type: string, description: 'id (required) + měněná pole' }
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: object, properties: { id: { type: integer } } }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /offer/delete-work/{id}:
    post:
      tags: [Offer Work]
      summary: Delete offer work record
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token: { type: string }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessResponse' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /offer/get-files/{id}:
    post:
      tags: [Offer Files]
      summary: List offer files
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token: { type: string }
      responses:
        '200':
          description: File list
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/FileItem' }
        '404':
          description: Offer not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /offer/file-upload:
    post:
      tags: [Offer Files]
      summary: Upload an offer file (base64 data URL)
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    JSON objekt: { cl_offer_id, file: FileUploadInput }.
                    file.dataUrl = data:<mime>;base64,<...> (viz schema FileUploadInput)
      responses:
        '200':
          description: Uploaded
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: object
                    properties:
                      id: { type: integer }
                      file_name: { type: string }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Offer not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /offer/file-download/{id}:
    post:
      tags: [Offer Files]
      summary: Download an offer file (binary)
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token: { type: string }
      responses:
        '200':
          description: Binary file content
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        '404':
          description: File not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /offer/file-delete/{id}:
    post:
      tags: [Offer Files]
      summary: Delete an offer file
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token: { type: string }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessResponse' }
        '404':
          description: File not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoicearrived/get-all:
    post:
      tags: [Invoice Arrived]
      summary: List arrived invoices
      description: |
        Výpis přijatých faktur autorizované firmy, řazeno `inv_date DESC, inv_number DESC`.
        Vrací `total` = počet záznamů odpovídajících filtru před stránkováním.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
                dataJSON:
                  type: string
                  description: |
                    JSON s filtry (všechny nepovinné, lze kombinovat):
                    - `search` (string) – LIKE přes inv_number, rinv_number, var_symb, inv_title a název dodavatele
                    - `cl_partners_book_id` (int) – faktury dodavatele
                    - `cl_status_id` (int) – faktury ve stavu
                    - `cl_center_id` (int) – faktury střediska
                    - `cl_currencies_id` (int) – faktury v měně
                    - `cl_payment_types_id` (int) – faktury s formou úhrady
                    - `cl_commission_id` (int) – faktury navázané na zakázku
                    - `date_from` / `date_to` (Y-m-d) – rozsah data vystavení (inv_date)
                    - `due_from` / `due_to` (Y-m-d) – rozsah data splatnosti (due_date)
                    - `unpaid` (1) – jen neuhrazené (pay_date IS NULL)
                    - `overdue` (1) – jen neuhrazené po splatnosti (pay_date IS NULL AND due_date < dnes)
                    - `changed_since` (Y-m-d H:i:s) – faktury změněné nebo založené od zadaného okamžiku (synchronizace appky)
                    - `limit` (int, výchozí 50) – ořízne se do rozsahu 1–500
                    - `offset` (int, výchozí 0) – záporná hodnota se bere jako 0
                  example: '{"unpaid":1,"limit":20}'
      responses:
        '200':
          description: List
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/InvoiceArrivedListItem' }
                  total: { type: integer, description: 'Počet záznamů odpovídajících filtru před stránkováním', example: 137 }
        '400':
          description: 'Neplatné datum ve filtru (date_from, date_to, due_from, due_to, changed_since) — např. „Neplatný formát data v poli date_from". Datum lze poslat i jako d.m.Y.'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoicearrived/get-all-payments:
    post:
      tags: [Invoice Arrived]
      summary: Bulk arrived-invoice payments for a period
      description: |
        Hromadné čtení úhrad přijatých faktur pro reporting cashflow výdajů.

        `date_from` a `date_to` se vztahují k **datu úhrady** (`pay_date`).
        `cl_partners_book_id` a `cl_currencies_id` filtrují podle faktury,
        `cl_payment_types_id` podle úhrady. `changed_since` hlídá jen řádek úhrady.
        Řazeno podle `id`, `limit` max 500 — vždy stránkujte podle `total`.

        `changed`/`created` jsou lokální čas serveru (Europe/Prague) bez pásma — `T`
        pro `changed_since` berte z hodin serveru a v dalším běhu ho o pár minut
        posuňte zpět, překryv nevadí (upsert podle `id` je idempotentní).

        Smazané úhrady vrací /invoicearrived/get-deleted.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
                dataJSON:
                  type: string
                  description: 'JSON: date_from, date_to, changed_since, cl_partners_book_id, cl_currencies_id, cl_payment_types_id, limit, offset'
                  example: '{"date_from":"2026-01-01","limit":500,"offset":0}'
      responses:
        '200':
          description: Payments, paginated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  total: { type: integer }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/InvoiceArrivedPayment' }
        '400':
          description: Neplatné datum ve filtru
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - chybí právo read
  /invoicearrived/get-all-allocations:
    post:
      tags: [Invoice Arrived]
      summary: Bulk allocations of arrived invoices to commissions
      description: |
        Hromadné čtení rozpuštění nákladů přijatých faktur na zakázky — podklad
        pro ziskovost zakázek.

        Řádek rozpuštění vlastní datum nemá: `date_from` a `date_to` se vztahují
        k **datu faktury** (`inv_date`). `cl_commission_id` filtruje podle zakázky,
        `changed_since` hlídá jen řádek rozpuštění. Řazeno podle `id`, `limit` max 500.

        `changed`/`created` jsou lokální čas serveru (Europe/Prague) bez pásma — `T`
        pro `changed_since` berte z hodin serveru a v dalším běhu ho o pár minut
        posuňte zpět, překryv nevadí (upsert podle `id` je idempotentní).

        Smazaná rozpuštění vrací /invoicearrived/get-deleted.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
                dataJSON:
                  type: string
                  description: 'JSON: date_from, date_to, changed_since, cl_partners_book_id, cl_currencies_id, cl_commission_id, limit, offset'
                  example: '{"date_from":"2026-01-01","limit":500,"offset":0}'
      responses:
        '200':
          description: Allocations, paginated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  total: { type: integer }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/InvoiceArrivedCommission' }
        '400':
          description: Neplatné datum ve filtru
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - chybí právo read
  /invoicearrived/get-deleted:
    post:
      tags: [Invoice Arrived]
      summary: Deleted arrived invoices, payments and allocations since a time
      description: |
        Seznam přijatých faktur, úhrad a rozpuštění smazaných od `since` — pro
        inkrementální načítání do BI.

        **Pozor:** úhrady a rozpuštění smazané spolu s celou fakturou (kaskádou)
        se tu **neobjeví**. Při `table = cl_invoice_arrived` smažte u sebe i všechny
        řádky s tímto `cl_invoice_arrived_id`.

        `deleted_at` je lokální čas serveru (Europe/Prague) bez pásma — `since` berte
        z hodin serveru a v dalším běhu ho o pár minut posuňte zpět, překryv nevadí
        (mazání podle `id` je idempotentní).

        Omezeno jen na firmu. `since` je povinné.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
                dataJSON:
                  type: string
                  description: 'JSON: since (povinné), limit, offset'
                  example: '{"since":"2026-09-22 00:00:00","limit":500,"offset":0}'
      responses:
        '200':
          description: Deleted records, paginated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  total: { type: integer }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/DeletedRecord' }
        '400':
          description: Chybí nebo je neplatné since
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - chybí právo read

  /reporting/get-store-moves:
    post:
      tags: [Reporting]
      summary: Store moves for BI
      description: |
        Skladové pohyby: množství (`s_in`, `s_out`), nákupní cena (`price_s`), prodejní cena
        (`price_e2`) a zisk. `doc_date` a `doc_type` jsou z dokladu, aby nebylo nutné joinovat.

        Filtry: `date_from`/`date_to` (datum DOKLADU), `doc_type` (0 příjem, 1 výdej),
        `cl_storage_id`, `cl_pricelist_id`, `changed_since` (změna pohybu NEBO jeho dokladu —
        když se na dokladu změní datum nebo typ, vrátí se i jeho pohyby, aby `doc_date`/
        `doc_type` na pohybu zůstaly aktuální). Omezení na úrovni záznamu (pobočka, jen
        vlastní) se bere z dokladu. Vyžaduje právo `read` na Sklad.

        **Archivace skladu:** pohyb s `rollup = 1` je souhrn, kterým firma nahradila archivované
        detailní pohyby. Když ho inkrementální běh přinese, stáhněte pohyby i doklady znovu celé
        (bez `changed_since`) a nahraďte jimi lokální data — archivované detaily se v
        get-deleted neobjeví.

        Stav k datu = součet `s_in − s_out` pohybů s `doc_date` ≤ datum po
        (`cl_pricelist_id`, `cl_storage_id`).

        Stránkování: pošlete `after_id` (na první stránce 0) a pokračujte s `next_after_id`
        z odpovědi, dokud není `null`. S `after_id` se `total` nepočítá (je `null`) — u
        velkých firem je to řádově rychlejší. Bez `after_id` funguje `offset` a vrací se
        `total`. `limit` max 500 (vyšší se tiše ořízne). Řazeno podle `id`. Časy jsou
        serverový čas Europe/Prague bez značky zóny.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: date_from, date_to, doc_type, cl_storage_id, cl_pricelist_id, changed_since, after_id, limit, offset'
                  example: '{"date_from":"2026-01-01","after_id":0,"limit":500}'
      responses:
        '200':
          description: Store moves page
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ReportingPage'
                  - type: object
                    properties:
                      data: { type: array, items: { $ref: '#/components/schemas/ReportingStoreMove' } }
        '400':
          description: Neplatné datum, doc_type nebo after_id
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - chybí právo read na modul

  /reporting/get-store-docs:
    post:
      tags: [Reporting]
      summary: Store documents for BI
      description: |
        Hlavičky skladových dokladů: typ (`doc_type`), datum, partner, sklad a vazby na
        faktury a dodací listy (vydané i přijaté).

        Filtry: `date_from`/`date_to` (datum dokladu), `doc_type` (0 příjem, 1 výdej),
        `cl_storage_id`, `cl_partners_book_id`, `changed_since`. Omezení na úrovni záznamu
        (pobočka, jen vlastní) se uplatňuje stejně jako ve webu. Vyžaduje právo `read` na
        Sklad.

        **Archivace skladu:** archivační běh založí souhrnný doklad s `doc_date` = datum
        archivace, na který ukazují souhrnné pohyby (`rollup = 1`) — viz get-store-moves.

        Stránkování: pošlete `after_id` (na první stránce 0) a pokračujte s `next_after_id`
        z odpovědi, dokud není `null`. S `after_id` se `total` nepočítá (je `null`) — u
        velkých firem je to řádově rychlejší. Bez `after_id` funguje `offset` a vrací se
        `total`. `limit` max 500 (vyšší se tiše ořízne). Řazeno podle `id`. Časy jsou
        serverový čas Europe/Prague bez značky zóny.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: date_from, date_to, doc_type, cl_storage_id, cl_partners_book_id, changed_since, after_id, limit, offset'
                  example: '{"date_from":"2026-01-01","after_id":0,"limit":500}'
      responses:
        '200':
          description: Store documents page
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ReportingPage'
                  - type: object
                    properties:
                      data: { type: array, items: { $ref: '#/components/schemas/ReportingStoreDoc' } }
        '400':
          description: Neplatné datum, doc_type nebo after_id
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - chybí právo read na modul

  /reporting/get-stock:
    post:
      tags: [Reporting]
      summary: Current stock for BI
      description: |
        Aktuální souhrnný stav skladu (karty `cl_store`) po položce, skladu a šarži.

        Filtry: `cl_storage_id`, `cl_pricelist_id`, `changed_since`. Vyžaduje právo `read`
        na Sklad.

        U pohybů s importem (`import`, `import_fin`) se součet pohybů od tohoto souhrnu
        může v jednotkách položek lišit — souhrn import nezapočítává.

        Stránkování: pošlete `after_id` (na první stránce 0) a pokračujte s `next_after_id`
        z odpovědi, dokud není `null`. S `after_id` se `total` nepočítá (je `null`) — u
        velkých firem je to řádově rychlejší. Bez `after_id` funguje `offset` a vrací se
        `total`. `limit` max 500 (vyšší se tiše ořízne). Řazeno podle `id`. Časy jsou
        serverový čas Europe/Prague bez značky zóny.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: cl_storage_id, cl_pricelist_id, changed_since, after_id, limit, offset'
                  example: '{"after_id":0,"limit":500}'
      responses:
        '200':
          description: Stock page
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ReportingPage'
                  - type: object
                    properties:
                      data: { type: array, items: { $ref: '#/components/schemas/ReportingStock' } }
        '400':
          description: Neplatné datum nebo after_id
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - chybí právo read na modul

  /reporting/get-pricelist:
    post:
      tags: [Reporting]
      summary: Pricelist for BI
      description: |
        Ceník včetně neaktivních položek (`not_active = 1` se nefiltruje — BI je potřebuje
        jako dimenzi historických pohybů).

        Filtry: `cl_pricelist_group_id`, `changed_since`. Vyžaduje právo `read` na Ceník.

        Stránkování: pošlete `after_id` (na první stránce 0) a pokračujte s `next_after_id`
        z odpovědi, dokud není `null`. S `after_id` se `total` nepočítá (je `null`) — u
        velkých firem je to řádově rychlejší. Bez `after_id` funguje `offset` a vrací se
        `total`. `limit` max 500 (vyšší se tiše ořízne). Řazeno podle `id`. Časy jsou
        serverový čas Europe/Prague bez značky zóny.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: cl_pricelist_group_id, changed_since, after_id, limit, offset'
                  example: '{"after_id":0,"limit":500}'
      responses:
        '200':
          description: Pricelist page
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ReportingPage'
                  - type: object
                    properties:
                      data: { type: array, items: { $ref: '#/components/schemas/ReportingPricelistItem' } }
        '400':
          description: Neplatné datum nebo after_id
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - chybí právo read na modul

  /reporting/get-pricelist-groups:
    post:
      tags: [Reporting]
      summary: Pricelist groups
      description: |
        Skupiny ceníku (`cl_pricelist_group`) vč. nadřazené skupiny
        (`cl_pricelist_group_id`). Bez filtrů a stránkování — číselník bývá malý.
        Vyžaduje právo `read` na Ceník.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: Pricelist groups
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: array, items: { $ref: '#/components/schemas/ReportingPricelistGroup' } }
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - chybí právo read na modul

  /reporting/get-storages:
    post:
      tags: [Reporting]
      summary: Storages
      description: |
        Sklady (`cl_storage`) vč. nadřazeného skladu (`cl_storage_id`). Bez filtrů a
        stránkování — číselník bývá malý. Vyžaduje právo `read` na Sklady.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: Storages
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: array, items: { $ref: '#/components/schemas/ReportingStorage' } }
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - chybí právo read na modul

  /reporting/get-deleted:
    post:
      tags: [Reporting]
      summary: Deleted store and pricelist records since a time
      description: |
        Smazané pohyby, doklady, karty, položky ceníku, skupiny, sklady, zakázky,
        nabídky, dodací listy a partneři od `since` (tabulky `cl_store_move`,
        `cl_store_docs`, `cl_store`, `cl_pricelist`, `cl_pricelist_group`,
        `cl_storage`, `cl_commission`, `cl_commission_items_sel`,
        `cl_commission_work`, `cl_offer`, `cl_offer_items`, `cl_delivery_note`,
        `cl_delivery_note_items`, `cl_delivery_note_items_back`,
        `cl_partners_book`, `cl_partners_branch`). Vrací jen tabulky, na jejichž
        modul má volající právo `read` — bez práva na žádnou z nich 403.

        Mapování na výpisy: `cl_store_move` ↔ get-store-moves, `cl_store_docs` ↔
        get-store-docs, `cl_store` ↔ get-stock, `cl_pricelist` ↔ get-pricelist,
        `cl_pricelist_group` ↔ get-pricelist-groups, `cl_storage` ↔ get-storages,
        `cl_commission` ↔ get-commissions, `cl_commission_items_sel` ↔
        get-commission-items, `cl_commission_work` ↔ get-commission-work,
        `cl_offer` ↔ get-offers, `cl_offer_items` ↔ get-offer-items,
        `cl_delivery_note` ↔ get-delivery-notes, `cl_delivery_note_items` ↔
        get-delivery-note-items, `cl_delivery_note_items_back` ↔
        get-delivery-note-items-back, `cl_partners_book` ↔ get-partners,
        `cl_partners_branch` ↔ get-partner-branches.

        Smazaná karta `cl_store` maže kaskádou své pohyby — zahoďte i pohyby s tímto
        `cl_store_id`. Archivované pohyby se tu neobjeví (viz `rollup` u
        get-store-moves).

        Smazaná zakázka maže kaskádou své prodejní položky a práci, nabídka své
        položky, dodací list své položky (vratky ne — DL s vratkami nejde smazat)
        a partner své pobočky. Tato smazání se tu neobjeví — zahoďte je podle
        hlavičky.

        `deleted_at` je lokální čas serveru (Europe/Prague) bez pásma — `since` berte
        z hodin serveru a v dalším běhu ho o pár minut posuňte zpět, překryv nevadí
        (mazání podle `id` je idempotentní).

        Stránkování je klasické `offset`/`limit` (max 500) s `total`, bez kurzoru
        (`after_id`) — stejně jako u /invoicearrived/get-deleted. `since` je povinné.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: since (povinné), limit, offset'
                  example: '{"since":"2026-09-22 00:00:00","limit":500,"offset":0}'
      responses:
        '200':
          description: Deleted records, paginated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  total: { type: integer }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/DeletedRecord' }
        '400':
          description: Chybí nebo je neplatné since
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - K tomuto modulu nemáte přístup (právo read chybí na všechny tabulky)

  /reporting/get-commissions:
    post:
      tags: [Reporting]
      summary: Commissions for BI
      description: |
        Zakázky: hlavičky s cenami, ziskem (celkovým i po prodejních položkách a
        práci), stavem a vazbami na faktury a skladový doklad.

        Filtry: `cl_partners_book_id`, `cl_status_id`, `storno` (0/1 přes
        `array_key_exists`, 0 je platný filtr; jiná hodnota vrací 400 `storno musí
        být 0 nebo 1`), `date_from`/`date_to` (datum zakázky `cm_date`),
        `changed_since`. Omezení na úrovni záznamu (pobočka, jen vlastní záznamy
        vč. druhého řešitele `cl_users_id2`) se uplatňuje stejně jako ve webu.
        Vyžaduje právo `read` na Zakázky.

        Stránkování: pošlete `after_id` (na první stránce 0) a pokračujte s `next_after_id`
        z odpovědi, dokud není `null`. S `after_id` se `total` nepočítá (je `null`) — u
        velkých firem je to řádově rychlejší. Bez `after_id` funguje `offset` a vrací se
        `total`. `limit` max 500 (vyšší se tiše ořízne). Řazeno podle `id`. Časy jsou
        serverový čas Europe/Prague bez značky zóny.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: cl_partners_book_id, cl_status_id, storno, date_from, date_to, changed_since, after_id, limit, offset'
                  example: '{"date_from":"2026-01-01","after_id":0,"limit":500}'
      responses:
        '200':
          description: Commissions page
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ReportingPage'
                  - type: object
                    properties:
                      data: { type: array, items: { $ref: '#/components/schemas/ReportingCommission' } }
        '400':
          description: Neplatné datum, storno nebo after_id
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - chybí právo read na modul

  /reporting/get-commission-items:
    post:
      tags: [Reporting]
      summary: Commission sales items for BI
      description: |
        Prodejní položky zakázky (`cl_commission_items_sel`): množství, ceny a zisk
        položky. Nákladové položky zakázky se nevracejí — náklady dává
        /invoicearrived/get-all-allocations.

        Filtry: `cl_commission_id`, `date_from`/`date_to` (datum zakázky `cm_date`),
        `changed_since`. Omezení na úrovni záznamu se bere ze zakázky (pobočka, jen
        vlastní záznamy vč. druhého řešitele). Vyžaduje právo `read` na Zakázky.

        `changed_since` vrací i řádky, jejichž hlavička se změnila (datum, partner, stav).

        Stránkování: pošlete `after_id` (na první stránce 0) a pokračujte s `next_after_id`
        z odpovědi, dokud není `null`. S `after_id` se `total` nepočítá (je `null`) — u
        velkých firem je to řádově rychlejší. Bez `after_id` funguje `offset` a vrací se
        `total`. `limit` max 500 (vyšší se tiše ořízne). Řazeno podle `id`. Časy jsou
        serverový čas Europe/Prague bez značky zóny.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: cl_commission_id, date_from, date_to, changed_since, after_id, limit, offset'
                  example: '{"cl_commission_id":123,"after_id":0,"limit":500}'
      responses:
        '200':
          description: Commission items page
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ReportingPage'
                  - type: object
                    properties:
                      data: { type: array, items: { $ref: '#/components/schemas/ReportingCommissionItem' } }
        '400':
          description: Neplatné datum nebo after_id
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - chybí právo read na modul

  /reporting/get-commission-work:
    post:
      tags: [Reporting]
      summary: Commission work items for BI
      description: |
        Práce na zakázce (`cl_commission_work`): čas, sazba a zisk řádku práce.

        Filtry: `cl_commission_id`, `date_from`/`date_to` (datum zakázky `cm_date`),
        `changed_since`. Omezení na úrovni záznamu se bere ze zakázky (pobočka, jen
        vlastní záznamy vč. druhého řešitele). Vyžaduje právo `read` na Zakázky.

        `changed_since` vrací i řádky, jejichž hlavička se změnila (datum, partner, stav).

        Stránkování: pošlete `after_id` (na první stránce 0) a pokračujte s `next_after_id`
        z odpovědi, dokud není `null`. S `after_id` se `total` nepočítá (je `null`) — u
        velkých firem je to řádově rychlejší. Bez `after_id` funguje `offset` a vrací se
        `total`. `limit` max 500 (vyšší se tiše ořízne). Řazeno podle `id`. Časy jsou
        serverový čas Europe/Prague bez značky zóny.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: cl_commission_id, date_from, date_to, changed_since, after_id, limit, offset'
                  example: '{"cl_commission_id":123,"after_id":0,"limit":500}'
      responses:
        '200':
          description: Commission work page
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ReportingPage'
                  - type: object
                    properties:
                      data: { type: array, items: { $ref: '#/components/schemas/ReportingCommissionWork' } }
        '400':
          description: Neplatné datum nebo after_id
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - chybí právo read na modul

  /reporting/get-offers:
    post:
      tags: [Reporting]
      summary: Offers for BI
      description: |
        Nabídky: hlavičky s cenami, platností a vazbou na zakázku (pokud z nabídky
        vznikla) a fakturu.

        Filtry: `cl_partners_book_id`, `cl_status_id`, `date_from`/`date_to` (datum
        nabídky `offer_date`), `changed_since`. Omezení na úrovni záznamu (pobočka,
        jen vlastní záznamy) se uplatňuje stejně jako ve webu. Vyžaduje právo `read`
        na Nabídky.

        Stránkování: pošlete `after_id` (na první stránce 0) a pokračujte s `next_after_id`
        z odpovědi, dokud není `null`. S `after_id` se `total` nepočítá (je `null`) — u
        velkých firem je to řádově rychlejší. Bez `after_id` funguje `offset` a vrací se
        `total`. `limit` max 500 (vyšší se tiše ořízne). Řazeno podle `id`. Časy jsou
        serverový čas Europe/Prague bez značky zóny.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: cl_partners_book_id, cl_status_id, date_from, date_to, changed_since, after_id, limit, offset'
                  example: '{"date_from":"2026-01-01","after_id":0,"limit":500}'
      responses:
        '200':
          description: Offers page
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ReportingPage'
                  - type: object
                    properties:
                      data: { type: array, items: { $ref: '#/components/schemas/ReportingOffer' } }
        '400':
          description: Neplatné datum nebo after_id
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - chybí právo read na modul

  /reporting/get-offer-items:
    post:
      tags: [Reporting]
      summary: Offer items for BI
      description: |
        Položky nabídky (`cl_offer_items`): množství, ceny a zisk položky.

        Filtry: `cl_offer_id`, `date_from`/`date_to` (datum nabídky `offer_date`),
        `changed_since`. Omezení na úrovni záznamu se bere z nabídky. Vyžaduje
        právo `read` na Nabídky.

        `changed_since` vrací i řádky, jejichž hlavička se změnila (datum, partner, stav).

        Stránkování: pošlete `after_id` (na první stránce 0) a pokračujte s `next_after_id`
        z odpovědi, dokud není `null`. S `after_id` se `total` nepočítá (je `null`) — u
        velkých firem je to řádově rychlejší. Bez `after_id` funguje `offset` a vrací se
        `total`. `limit` max 500 (vyšší se tiše ořízne). Řazeno podle `id`. Časy jsou
        serverový čas Europe/Prague bez značky zóny.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: cl_offer_id, date_from, date_to, changed_since, after_id, limit, offset'
                  example: '{"cl_offer_id":123,"after_id":0,"limit":500}'
      responses:
        '200':
          description: Offer items page
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ReportingPage'
                  - type: object
                    properties:
                      data: { type: array, items: { $ref: '#/components/schemas/ReportingOfferItem' } }
        '400':
          description: Neplatné datum nebo after_id
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - chybí právo read na modul

  /reporting/get-delivery-notes:
    post:
      tags: [Reporting]
      summary: Delivery notes for BI
      description: |
        Dodací listy: hlavičky s cenami, úhradou a vazbou na fakturu a skladový
        doklad.

        Filtry: `cl_partners_book_id`, `cl_status_id`, `storno` (0/1 přes
        `array_key_exists`, 0 je platný filtr; jiná hodnota vrací 400 `storno musí
        být 0 nebo 1`), `date_from`/`date_to` (datum vystavení `issue_date`),
        `changed_since`. Omezení na úrovni záznamu (pobočka, jen vlastní záznamy)
        se uplatňuje stejně jako ve webu. Vyžaduje právo `read` na Dodací listy.

        Stránkování: pošlete `after_id` (na první stránce 0) a pokračujte s `next_after_id`
        z odpovědi, dokud není `null`. S `after_id` se `total` nepočítá (je `null`) — u
        velkých firem je to řádově rychlejší. Bez `after_id` funguje `offset` a vrací se
        `total`. `limit` max 500 (vyšší se tiše ořízne). Řazeno podle `id`. Časy jsou
        serverový čas Europe/Prague bez značky zóny.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: cl_partners_book_id, cl_status_id, storno, date_from, date_to, changed_since, after_id, limit, offset'
                  example: '{"date_from":"2026-01-01","after_id":0,"limit":500}'
      responses:
        '200':
          description: Delivery notes page
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ReportingPage'
                  - type: object
                    properties:
                      data: { type: array, items: { $ref: '#/components/schemas/ReportingDeliveryNote' } }
        '400':
          description: Neplatné datum, storno nebo after_id
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - chybí právo read na modul

  /reporting/get-delivery-note-items:
    post:
      tags: [Reporting]
      summary: Delivery note items for BI
      description: |
        Položky dodacího listu (`cl_delivery_note_items`): množství, ceny a vazba
        na fakturu a skladový pohyb.

        Filtry: `cl_delivery_note_id`, `date_from`/`date_to` (datum vystavení
        `issue_date`), `changed_since`. Omezení na úrovni záznamu se bere z
        dodacího listu. Vyžaduje právo `read` na Dodací listy.

        `changed_since` vrací i řádky, jejichž hlavička se změnila (datum, partner, stav).

        Stránkování: pošlete `after_id` (na první stránce 0) a pokračujte s `next_after_id`
        z odpovědi, dokud není `null`. S `after_id` se `total` nepočítá (je `null`) — u
        velkých firem je to řádově rychlejší. Bez `after_id` funguje `offset` a vrací se
        `total`. `limit` max 500 (vyšší se tiše ořízne). Řazeno podle `id`. Časy jsou
        serverový čas Europe/Prague bez značky zóny.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: cl_delivery_note_id, date_from, date_to, changed_since, after_id, limit, offset'
                  example: '{"cl_delivery_note_id":123,"after_id":0,"limit":500}'
      responses:
        '200':
          description: Delivery note items page
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ReportingPage'
                  - type: object
                    properties:
                      data: { type: array, items: { $ref: '#/components/schemas/ReportingDeliveryNoteItem' } }
        '400':
          description: Neplatné datum nebo after_id
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - chybí právo read na modul

  /reporting/get-delivery-note-items-back:
    post:
      tags: [Reporting]
      summary: Delivery note returned items for BI
      description: |
        Vratky na dodacím listu (`cl_delivery_note_items_back`): vlastní řada `id`
        oddělená od položek (viz get-delivery-note-items) — v BI je použijte jako
        dvojici (typ, id).

        Filtry: `cl_delivery_note_id`, `date_from`/`date_to` (datum vystavení
        `issue_date`), `changed_since`. Omezení na úrovni záznamu se bere z
        dodacího listu. Vyžaduje právo `read` na Dodací listy.

        `changed_since` vrací i řádky, jejichž hlavička se změnila (datum, partner, stav).

        Stránkování: pošlete `after_id` (na první stránce 0) a pokračujte s `next_after_id`
        z odpovědi, dokud není `null`. S `after_id` se `total` nepočítá (je `null`) — u
        velkých firem je to řádově rychlejší. Bez `after_id` funguje `offset` a vrací se
        `total`. `limit` max 500 (vyšší se tiše ořízne). Řazeno podle `id`. Časy jsou
        serverový čas Europe/Prague bez značky zóny.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: cl_delivery_note_id, date_from, date_to, changed_since, after_id, limit, offset'
                  example: '{"cl_delivery_note_id":123,"after_id":0,"limit":500}'
      responses:
        '200':
          description: Delivery note returned items page
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ReportingPage'
                  - type: object
                    properties:
                      data: { type: array, items: { $ref: '#/components/schemas/ReportingDeliveryNoteItemBack' } }
        '400':
          description: Neplatné datum nebo after_id
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - chybí právo read na modul

  /reporting/get-partners:
    post:
      tags: [Reporting]
      summary: Partners for BI
      description: |
        Adresář partnerů jako dimenze pro BI — bez kontaktů (e-mail, telefon,
        kontaktní osoba), bankovních údajů, komentáře a přístupových klíčů (viz
        popis schématu `ReportingPartner`). Vrací i smazané (`deleted = 1`) a
        neaktivní (`active = 0`) partnery s příznakem — historické doklady na ně
        odkazují.

        Filtry: `cl_partners_category_id`, `changed_since`. Omezení na úrovni
        záznamu (jen vlastní záznamy) se uplatňuje stejně jako ve webu. Vyžaduje
        právo `read` na Adresář partnerů.

        Stránkování: pošlete `after_id` (na první stránce 0) a pokračujte s `next_after_id`
        z odpovědi, dokud není `null`. S `after_id` se `total` nepočítá (je `null`) — u
        velkých firem je to řádově rychlejší. Bez `after_id` funguje `offset` a vrací se
        `total`. `limit` max 500 (vyšší se tiše ořízne). Řazeno podle `id`. Časy jsou
        serverový čas Europe/Prague bez značky zóny.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: cl_partners_category_id, changed_since, after_id, limit, offset'
                  example: '{"after_id":0,"limit":500}'
      responses:
        '200':
          description: Partners page
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ReportingPage'
                  - type: object
                    properties:
                      data: { type: array, items: { $ref: '#/components/schemas/ReportingPartner' } }
        '400':
          description: Neplatné datum nebo after_id
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - chybí právo read na modul

  /reporting/get-partner-branches:
    post:
      tags: [Reporting]
      summary: Partner branches for BI
      description: |
        Pobočky partnerů — bez kontaktů (`b_phone`, `b_email`, `b_person`);
        viz popis schématu `ReportingPartnerBranch`.
        `changed` je u pobočky sloupec typu `date` (`cl_partners_branch.changed`),
        vrací se jako `Y-m-d 00:00:00`.

        Filtry: `cl_partners_book_id`, `changed_since`. Omezení na úrovni záznamu
        se bere z partnera. Vyžaduje právo `read` na Adresář partnerů.

        `changed_since` vrací i řádky, jejichž hlavička se změnila (datum, partner, stav).

        Stránkování: pošlete `after_id` (na první stránce 0) a pokračujte s `next_after_id`
        z odpovědi, dokud není `null`. S `after_id` se `total` nepočítá (je `null`) — u
        velkých firem je to řádově rychlejší. Bez `after_id` funguje `offset` a vrací se
        `total`. `limit` max 500 (vyšší se tiše ořízne). Řazeno podle `id`. Časy jsou
        serverový čas Europe/Prague bez značky zóny.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: cl_partners_book_id, changed_since, after_id, limit, offset'
                  example: '{"cl_partners_book_id":123,"after_id":0,"limit":500}'
      responses:
        '200':
          description: Partner branches page
          content:
            application/json:
              schema:
                allOf:
                  - $ref: '#/components/schemas/ReportingPage'
                  - type: object
                    properties:
                      data: { type: array, items: { $ref: '#/components/schemas/ReportingPartnerBranch' } }
        '400':
          description: Neplatné datum nebo after_id
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - chybí právo read na modul

  /invoicearrived/get-one/{id}:
    post:
      tags: [Invoice Arrived]
      summary: Arrived invoice detail with nested payments/commissions/files
      description: |
        Detail faktury vč. `payments[]` (úhrady), `commissions[]` (rozpuštění nákladu na zakázky)
        a `files[]` (přílohy). Faktura jiné firmy vrací 404, ne 403.
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
      responses:
        '200':
          description: Detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/InvoiceArrivedDetail' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Invoice not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoicearrived/create:
    post:
      tags: [Invoice Arrived]
      summary: Create arrived invoice
      description: |
        Založí přijatou fakturu. Nevyplněné `inv_number` se přidělí z číselné řady
        typu `invoice_arrived`; vlastní `inv_number` musí být v rámci firmy unikátní.
        Nevyplněná `due_date` se dopočítá dle splatnosti karty dodavatele, jinak dle nastavení firmy.
        Nevyplněná `currency_rate` se dosadí z `fix_rate` měny dokladu (jinak 1).
        Nevyplněný `cl_status_id` se nastaví na stav s příznakem `s_new`.

        **Přepočet DPH:** server dopočítá `price_vat*`, `price_total*`, `price_e2` a `price_e2_vat`
        ze základů a sazeb (`price_vat1 = round(price_base1 * vat1 / 100, 2)`,
        `price_total1 = round(price_base1 + price_vat1, 2)`,
        `price_e2 = round(price_base0..3 + price_correction, 2)`,
        `price_e2_vat = round(price_e2 + price_vat1..3, 2)`).
        `price_correction` vstupuje už do `price_e2`. Při `recalc_disabled = 1` se přeskočí
        **jen** výpočet `price_vat*` / `price_total*` (daň se vezme tak, jak ji poslal klient),
        součty `price_e2` a `price_e2_vat` se počítají dál. Celý přepočet se vynechá pouze tehdy,
        když klient pošle `price_e2_vat` explicitně.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
                dataJSON:
                  type: string
                  description: |
                    JSON s poli faktury. Povinné: `cl_partners_book_id` (dodavatel, musí patřit
                    autorizované firmě), `rinv_number` (číslo faktury dodavatele).
                    Nepovinné: `inv_number`, `cl_number_series_id`, `inv_title`, `inv_title2`,
                    `inv_memo`, `inv_date`, `arv_date`, `vat_date`, `due_date`, `var_symb`,
                    `konst_symb`, `spec_symb`, `od_number`, `delivery_number`, `header_txt`,
                    `header_show`, `footer_txt`, `footer_show`, `cl_partners_branch_id`,
                    `cl_partners_book_workers_id`, `cl_status_id` (status_use = invoice_arrived),
                    `cl_users_id`, `cl_center_id`, `cl_company_branch_id` (pobočka firmy – u firem
                    s pobočkami doporučeno vyplnit), `cl_commission_id`, `cl_invoice_types_id`,
                    `cl_payment_types_id`, `cl_currencies_id`, `currency_rate`, `price_base0`–`price_base3`,
                    `vat1`–`vat3`, `price_correction`, `pdp`, `import`, `recalc_disabled`
                    a případně explicitní `price_vat1`–`price_vat3`, `price_total1`–`price_total3`,
                    `price_e2`, `price_e2_vat` (viz přepočet DPH v popisu).
                    Dále nepovinné: `cl_partners_account_id` (účet z karty dodavatele; má přednost),
                    `partner_account` (string – účet dodavatele v CZ formátu `[předčíslí-]číslo/kód_banky`
                    nebo IBAN bez mezer). `partner_account` se dohledá mezi účty dodavatele; při neshodě
                    se založí nový účet na jeho kartě a naváže na fakturu. Nevalidní formát se ignoruje.
                  example: '{"cl_partners_book_id":464305,"rinv_number":"API-TEST-1","inv_title":"API test faktura","inv_date":"2026-07-23","price_base1":1000,"vat1":21,"price_correction":-0.4}'
      responses:
        '200':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: object
                    properties:
                      id: { type: integer, example: 141444 }
                      inv_number: { type: string, example: 'F447' }
                  warnings:
                    type: array
                    description: |
                      Upozornění, která nebrání založení dokladu. Objeví se jen když nějaké je.
                      Dnes jediné: v evidenci už je faktura se stejným číslem, dodavatelem
                      a datem vystavení.
                    items: { type: string }
                    example: ['Faktura 1000118921 od tohoto dodavatele s datem 29.05.2025 už je v evidenci pod číslem 250380.']
        '400':
          description: |
            Validation error – chybí `cl_partners_book_id` nebo `rinv_number`, neplatný formát data,
            neplatný stav, duplicitní `inv_number`, `vat_date`/`arv_date` před uzávěrkou DPH,
            nebo pro přijaté faktury není nastavená číselná řada
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Partner not found (dodavatel neexistuje nebo patří jiné firmě)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoicearrived/update:
    post:
      tags: [Invoice Arrived]
      summary: Update arrived invoice header (partial)
      description: |
        Částečná úprava hlavičky – posílají se jen měněná pole, pro nedodaná se při přepočtu DPH
        berou hodnoty z uloženého řádku (změna jediné sazby neshodí zbytek rozpisu).

        **Neměnitelná pole:** `price_payed`, `pay_date`, `advance_payed`, `price_on_commission`,
        `cl_payment_order_id`, `cl_collection_order_id` ani `locked` se přes `update` nastavit nedají
        (požadavek je tiše ignoruje) – mění je dedikované endpointy, interní přepočty, nebo jen aplikace.

        Po uložení server sám přepočítá úhrady (`price_payed`, `pay_date`), takže navýšení částek
        u dříve uhrazené faktury ji přestane označovat jako uhrazenou a `price_remaining` ukáže doplatek.

        Uzávěrka DPH se kontroluje na **efektivních** hodnotách (co klient poslal, jinak co je uložené),
        takže fakturu v uzavřeném období nelze změnit ani částečným updatem bez datumových polí.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
                dataJSON:
                  type: string
                  description: |
                    JSON: `id` (int, povinné) + měněná pole – `inv_number`, `rinv_number`, `inv_title`,
                    `inv_title2`, `inv_memo`, `var_symb`, `konst_symb`, `spec_symb`, `od_number`,
                    `delivery_number`, `header_txt`, `header_show`, `footer_txt`, `footer_show`,
                    `inv_date`, `arv_date`, `vat_date`, `due_date`, `cl_partners_book_id`,
                    `cl_partners_branch_id`, `cl_partners_book_workers_id`, `cl_status_id`,
                    `cl_center_id`, `cl_company_branch_id`, `cl_commission_id`, `cl_users_id`,
                    `cl_currencies_id`, `currency_rate`, `cl_payment_types_id`, `cl_invoice_types_id`,
                    `cl_number_series_id`, `price_base0`–`price_base3`, `vat1`–`vat3`, `price_correction`,
                    `price_e2`, `price_e2_vat`, `price_vat1`–`price_vat3`, `price_total1`–`price_total3`,
                    `pdp`, `import`, `recalc_disabled`.
                    Dále `cl_partners_account_id` (má přednost) a `partner_account` (CZ účet nebo IBAN;
                    dohledá/založí účet dodavatele a naváže na fakturu; nevalidní se ignoruje).
                  example: '{"id":141444,"price_base1":2000,"vat1":21}'
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: object, properties: { id: { type: integer } } }
                  warnings:
                    type: array
                    description: |
                      Upozornění, která nebrání uložení změn. Objeví se jen když nějaké je.
                      Dnes jediné: v evidenci už je faktura se stejným číslem, dodavatelem
                      a datem vystavení.
                    items: { type: string }
                    example: ['Faktura 1000118921 od tohoto dodavatele s datem 29.05.2025 už je v evidenci pod číslem 250380.']
        '400':
          description: |
            Validation error – chybí `id`, neplatný formát data, `null` do sloupce NOT NULL
            (např. `inv_title`, `price_base1`), neplatný stav, duplicitní `inv_number`,
            nebo efektivní `vat_date`/`arv_date` před uzávěrkou DPH
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Invoice not found / Partner not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Faktura je zamčená (locked = 1), nelze ji měnit'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoicearrived/delete/{id}:
    post:
      tags: [Invoice Arrived]
      summary: Delete arrived invoice (guard na navázaná data)
      description: |
        Fakturu nelze smazat, pokud je zamčená (`locked = 1`), má úhrady, rozpuštění na zakázky,
        přílohy nebo navázaný platební / inkasní příkaz – vrací 409 se srozumitelným výčtem důvodů.
        Mazání faktury, jejíž uložené `vat_date` / `arv_date` spadá před uzávěrku DPH, vrací také 409,
        stejně jako neošetřená vazba v databázi (cizí klíč), která se nemapuje na 500.
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Invoice not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: |
            Fakturu nelze smazat – zámek, úhrady, rozpuštění na zakázky, přílohy,
            navázaný platební / inkasní příkaz, uzávěrka DPH nebo chyba cizího klíče
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoicearrived/set-status:
    post:
      tags: [Invoice Arrived]
      summary: Change arrived invoice status
      description: |
        Přepne stav faktury. Stav musí patřit číselníku přijatých faktur
        (`cl_status.status_use = invoice_arrived`), jinak 400.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
                dataJSON:
                  type: string
                  description: 'JSON: `id` (int, povinné), `cl_status_id` (int, povinné – stav se status_use = invoice_arrived)'
                  example: '{"id":141444,"cl_status_id":12}'
      responses:
        '200':
          description: Status changed
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: object
                    properties:
                      id: { type: integer }
                      cl_status_id: { type: integer }
                      status_name: { type: string }
        '400':
          description: 'Chybí `id` nebo `cl_status_id`, případně Invalid status (stav nepatří k status_use = invoice_arrived)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Invoice not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Faktura je zamčená (locked = 1)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoicearrived/pay-full:
    post:
      tags: [Invoice Arrived]
      summary: Pay the remaining amount of the invoice in one call
      description: |
        Doplatí zbytek faktury jedním voláním – založí řádek úhrady na částku `price_remaining`
        a nastaví `price_payed`, `pay_date` a finální stav faktury.
        Opakované volání nad již uhrazenou fakturou vrací 409 („Faktura již byla dříve uhrazena").
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
                dataJSON:
                  type: string
                  description: 'JSON: `id` (int, povinné), `pay_date` (Y-m-d, nepovinné – výchozí dnes)'
                  example: '{"id":141444}'
      responses:
        '200':
          description: Paid
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: object
                    properties:
                      id: { type: integer, example: 141444 }
                      payment_id: { type: integer, nullable: true, example: 114905 }
        '400':
          description: 'Chybí `id` nebo neplatný formát `pay_date`'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Invoice not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: |
            Faktura je zamčená (locked = 1), faktura již byla dříve uhrazena,
            nebo doklad není dostupný v aktuální pobočce
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoicearrived/create-payment:
    post:
      tags: [Invoice Arrived Payments]
      summary: Create a (partial) payment of an arrived invoice
      description: |
        Zapíše dílčí úhradu. `item_order` server dosadí jako MAX+1. Po zápisu se přepočítá
        `price_payed` / `pay_date` faktury; u hotovostní formy úhrady vzniká pokladní doklad.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
                dataJSON:
                  type: string
                  description: |
                    JSON: `cl_invoice_arrived_id` (int, povinné), `pay_price` (number, povinné),
                    `pay_date` (Y-m-d, nepovinné – výchozí dnes), `pay_doc` (string, nepovinné),
                    `pay_type` (int, nepovinné), `pay_vat` (int, nepovinné), `vat` (number, nepovinné),
                    `cl_payment_types_id` (int, nepovinné – výchozí forma úhrady faktury),
                    `cl_currencies_id` (int, nepovinné – výchozí měna faktury),
                    `cl_users_id` (int, nepovinné).
                  example: '{"cl_invoice_arrived_id":141444,"pay_price":500,"pay_date":"2026-07-24"}'
      responses:
        '200':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/InvoiceArrivedPayment' }
        '400':
          description: 'Chybí `cl_invoice_arrived_id` nebo `pay_price`, případně neplatný formát `pay_date`'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Invoice not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Faktura je zamčená (locked = 1)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoicearrived/update-payment:
    post:
      tags: [Invoice Arrived Payments]
      summary: Update a payment (partial)
      description: |
        Úprava úhrady; po uložení se přepočítá `price_payed` / `pay_date` faktury.
        `null` do sloupce NOT NULL (`pay_price`, `pay_doc`, `pay_type`, `pay_vat`, `vat`) vrací 400 –
        MySQL by ho v nestriktním režimu tiše nahradil nulou.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
                dataJSON:
                  type: string
                  description: |
                    JSON: `id` (int, povinné) + měněná pole – `pay_price`, `pay_date`, `pay_doc`,
                    `pay_type`, `pay_vat`, `vat`, `cl_payment_types_id`, `cl_currencies_id`.
                  example: '{"id":114905,"pay_price":600}'
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/InvoiceArrivedPayment' }
        '400':
          description: 'Chybí `id`, neplatný formát `pay_date`, nebo `null` do sloupce NOT NULL'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Payment not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Faktura je zamčená (locked = 1)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoicearrived/delete-payment/{id}:
    post:
      tags: [Invoice Arrived Payments]
      summary: Delete a payment
      description: 'Po smazání se přepočítá `price_payed` / `pay_date` faktury.'
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Payment not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Faktura je zamčená (locked = 1)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoicearrived/create-commission:
    post:
      tags: [Invoice Arrived Commissions]
      summary: Allocate invoice cost to a commission
      description: |
        Rozpustí náklad faktury na zakázku. `item_order` server dosadí jako MAX+1;
        po zápisu se přepočítá `price_on_commission` na hlavičce faktury.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
                dataJSON:
                  type: string
                  description: |
                    JSON: `cl_invoice_arrived_id` (int, povinné), `cl_commission_id` (int, povinné –
                    zakázka musí patřit autorizované firmě), `amount` (number, povinné),
                    `into_costs` (int, nepovinné), `note` (string, nepovinné), `cl_users_id` (int, nepovinné).
                  example: '{"cl_invoice_arrived_id":141444,"cl_commission_id":1001,"amount":400}'
      responses:
        '200':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/InvoiceArrivedCommission' }
        '400':
          description: 'Chybí `cl_invoice_arrived_id`, `cl_commission_id` nebo `amount`'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Invoice not found / Commission not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Faktura je zamčená (locked = 1)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoicearrived/update-commission:
    post:
      tags: [Invoice Arrived Commissions]
      summary: Update a cost allocation (partial)
      description: 'Po uložení se přepočítá `price_on_commission` na hlavičce faktury.'
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
                dataJSON:
                  type: string
                  description: |
                    JSON: `id` (int, povinné) + měněná pole – `cl_commission_id` (zakázka musí patřit
                    autorizované firmě), `amount`, `into_costs`, `note`, `item_order`.
                  example: '{"id":8801,"amount":450}'
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/InvoiceArrivedCommission' }
        '400':
          description: 'Chybí `id`, nebo `null` do sloupce NOT NULL (`amount`, `into_costs`, `note`, `item_order`)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Allocation not found / Commission not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Faktura je zamčená (locked = 1)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoicearrived/delete-commission/{id}:
    post:
      tags: [Invoice Arrived Commissions]
      summary: Delete a cost allocation
      description: 'Po smazání se přepočítá `price_on_commission` na hlavičce faktury.'
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Allocation not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Faktura je zamčená (locked = 1)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoicearrived/get-files/{id}:
    post:
      tags: [Invoice Arrived Files]
      summary: List arrived invoice files
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer, description: 'ID faktury' } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
      responses:
        '200':
          description: File list
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/FileItem' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Invoice not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoicearrived/file-upload:
    post:
      tags: [Invoice Arrived Files]
      summary: Upload an arrived invoice file (base64 data URL)
      description: |
        Nahraje přílohu k faktuře (typicky sken nebo fotka dokladu).
        Zamčení faktury (`locked = 1`) přílohám záměrně nebrání.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
                dataJSON:
                  type: string
                  description: |
                    JSON objekt: { cl_invoice_arrived_id, file: FileUploadInput }.
                    file.dataUrl = data:<mime>;base64,<...> (viz schema FileUploadInput)
                  example: '{"cl_invoice_arrived_id":141444,"file":{"name":"faktura-test.txt","type":"text/plain","size":42,"dataUrl":"data:text/plain;base64,..."}}'
      responses:
        '200':
          description: Uploaded
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: object
                    properties:
                      id: { type: integer }
                      file_name: { type: string }
        '400':
          description: 'Chybí `cl_invoice_arrived_id` nebo `file` s `dataUrl`, případně Invalid file data (base64 decode failed)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Invoice not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoicearrived/file-download/{id}:
    post:
      tags: [Invoice Arrived Files]
      summary: Download an arrived invoice file (binary)
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer, description: 'ID přílohy' } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
      responses:
        '200':
          description: Binary file content
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: File not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoicearrived/file-delete/{id}:
    post:
      tags: [Invoice Arrived Files]
      summary: Delete an arrived invoice file
      description: 'Zamčení faktury (`locked = 1`) mazání přílohy nebrání.'
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer, description: 'ID přílohy' } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: File not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoicearrived/parse-file:
    post:
      tags: [Invoice Arrived Files]
      summary: Bezstavové parsování ISDOC souboru dodavatelské faktury
      description: |
        Zparsuje nahraný soubor a vrátí hlavičku ve tvaru polí `create` endpointu.
        Soubor se nikam neukládá (přílohu appka nahraje zvlášť přes `file-upload`).
        Podporuje `.isdoc` (XML), `.isdocx` (ZIP) a ISDOC-PDF (PDF s vloženou `.isdoc`).
        Poškozený/neparsovatelný/neznámý obsah není chyba → `detected: "none"`, `header: null`.
        (QR-z-PDF zatím není podporováno.)
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
                dataJSON:
                  type: string
                  description: 'JSON `{ file: FileUploadInput }` – `file.dataUrl = data:<mime>;base64,<...>`.'
                  example: '{"file":{"name":"faktura.isdoc","type":"application/xml","size":2130,"dataUrl":"data:application/xml;base64,PD94..."}}'
      responses:
        '200':
          description: OK (i pro nerozpoznaný obsah – detected=none)
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: object
                    properties:
                      detected: { type: string, enum: [isdoc, isdoc_pdf, qr, none] }
                      header:
                        type: object
                        nullable: true
                        description: 'Pole ve tvaru create; supplier/bank informativní (do create se neposílají).'
                        properties:
                          rinv_number: { type: string, nullable: true }
                          inv_title: { type: string, nullable: true }
                          inv_date: { type: string, nullable: true }
                          vat_date: { type: string, nullable: true }
                          due_date: { type: string, nullable: true }
                          var_symb: { type: string, nullable: true }
                          konst_symb: { type: string, nullable: true }
                          currency_code: { type: string, nullable: true }
                          price_base0: { type: number }
                          price_base1: { type: number }
                          price_base2: { type: number }
                          price_base3: { type: number }
                          vat1: { type: number }
                          vat2: { type: number }
                          vat3: { type: number }
                          price_vat1: { type: number }
                          price_vat2: { type: number }
                          price_vat3: { type: number }
                          price_e2_vat: { type: number }
                          supplier:
                            type: object
                            nullable: true
                            properties:
                              name: { type: string, nullable: true }
                              ico: { type: string, nullable: true }
                              dic: { type: string, nullable: true }
                          bank:
                            type: object
                            nullable: true
                            properties:
                              account: { type: string, nullable: true }
                              iban: { type: string, nullable: true }
                              swift: { type: string, nullable: true }
        '400':
          description: Chybí `file`/`dataUrl` nebo nevalidní base64
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoicearrived/get-statuses:
    post:
      tags: [Invoice Arrived Lookups]
      summary: List arrived invoice statuses
      description: 'Číselník stavů přijaté faktury (cl_status, status_use = invoice_arrived), řazeno dle status_name.'
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
      responses:
        '200':
          description: Status list
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/InvoiceArrivedStatus' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoicearrived/get-payment-types:
    post:
      tags: [Invoice Arrived Lookups]
      summary: List payment types
      description: 'Číselník forem úhrady (cl_payment_types, jen not_active = 0), řazeno dle name.'
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
      responses:
        '200':
          description: Payment type list
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/PaymentType' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoicearrived/get-currencies:
    post:
      tags: [Invoice Arrived Lookups]
      summary: List currencies with rates
      description: |
        Číselník měn (cl_currencies) vč. aktuálního kurzu, řazeno dle currency_code.
        `is_default = true` u výchozí měny firmy.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
      responses:
        '200':
          description: Currency list
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Currency' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoicearrived/get-centers:
    post:
      tags: [Invoice Arrived Lookups]
      summary: List centers
      description: 'Číselník středisek (cl_center), řazeno dle name.'
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
      responses:
        '200':
          description: Center list
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Center' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoicearrived/get-invoice-types:
    post:
      tags: [Invoice Arrived Lookups]
      summary: List arrived invoice document types
      description: 'Číselník druhů dokladu přijatých faktur (cl_invoice_types, inv_type = 4), řazeno dle name.'
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
      responses:
        '200':
          description: Invoice type list
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/InvoiceType' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoicearrived/get-vat-rates:
    post:
      tags: [Invoice Arrived Lookups]
      summary: List VAT rates valid at a date
      description: 'Sazby DPH platné k zadanému datu pro zemi autorizované firmy.'
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
                dataJSON:
                  type: string
                  description: 'JSON: `date` (Y-m-d, nepovinné – výchozí dnes)'
                  example: '{"date":"2026-07-23"}'
      responses:
        '200':
          description: VAT rate list
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/VatRate' }
        '400':
          description: 'Neplatný formát data v poli `date`'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /cash/get-all:
    post:
      tags: [Cash]
      summary: List cash documents
      description: |
        Výpis pokladních dokladů autorizované firmy, řazeno `inv_date DESC, cash_number DESC`.
        Vrací `total` = počet záznamů odpovídajících filtru před stránkováním.
        Autorizace jen `bearer_token` (sync_token zde neplatí). Vyžaduje právo `read`.

        Kromě firmy platí stejná omezení na úrovni záznamu jako ve webu: pobočka uživatele
        (má-li ji vyplněnou) a role „Jen vlastní záznamy" (pak jen doklady s vlastním
        cl_users_id nebo bez vlastníka). Uplatňují se shodně u všech akcí modulu.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    JSON s filtry (všechny nepovinné, lze kombinovat):
                    - `cl_cash_def_id` (int) – doklady pokladny
                    - `cl_partners_book_id` (int) – doklady partnera
                    - `cl_center_id` (int) – doklady střediska
                    - `cl_currencies_id` (int) – doklady v měně
                    - `type` (`in`/`out`) – jen příjem (cash >= 0) nebo výdej (cash < 0)
                    - `search` (string) – LIKE přes cash_number, title a název partnera
                    - `date_from` / `date_to` (Y-m-d) – rozsah data dokladu (inv_date)
                    - `changed_since` (Y-m-d H:i:s) – doklady změněné nebo založené od okamžiku (synchronizace appky)
                    - `limit` (int, výchozí 50) – ořízne se do rozsahu 1–500
                    - `offset` (int, výchozí 0) – záporná hodnota se bere jako 0
                  example: '{"type":"out","limit":20}'
      responses:
        '200':
          description: List
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/CashListItem' }
                  total: { type: integer, description: 'Počet záznamů odpovídajících filtru před stránkováním', example: 12 }
        '400':
          description: 'Neplatná hodnota parametru type (povoleno in, out)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu pokladna nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /cash/get-one/{id}:
    post:
      tags: [Cash]
      summary: Cash document detail
      description: |
        Detail dokladu vč. `files[]` (přílohy) a `paired_docs[]` (doklady spárované přes cl_paired_docs).
        Doklad jiné firmy vrací 404, ne 403. Vyžaduje právo `read`.
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: Detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/CashDetail' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu pokladna nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Cash document not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /cash/create:
    post:
      tags: [Cash]
      summary: Create cash document
      description: |
        Založí pokladní doklad. Vyžaduje právo `write`. Doklad vzniká **nezamčený**
        (`locked = 0`) – API nepoužívá `CashManager::makeCash()`, ta cesta je vyhrazená
        dokladům generovaným z jiného dokladu (úhrada faktury, prodejka).

        **Token pokladního zařízení (`device_token`, krok 3c).** Obsahuje-li tělo
        požadavku `device_token` (i prázdný), presenter přepne do režimu zařízení:
        autorizuje ho `PosDeviceAuthTrait` (stejně jako `/sale/create`) místo
        `bearer_token`/`api_token` a kontrola práva `write` se přeskočí. Obsluhu
        nese `cl_users_id` z těla – ověří ji `PosCashier::check()` (člen firmy
        zařízení, aktivní, právo zápisu do **Prodejny** – ne do Pokladny –
        a licence modulu) a stane se jednajícím uživatelem (`create_by`).

        **Licence Prodejny (spec `2026-09-29-pos-sale-license-seats-design.md`).**
        Má-li obsluha místo v licenci, doklad založí a transakce ho obsadí, pokud
        ho ještě neměla. Nemá-li místo a licence je plná, doklad se **přesto
        uloží** (nezamítá se jako u `/sale/create`/`/sale/create-correction`) –
        **odpověď `/cash/create` ale žádné pole `warnings` nemá**, takže
        `license_exceeded` appka nevidí. Zapíše se jen do `cl_pos_sale_warnings`
        s odkazem na tento doklad a projeví se v denním souhrnu správci
        (`docs/ODPOVED-backend-pokladna.md` oddíl N). Stejně obsluha bez platné
        licence (modul Prodejna chybí / prošel, licence prošlá, konec zkušební
        doby): doklad se uloží bez obsazení místa a v denním souhrnu je
        `license_expired` (oddíl N2a) - jen když doklad (`inv_date` jako ten den,
        bez něj teď) vznikl ještě za platnosti licence, jinak 403 `license_ended`
        (oddíl N2b).

        V tomto režimu:
        - `api_request_key` a `cl_users_id` jsou **povinné** (jinak 400
          `invalid_input`); ostatní pole `dataJSON` se nemění;
        - `cl_cash_def_id` a `cl_company_branch_id` se vezmou ze zařízení (stejná
          kaskáda jako při párování), poslané hodnoty klienta se **ignorují**;
        - opakování se stejným `api_request_key` se vyhodnotí **před** ověřením
          obsluhy – už přijatý doklad appka dostane i po deaktivaci obsluhy;
        - chyby chodí jako **problem+json** s polem `error` (schéma `PosProblem`),
          ne jako `ErrorResponse` – stejný tvar jako `/api/pos/*` a
          `/api/sale/create`. Cizí nebo neexistující číselník z těla (jinde 404)
          je tu chybný vstup → **400** `invalid_input`;
        - jakákoli jiná akce presenteru (`update`, `get-all`, `get-one`, …) s
          `device_token` v těle vrací **401** `invalid_device_token` bez pokusu
          o jinou autorizaci – token zařízení platí jen pro `create`.

        Stávající klienti (`bearer_token`/`api_token`, bez `device_token`) se nemění.

        Číslo dokladu (`cash_number`) se vždy přidělí z číselné řady `cash_in`/`cash_out`
        podle směru. Poslaný `cl_number_series_id` musí být pokladní řada (`form_use`
        `cash_in`/`cash_out`) odpovídající směru – jinak 400. Řada jiného druhu by dokladu
        přidělila cizí číslo a trvale posunula její čítač.

        Nevyplněný `cl_status_id` se nastaví na stav s příznakem `s_new`.
        Nevyplněná `currency_rate` se dosadí z `fix_rate` měny dokladu (jinak 1); poslaná
        hodnota musí být číslo, jinak 400. Nevyplněný `cl_currencies_id` se dosadí z nastavení
        firmy. Nevyplněné `inv_date` se nastaví na dnešek. Nevyplněný `cl_users_id` se nastaví
        na přihlášeného uživatele. Nevyplněný `cl_cash_def_id` se nastaví na výchozí pokladnu
        firmy (`def_cash = 1`) a nevyplněný `cl_company_branch_id` na pobočku uživatele –
        obojí jako ve webovém formuláři.

        **Směr a znaménko:** je-li poslán `direction`, musí to být přesně `in` nebo `out`
        (jiná hodnota → 400) a platí explicitně; jinak se směr odvodí z `cl_invoice_types_id`
        (inv_type=6 → výdej); jinak je doklad příjem. Server podle výsledného směru sám nastaví
        znaménko `cash` (výdej záporně, příjem kladně) bez ohledu na znaménko od klienta.

        Doklad nelze založit mimo vlastní dosah – uživatel přiřazený na pobočku nesmí zvolit
        jinou a při roli „Jen vlastní záznamy" nesmí doklad přiřadit jinému uživateli (400).

        **`eet_relevant`** (evidence do EET): pošle-li klient `1`/`0` (i jako řetězec nebo
        bool), platí to – klient má vždy poslední slovo. Nepošle-li pole vůbec, dosadí se
        výchozí podle směru, stejně jako web předvyplňuje nový doklad: příjem (`in`) `1`,
        výdaj (`out`) `0`.

        Pošle-li klient **jinou** hodnotu, vrací se **400** a neuloží se nic. Uznává se jen
        `1`, `"1"`, `true`, `0`, `"0"`, `false` – záměrně ani `1.0`, `" 1"`, `"true"` nebo
        `"ano"`. U pole, jehož důsledkem je nevratně zaevidovaná tržba u finanční správy,
        se nic nehádá; pozor zejména na `0.0` z JSONu, které dřív tiše skončilo jako `1`.

        **Firma bez zapnutého EET: poslaná hodnota se ignoruje, není to chyba.** Nemá-li
        firma `eet_active` ani `eet_test`, uloží se vždy `0`, i když klient výslovně pošle
        `eet_relevant = 1` – a odpověď pak vrátí `eet_relevant = 0`. Nevrací se 400 ani
        žádné upozornění; je to záměr, ne vada. Web u takové firmy volbu „Evidovat do EET"
        do formuláře vůbec nepřidá, takže sloupec zůstane na výchozí `0`, a API se chová
        stejně. Klient, který potřebuje mít jistotu, si výsledek přečte z odpovědi.

        Po uložení doklad zpracuje `EetCashService::sendForCashDoc()` stejně jako web:
        doklad, který evidenci podléhá – `eet_relevant = 1`, nenulová částka, není to
        prodejka ani doklad dodacího listu, ze kterého ještě nevznikla faktura – se rovnou
        zaeviduje v EET a dostane vazbu `cl_eet_id`. Bez `eet_relevant = 1` evidence
        neproběhne.

        Selhání evidence uložení dokladu **nezruší** – odpověď je i tak 200 a doklad
        v pokladně zůstává. Přijatá hotovost je fakt, který se zapsat musí.

        Co se stane dál, závisí na tom, kde se to zlomí. Selže-li samotné odeslání
        (výpadek sítě, neplatný certifikát, chyba při sestavení nebo podpisu zprávy),
        zapíše se neúspěšný pokus do `cl_eet` se stavem chyby, takže ho převezme fronta
        doposílání a ohlídá 48hodinovou lhůtu. **Neplatí to ale, když se k odeslání vůbec
        nedojde** – typicky když se nepodaří sestavit podpisová data firmy nebo provozovny
        (`CompaniesManager::getDataForSignEET()` vrátí `false`). Tam nevznikne nic: žádný
        řádek v `cl_eet`, žádný záznam v logu, a fronta doposílání tedy nemá co vyzvednout.
        Uloženou hodnotu vrací odpověď (`eet_relevant`).
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token:
                  type: string
                  description: |
                    **Zápis přes integrační token není podporovaný.** Token je vždy jen
                    ke čtení (viz schéma `apiToken`), takže založení dokladu skončí 403
                    ještě před jakýmkoli zápisem. Pokladní doklad lze přes API založit
                    pouze s `bearer_token`.
                bearer_token: { type: string }
                device_token:
                  type: string
                  description: |
                    Token pokladního zařízení (krok 3c) – přítomnost klíče (i s prázdnou
                    hodnotou) přepne požadavek do režimu zařízení, viz popis výše a schéma
                    zabezpečení `deviceToken`. Nahrazuje `bearer_token`/`api_token` a
                    přeskočí kontrolu práva `write`; `cl_users_id` v `dataJSON` se pak
                    ověřuje jako obsluha (`PosCashier`), ne jako cizí klíč.
                dataJSON:
                  type: string
                  description: |
                    JSON s poli dokladu. Povinné: `cash` (number). Nepovinné: `direction`
                    (`in`/`out`), `title`, `description_txt`, `inv_date`, `cl_cash_def_id`,
                    `cl_partners_book_id`, `cl_partners_book_workers_id`, `cl_center_id`,
                    `cl_company_branch_id`, `cl_invoice_types_id`, `cl_currencies_id`,
                    `currency_rate`, `cl_status_id`, `cl_users_id`, `cl_number_series_id`,
                    `eet_relevant` (jen 0/1, evidence do EET – výchozí `1` u příjmu, `0` u výdaje;
                    jiná hodnota → 400), `api_request_key`. Každý poslaný cizí klíč se ověřuje
                    proti autorizované firmě. S `device_token` jsou navíc povinné
                    `api_request_key` a `cl_users_id` (obsluha) a `cl_cash_def_id` s
                    `cl_company_branch_id` se ignorují (viz popis výše).

                    **`api_request_key` – ochrana proti dvojímu založení.** Doklad se uloží
                    a tržba odejde do EET, ale odpověď se ke klientovi nemusí dostat (vyprší
                    timeout, spadne síť). Když klient volání zopakuje bez klíče, vznikne druhý
                    doklad i druhá tržba – u finanční správy nevratná a doklad s přiděleným
                    POK už nejde smazat. Proto:

                    - pošlete ke každému dokladu vlastní jednoznačný klíč (např. UUID
                      vygenerované při uzavření účtenky) a při opakování tentýž;
                    - opakování se stejným klíčem a stejným obsahem nic nezaloží, vrátí původní
                      doklad (200) s hlavičkou `Idempotent-Replayed: true` a evidenci znovu
                      nespouští;
                    - stejný klíč s jiným obsahem skončí 409 – klíč nepoužívejte znovu pro
                      jiný doklad, nový doklad by se jinak nezaložil ani nezaevidoval;
                    - klíč: 1 až 64 znaků z písmen, číslic a `. _ : -`, platí v rámci firmy;
                      jiný tvar → 400.

                    Pole je nepovinné; bez něj se API chová jako dřív, jen bez ochrany.
                    Důrazně doporučeno u firem se zapnutým EET.
                  example: '{"direction":"out","cash":500,"title":"Nákup kancelářských potřeb","cl_cash_def_id":1,"inv_date":"2026-08-05","api_request_key":"5f0c3a8e-2b1d-4c6e-9a7f-1e2d3c4b5a69"}'
      responses:
        '200':
          description: |
            Created. Při opakování se stejným `api_request_key` a stejným obsahem vrací
            původní doklad a nastaví hlavičku `Idempotent-Replayed: true`.
          headers:
            Idempotent-Replayed:
              description: '`true`, když jde o opakovaný požadavek a doklad nevznikl znovu'
              schema: { type: string, enum: ['true'] }
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/CashListItem' }
        '400':
          description: |
            Validation error – chybí nebo není číslo `cash`, `currency_rate` není číslo,
            neplatný formát data v `inv_date`, neplatná hodnota `direction`, neplatná hodnota
            `eet_relevant` (povoleno jen 0/1), neplatný tvar `api_request_key`, číselná řada
            není pokladní nebo neodpovídá směru, zápis mimo vlastní pobočku či cizímu
            uživateli, nebo pro pokladní doklady daného směru není nastavená číselná řada.

            **Token zařízení:** stejné validace navíc chybějící `api_request_key` nebo
            neplatná/chybějící obsluha `cl_users_id`; cizí nebo neexistující číselník z
            těla, který by jinak vrátil 404, je tu taky 400 `invalid_input`. Tvar odpovědi
            je `PosProblem`, ne `ErrorResponse`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }
        '401':
          description: |
            Unauthorized. Token zařízení: `invalid_device_token` (`PosProblem`) – token
            chybí, neexistuje, nebo je zařízení odvolané (`PosDeviceGuard::deny()`); stejný
            kód dostane i jakákoli jiná akce presenteru (`update`, `get-all`, …), které
            přišel `device_token` v těle – token zařízení platí jen pro `create`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }
        '403':
          description: |
            Chybí přístup k modulu pokladna nebo právo write. Token zařízení: `invalid_user`
            (`PosProblem`) – obsluha `cl_users_id` není členem firmy zařízení, je
            neaktivní/smazaná nebo nemá právo zápisu do Prodejny. **Licenční důvody
            403 nevrací** – bez místa v plné licenci i bez platné licence (licence
            obsluhy nemá modul Prodejna, modul Prodejna v licenci prošel (`exp`),
            licence prošla, zkušební doba skončila, tarif Prodejnu neumožňuje) se doklad
            přijme (viz popis licence výše, `license_exceeded` / `license_expired` jen
            v denním souhrnu). Výjimka: `license_ended` (`PosProblem`) - obsluha bez
            platné licence a doklad (`inv_date` jako ten den od 00:00, bez něj čas
            přijetí) vznikl až po konci platnosti licence Prodejny.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }
        '404':
          description: |
            Cizí klíč poslaný klientem neexistuje nebo patří jiné firmě (Cash register,
            Partner, Partner contact, Center, Company branch, Invoice type, Currency,
            Status, Number series, User not found). Token zařízení tenhle kód nevrací –
            stejný případ je tam 400 (viz výše).
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: |
            `api_request_key` už byl ve firmě použit pro doklad s jiným obsahem, nebo
            doklad s tímto klíčem existuje, ale volající k němu nemá přístup. Nic se
            nezaložilo ani nezaevidovalo. Token zařízení: stejný případ, `PosProblem`
            s `error: request_key_conflict`.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }
        '500':
          description: |
            Internal error. Token zařízení: `server_error` (`PosProblem`), zalogováno bez
            tokenu.
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }

  /cash/update:
    post:
      tags: [Cash]
      summary: Update cash document header (partial)
      description: |
        Částečná úprava hlavičky – posílají se jen měněná pole. Vyžaduje právo `edit`.

        Vrací **409**, pokud je doklad vázaný na jiný doklad (vyplněný kterýkoli ze sloupců
        `cl_invoice_id`, `cl_invoice_advance_id`, `cl_invoice_arrived_id`, `cl_sale_id`,
        `cl_delivery_note_id`, `cl_transport_id`, `cl_transport_cash_id`) nebo má záznam
        v `cl_paired_docs`. Zámek `locked` na tuto ochranu vliv NEMÁ.

        Znaménko částky se přepočítá vždy, když se mění `cash`, `direction` nebo
        `cl_invoice_types_id` – i samotné přepnutí dokladu na výdajový typ tedy obrátí
        znaménko dosud uložené částky, stejně jako to dělá web při každém uložení.
        Částku můžete poslat s libovolným znaménkem, rozhoduje směr dokladu:

        1. poslaný `direction` platí vždy;
        2. jinak typ dokladu (nově poslaný, jinak uložený) – výdajový typ je výdaj,
           jiný typ příjem;
        3. doklad bez typu (u starších záznamů chybí) si ponechá směr, který už má –
           `{"cash": 100}` u výdaje bez typu uloží −100, ne +100.

        Po úspěšné úpravě se zneplatní dřív vytištěné PDF (`cl_documents.valid = 0`), aby
        veřejný odkaz na doklad nevydával zastaralý dokument – shodně s webem.

        Nepodaří-li se zápis, akce to přizná: doklad, který mezitím přestal být dostupný,
        vrací 404, doklad změněný mezitím někým jiným vrací 409. Dřív odcházelo 200 „ok"
        s nezměněným dokladem, i když se nezapsalo nic.

        Doklad nelze přesunout mimo vlastní dosah – uživatel přiřazený na pobočku nesmí
        zvolit jinou a při roli „Jen vlastní záznamy" nesmí doklad přiřadit někomu jinému (400).

        **`eet_relevant`**: nepošle-li klient toto pole, hodnota se nemění (stejné pravidlo
        jako u ostatních polí). Pošle-li `1`/`0` (i jako řetězec nebo bool), platí to
        **doslova** – u existujícího dokladu se žádná výchozí hodnota nedopočítává a směr
        dokladu na příznak nemá vliv. Jiná hodnota vrací **400** a neuloží se nic (uznává se
        jen `1`, `"1"`, `true`, `0`, `"0"`, `false` – ani `1.0`, `" 1"`, `"true"`, `"ano"`).

        Vrací **409**, pokud má doklad už zaevidovanou tržbu (POK) a příznak by se změnil:
        odškrtnutí by zaevidovanou tržbu jen schovalo z aplikace, u finanční správy zůstane.
        Stejný zámek jako ve webu (`lockEetFields()`); poslání stejné hodnoty, jaká je
        uložená, projde. Ostatní pole z `RegisteredPaymentGuard::CASH_FIELDS` (`cash`,
        `currency_rate`, `cl_currencies_id`, `cash_number`, `cl_company_branch_id`,
        `inv_date`) tenhle zámek přes API zatím nemají – je to starší mezera, která se řeší
        samostatně.

        **Firma bez zapnutého EET: poslaná hodnota se ignoruje, není to chyba.** Nemá-li
        firma `eet_active` ani `eet_test`, příznak se nemění vůbec, i když klient výslovně
        pošle `eet_relevant = 1` – odpověď vrátí hodnotu, která na dokladu zůstala uložená
        (u takové firmy prakticky vždy `0`). Nevrací se 400 ani žádné upozornění; je to
        záměr, ne vada – web u takové firmy volbu „Evidovat do EET" nenabízí a sloupec
        zůstává na výchozí `0`. Pošle-li klient jen tohle pole, vrátí se 400 „Nebyla
        zaslána žádná změna", protože se pak nemění nic.

        Po uložení doklad znovu zpracuje `EetCashService::sendForCashDoc()`: doklad, který
        evidenci podléhá a tržbu ještě nemá, se zaeviduje v EET – typicky když úprava
        přepne `eet_relevant` z `0` na `1`. Doklad, který už POK má, se podruhé neodesílá,
        takže opakované uložení druhou tržbu nezaloží.

        Selhání evidence úpravu dokladu **nezruší** – odpověď je i tak 200. Selže-li
        samotné odeslání, zapíše se neúspěšný pokus do `cl_eet` se stavem chyby a převezme
        ho fronta doposílání. **Nedojde-li ale k odeslání vůbec** – typicky když se
        nepodaří sestavit podpisová data firmy nebo provozovny
        (`CompaniesManager::getDataForSignEET()` vrátí `false`) – nevznikne nic: žádný
        řádek v `cl_eet` ani záznam v logu, takže fronta doposílání nemá co vyzvednout.
        Uloženou hodnotu vrací odpověď (`eet_relevant`).
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token:
                  type: string
                  description: |
                    **Zápis přes integrační token není podporovaný.** Token je vždy jen
                    ke čtení (viz schéma `apiToken`), takže úprava dokladu skončí 403
                    ještě před jakýmkoli zápisem. Pokladní doklad lze přes API upravit
                    pouze s `bearer_token`.
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    JSON: `id` (int, povinné) + měněná pole – `title`, `description_txt`,
                    `inv_date`, `cash`, `direction`, `cl_cash_def_id`, `cl_center_id`,
                    `cl_partners_book_id`, `cl_partners_book_workers_id`, `cl_company_branch_id`,
                    `cl_currencies_id`, `currency_rate`, `cl_invoice_types_id`, `cl_status_id`,
                    `cl_users_id`, `eet_relevant` (jen 0/1, evidence do EET – co nepošlete, se
                    nemění; jiná hodnota → 400). `cl_number_series_id` se přes update měnit nedá.
                    Každý poslaný cizí klíč se ověřuje proti autorizované firmě.
                  example: '{"id":9911,"title":"Nákup kancelářských potřeb - opraveno"}'
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/CashListItem' }
        '400':
          description: |
            Chybí id, cash/currency_rate není číslo, neplatný formát data v inv_date,
            neplatná hodnota direction, neplatná hodnota eet_relevant (povoleno jen 0/1),
            zápis mimo vlastní pobočku či cizímu uživateli, nebo nebyla zaslána žádná změna
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu pokladna nebo právo edit'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: 'Cash document not found, nebo poslaný cizí klíč neexistuje/patří jiné firmě'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Doklad je navázaný na jiný doklad nebo spárovaný, nelze jej upravit; doklad má zaevidovanou tržbu EET a měnil by se eet_relevant; nebo ho mezitím změnil jiný uživatel'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /cash/set-status:
    post:
      tags: [Cash]
      summary: Change cash document status
      description: |
        Nastaví stav dokladu. Stav musí mít `status_use = cash`. Vyžaduje právo `edit`.
        Nepodaří-li se zápis, vrací 404 (doklad mezitím přestal být dostupný) nebo 409
        (doklad mezitím změnil někdo jiný), ne 200 „ok" s nezměněným dokladem.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: `id` (int, povinné), `cl_status_id` (int, povinné)'
                  example: '{"id":9911,"cl_status_id":21}'
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/CashListItem' }
        '400':
          description: 'Chybí id nebo cl_status_id'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu pokladna nebo právo edit'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: 'Cash document not found, nebo Status not found (neexistuje / není určen pro pokladnu / patří jiné firmě)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Doklad mezitím změnil jiný uživatel, změna stavu nebyla uložena'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /cash/delete/{id}:
    post:
      tags: [Cash]
      summary: Delete cash document (guard na provázanost a přílohy)
      description: |
        Doklad nelze smazat, je-li vázaný na jiný doklad (stejný seznam sloupců jako u `update`)
        nebo spárovaný přes `cl_paired_docs`, ani má-li přílohy nebo zaevidovanou tržbu EET
        (potvrzený POK) – vrací 409 se srozumitelným výčtem důvodů. Zámek `locked` mazání neovlivňuje. Selhání kvůli cizímu klíči
        (například doklad, na který ukazuje prodejka přes `cl_sale.cl_cash_id`) se rovněž
        vrací jako 409, ne jako 500. Vyžaduje právo `erase`.
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: object, properties: { id: { type: integer } } }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu pokladna nebo právo erase'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Cash document not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Doklad nelze smazat – navázaný/spárovaný doklad, přílohy, zaevidovaná tržba EET, nebo se smazání jinak nepodařilo'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /cash/get-balance:
    post:
      tags: [Cash]
      summary: Cash register balances
      description: |
        Zůstatky po pokladnách (obdoba rychlých součtů ve webu). Měna se bere z definice
        pokladny, při nevyplnění z nastavení firmy. Vyžaduje právo `report`.

        Součet se počítá jen z dokladů, které uživatel vidí – tedy se stejným omezením
        na pobočku a „Jen vlastní záznamy" jako `get-all`. Uživateli přiřazenému na pobočku
        proto neukazuje celofiremní stav pokladny.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: `cl_cash_def_id` (int, nepovinné – jen jedna pokladna), `date` (Y-m-d, nepovinné – zůstatek k datu)'
                  example: '{"date":"2026-08-05"}'
      responses:
        '200':
          description: Balance list
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/CashBalanceItem' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu pokladna nebo právo report'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /cash/pdf/{id}:
    post:
      tags: [Cash]
      summary: Cash document as PDF (binary)
      description: |
        Vrátí pokladní doklad jako PDF. Vyžaduje právo `report`.

        **Vedlejší efekty zděděné z programu:** zakládá záznam v `cl_documents` a u firem
        s příznakem „Zamykat doklady po tisku" (`lock_ap = 1`) doklad po vygenerování zamkne
        (`locked = 1`) – stejné chování jako tlačítko PDF ve webu.

        **Může vrátit 404 i u dokladu, který `get-one` úspěšně vrátil** – generování si doklad
        načítá znovu cestou, která navíc filtruje na pobočku uživatele a soukromé záznamy.
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: Binary PDF content
          content:
            application/pdf:
              schema: { type: string, format: binary }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu pokladna nebo právo report'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: |
            Cash document not found – doklad neexistuje, patří jiné firmě, nebo pro
            přihlášeného uživatele není dostupný (filtr pobočky / soukromý záznam)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /cash/get-cash-registers:
    post:
      tags: [Cash Lookups]
      summary: List cash registers
      description: 'Číselník pokladen (cl_cash_def), řazeno dle name. Vyžaduje právo read.'
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: Cash register list
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/CashRegister' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu pokladna nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /cash/get-centers:
    post:
      tags: [Cash Lookups]
      summary: List centers
      description: 'Číselník středisek (cl_center), řazeno dle name. Vyžaduje právo read.'
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: Center list
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/Center' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu pokladna nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /cash/get-currencies:
    post:
      tags: [Cash Lookups]
      summary: List currencies
      description: 'Číselník měn (cl_currencies), řazeno dle currency_code. Vyžaduje právo read.'
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: Currency list
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/CashCurrency' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu pokladna nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /cash/get-number-series:
    post:
      tags: [Cash Lookups]
      summary: List number series for cash documents
      description: |
        Číselné řady pokladních dokladů. Parametr `use` omezí výpis na `cash_in`/`cash_out`,
        bez něj se vrátí obojí. Vyžaduje právo read.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: `use` (nepovinné, `cash_in`/`cash_out`)'
                  example: '{"use":"cash_out"}'
      responses:
        '200':
          description: Number series list
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/CashNumberSeries' }
        '400':
          description: 'Neplatná hodnota parametru use (povoleno cash_in, cash_out)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu pokladna nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /cash/get-statuses:
    post:
      tags: [Cash Lookups]
      summary: List cash document statuses
      description: 'Číselník stavů pokladního dokladu (cl_status, status_use = cash), řazeno dle item_order. Vyžaduje právo read.'
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: Status list
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/CashStatus' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu pokladna nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /cash/get-files/{id}:
    post:
      tags: [Cash Files]
      summary: List cash document files
      description: 'Vyžaduje právo read.'
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer, description: 'ID dokladu' } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: File list
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/FileItem' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu pokladna nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Cash document not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /cash/file-upload:
    post:
      tags: [Cash Files]
      summary: Upload a cash document file (base64 data URL)
      description: 'Nahraje přílohu k pokladnímu dokladu. Vyžaduje právo edit.'
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    JSON objekt: { cl_cash_id, file: FileUploadInput }.
                    file.dataUrl = data:<mime>;base64,<...> (viz schema FileUploadInput)
                  example: '{"cl_cash_id":9911,"file":{"name":"stvrzenka.jpg","type":"image/jpeg","size":42000,"dataUrl":"data:image/jpeg;base64,..."}}'
      responses:
        '200':
          description: Uploaded
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: object
                    properties:
                      id: { type: integer }
                      file_name: { type: string }
        '400':
          description: 'Chybí cl_cash_id nebo file, případně Invalid file data (base64 decode failed)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu pokladna nebo právo edit'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Cash document not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /cash/file-download/{id}:
    post:
      tags: [Cash Files]
      summary: Download a cash document file (binary)
      description: 'Vyžaduje právo read.'
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer, description: 'ID přílohy' } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: Binary file content
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu pokladna nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: File not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /cash/file-delete/{id}:
    post:
      tags: [Cash Files]
      summary: Delete a cash document file
      description: 'Vyžaduje právo erase.'
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer, description: 'ID přílohy' } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu pokladna nebo právo erase'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: File not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/get-all:
    post:
      tags: [Invoice]
      summary: List invoices
      description: |
        Výpis faktur autorizované firmy. Vrací `total` = počet záznamů odpovídajících
        filtru před stránkováním. Vyžaduje právo `read`. Kromě firmy platí stejná
        omezení na úrovni záznamu jako ve webu (pobočka, „Jen vlastní záznamy").
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    JSON s filtry (všechny nepovinné, lze kombinovat):
                    - `cl_partners_book_id`, `cl_status_id`, `cl_center_id`, `cl_currencies_id`,
                      `cl_payment_types_id`, `cl_invoice_types_id`, `cl_commission_id` (int)
                    - `date_from` / `date_to` (Y-m-d) – rozsah inv_date
                    - `due_from` / `due_to` (Y-m-d) – rozsah due_date
                    - `storno` (0/1) – jen nestornované/stornované
                    - `unpaid` (bool) – nezaplacené (podle plátcovství DPH firmy a odečtu záloh,
                      shodně s webovým filtrem)
                    - `overdue` (bool) – nezaplacené A po splatnosti
                    - `search` (string) – LIKE přes inv_number, var_symb, inv_title, od_number,
                      název partnera
                    - `changed_since` (Y-m-d H:i:s) – doklady změněné nebo založené od okamžiku
                    - `limit` (int, výchozí 50, ořez 1–500), `offset` (int, výchozí 0)
                  example: '{"unpaid":1,"limit":20}'
      responses:
        '200':
          description: List
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/InvoiceListItem' }
                  total: { type: integer, example: 137 }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/get-one/{id}:
    post:
      tags: [Invoice]
      summary: Invoice detail
      description: |
        Detail faktury vč. items[], items_back[], payments[], files[], paired_docs[]
        a vat_breakdown[]. Faktura jiné firmy nebo mimo dosah uživatele vrací 404,
        ne 403. Vyžaduje právo `read`.
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: Detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/InvoiceDetail' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Invoice not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/get-defaults:
    post:
      tags: [Invoice]
      summary: Preview default values for a new invoice
      description: |
        Náhled výchozích hodnot nové faktury (partner nepovinný) – stejné privátní
        metody jako `create` (číselná řada, typ, stav, splatnost, hlavička/patička),
        aby se odpověď nemohla rozejít. Číslo dokladu je jen PREVIEW – nespotřebovává
        čítač číselné řady. Nic se nezapisuje. Vyžaduje právo `read`.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'Nepovinné: cl_partners_book_id, cl_company_branch_id, cl_currencies_id (každý company-scoped)'
      responses:
        '200':
          description: Defaults preview
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: object, description: 'Podmnožina polí InvoiceListItem – náhled, ne uložený doklad' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: 'Partner/Company branch/Currency/Number series not found'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/create:
    post:
      tags: [Invoice]
      summary: Create invoice
      description: |
        Založí fakturu. Vyžaduje právo `write`. **Partner je povinný** (na rozdíl
        od get-defaults) – bez cl_partners_book_id vrací 400.

        Číslo dokladu se přidělí z číselné řady (form_use=invoice) AŽ PO VŠECH
        validacích. Nevyplněné cizí klíče se dosadí ze stejných pravidel jako
        get-defaults (typ dokladu, stav s s_new=1, měna firmy, typ úhrady a
        splatnost podle partnera, bankovní účet, středisko pobočky, hlavička/patička
        podle jazyka partnera).

        Kontroluje se uzávěrka DPH k vat_date (409) a zápis mimo vlastní pobočku/
        „Jen vlastní záznamy" (400).
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    Povinné: `cl_partners_book_id`. Nepovinné: `inv_date`, `vat_date`, `due_date`,
                    `inv_title`, `od_number`, `cl_partners_book_workers_id`, `cl_company_branch_id`,
                    `cl_center_id`, `cl_currencies_id`, `currency_rate`, `cl_payment_types_id`,
                    `cl_bank_accounts_id`, `cl_invoice_types_id`, `cl_status_id`, `cl_users_id`,
                    `cl_commission_id`, `vat_active`, `price_e_type`, `header_show`, `footer_show`,
                    `invoice_add_cash_doc`, `konst_symb`, `var_symb`, `spec_symb`.

                    `cl_invoice_types_id` musí být běžná vydaná faktura (`inv_type = 1`);
                    typy přijatých, opravných a zálohových dokladů se odmítají 404.
                    `cl_commission_id` se ověřuje na firmu (404, není-li zakázka vaše).
                  example: '{"cl_partners_book_id":42,"inv_date":"2026-08-08","vat_active":1}'
      responses:
        '200':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/InvoiceListItem' }
        '400':
          description: |
            Validation error – chybí/cizí cl_partners_book_id, neplatný formát data,
            currency_rate není číslo, hodnota mimo uzavřenou množinu (vat_active,
            price_e_type, header_show, footer_show, invoice_add_cash_doc), zápis mimo
            vlastní pobočku či cizímu uživateli
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo write'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: 'Cizí klíč poslaný klientem neexistuje nebo patří jiné firmě'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'DUZP spadá do uzavřeného období DPH'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: 'Internal error, nebo pro faktury vydané není nastavená číselná řada'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/update:
    post:
      tags: [Invoice]
      summary: Update invoice header (partial)
      description: |
        Částečná úprava hlavičky – posílají se jen měněná pole. Vyžaduje právo `edit`.
        `cl_number_series_id` a `inv_number` NEJDOU editovat. Partner nelze odebrat
        (poslat null) – jen vyměnit. Vrací 409 na zamčenou fakturu, odeslanou EET,
        uzávěrku DPH (proti výsledné hodnotě vat_date). Po úspěchu se vždy přepočtou
        součty, zneplatní dřív vytištěné PDF a synchronizují spárované doklady.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: '`id` (int, povinné) + měněná pole hlavičky (viz create, kromě cl_number_series_id/inv_number)'
                  example: '{"id":141500,"inv_title":"Oprava textu"}'
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/InvoiceListItem' }
        '400':
          description: 'Chybí id, neplatná hodnota, partner odebrán, žádná změna k uložení, zápis mimo vlastní pobočku/uživatele'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo edit'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: 'Invoice not found, nebo poslaný cizí klíč neexistuje/patří jiné firmě'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Faktura je zamčená / má odeslanou EET / spadá do uzávěrky DPH'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/set-status:
    post:
      tags: [Invoice]
      summary: Change invoice status
      description: |
        Nastaví stav dokladu (musí mít status_use=invoice, opravné doklady se zde
        nevrací ani nejdou nastavit). Vyžaduje právo `edit`. Vrací 409 na zamčenou
        fakturu / odeslanou EET / uzávěrku DPH proti aktuálnímu vat_date z DB.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: `id` (int, povinné), `cl_status_id` (int, povinné)'
                  example: '{"id":141500,"cl_status_id":6}'
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/InvoiceListItem' }
        '400':
          description: 'Chybí id nebo cl_status_id'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo edit'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: 'Invoice not found, nebo Status not found (neexistuje / status_use != invoice / patří jiné firmě)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Faktura je zamčená / má odeslanou EET / spadá do uzávěrky DPH'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/apply-partner:
    post:
      tags: [Invoice]
      summary: Recompute due date/payment type/symbols for partner (optionally change partner)
      description: |
        Přepočítá due_date, cl_payment_types_id a spec_symb podle partnera – appka to
        smí volat i beze změny partnera, jen kvůli přepočtu splatnosti. Kontakt a
        pobočka partnera se nulují jen při skutečné změně partnera. Bankovní účet se
        přepisuje jen chybí-li úplně (ne při neshodě měny – to řeší set-currency).
        Vyžaduje právo `edit`. Vrací 409 na zamčenou fakturu / EET / uzávěrku DPH.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: `id` (int, povinné), `cl_partners_book_id` (int, povinné – nelze odebrat)'
                  example: '{"id":141500,"cl_partners_book_id":99}'
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/InvoiceListItem' }
        '400':
          description: 'Chybí id nebo cl_partners_book_id'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo edit'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: 'Invoice not found, nebo Partner not found'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Faktura je zamčená / má odeslanou EET / spadá do uzávěrky DPH'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/set-currency:
    post:
      tags: [Invoice]
      summary: Change invoice currency, optionally recalculating item prices
      description: |
        Změní měnu faktury. Vyžaduje právo `edit`. Bankovní účet se přepíše na
        výchozí pro NOVOU měnu, pokud aktuální účet chybí NEBO je v jiné měně.

        **Vědomá odchylka od programu:** pošle-li appka `recalc=1`, přepočet starým/
        novým kurzem proběhne u OBOU sad položek (prodejní i vratné) – web při
        ručním přepočtu kurzem přepočítává jen prodejní položky, což je mezera ve
        webu (hlavička počítá price_e2 - price_e2_back, takže jednostranný přepočet
        by nechal položky ve dvou kurzech). Celá operace běží v jedné DB transakci.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: `id` (povinné), `cl_currencies_id` (povinné), `currency_rate` (nepovinné – beze změny, není-li poslán), `recalc` (0/1, nepovinné)'
                  example: '{"id":141500,"cl_currencies_id":2,"currency_rate":24.5,"recalc":1}'
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/InvoiceListItem' }
        '400':
          description: 'Chybí id nebo cl_currencies_id, currency_rate není číslo, recalc mimo 0/1'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo edit'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: 'Invoice not found, nebo Currency not found'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Faktura je zamčená / má odeslanou EET / spadá do uzávěrky DPH'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/recalc/{id}:
    post:
      tags: [Invoice]
      summary: Recalculate invoice header totals from current items
      description: |
        Ruční přepočet součtů hlavičky ze stávajících položek (InvoiceManager::
        updateInvoiceSum()) – nepřepočítává kurzem ani cenami, jen znovu sečte, co
        už na faktuře je. Na rozdíl od update/set-status/apply-partner/set-currency
        NEspouští syncPairedDocuments() – přepočet součtů sám o sobě spárované
        doklady nesynchronizuje, stejně jako to nedělá web. Vyžaduje právo `edit`.
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: Recalculated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/InvoiceListItem' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo edit'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Invoice not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Faktura je zamčená / má odeslanou EET / spadá do uzávěrky DPH'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/delete/{id}:
    post:
      tags: [Invoice]
      summary: Delete invoice (řada blokátorů, vědomě přísnější než web)
      description: |
        Vyžaduje právo `erase`. **Vrací 409, má-li faktura úhrady** – web fakturu
        s úhradami smaže bez varování (cl_invoice_payments má ON DELETE CASCADE),
        API to považuje za nebezpečné pro appku a mazání blokuje předem
        (rozhodnutí vlastníka). Stejně blokuje: skladové pohyby na položkách,
        spárované doklady (cl_paired_docs), položky inkasního/platebního příkazu,
        použití faktury jako zálohy k jiné úhradě, odeslanou EET a zámek (locked).
        Hláška vypíše všechny nalezené překážky najednou. Přílohy a helpdesk vazba
        (cl_partners_event) blokátory NEJSOU – aktivně se uklidí před smazáním.
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: object, properties: { id: { type: integer } } }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo erase'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Invoice not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Faktura má úhrady/skladové pohyby/spárované doklady/položky příkazu/použití jako zálohy/odeslanou EET/zámek, nebo mazání selhalo (cizí klíč)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/get-all-items:
    post:
      tags: [Invoice Items]
      summary: Bulk invoice items for a filter
      description: |
        Hromadné čtení položek všech faktur, které odpovídají filtru - protějšek
        get-all na úrovni položek. Nahrazuje volání get-items/{id} po jednom dokladu,
        které u ročního skenu stálo tisíce požadavků.

        Filtry i omezení na úrovni záznamu jsou shodné s /invoice/get-all (sdílí
        tentýž kód), včetně date_from, date_to a changed_since. `changed_since`
        se tu navíc porovnává i s časem změny samotné položky, aby v inkrementálním
        běhu neutekla úprava, která se do hlavičky faktury nepromítla.

        Vrací jeden plochý seznam: položky i vratky za sebou, odlišené polem
        `row_type` (`item` / `back`). Stránkuje se přes obě tabulky dohromady,
        takže `total` i `offset` platí pro celou množinu. Pole `cl_invoice_id`
        slouží k připojení hlavičky z get-all.

        Položky a vratky mají oddělené číselné řady `id` — u sebe je klíčujte
        dvojicí (`row_type`, `id`), ne samotným `id`.

        Vyžaduje právo `read`.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON s filtry get-all plus limit (max 500, výchozí 50) a offset'
                  example: '{"date_from":"2026-01-01","date_to":"2026-06-30","limit":500,"offset":0}'
      responses:
        '200':
          description: Items and returned items, paginated together
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  total: { type: integer, description: 'Počet položek i vratek dohromady' }
                  data:
                    type: array
                    items:
                      allOf:
                        - type: object
                          properties:
                            row_type: { type: string, enum: [item, back] }
                            cl_invoice_id: { type: integer }
                        - $ref: '#/components/schemas/InvoiceItem'
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - chybí právo read, nebo token na tenhle modul nedosáhne
  /invoice/get-all-payments:
    post:
      tags: [Invoice Payments]
      summary: Bulk invoice payments for a period
      description: |
        Hromadné čtení úhrad faktur vydaných pro reporting cashflow. Nahrazuje volání
        get-payments/{id} po jednom dokladu.

        `date_from` a `date_to` se vztahují k **datu úhrady** (`pay_date`), ne k datu
        faktury. `cl_partners_book_id` a `cl_currencies_id` filtrují podle faktury,
        `cl_payment_types_id` podle úhrady. `changed_since` vrací úhrady změněné nebo
        založené od zadaného času (jen řádek úhrady, hlavičky stahujte přes get-all).

        Omezení na úrovni záznamu (pobočka, jen vlastní záznamy) se bere z faktury,
        stejně jako v get-all. Řazeno podle `id`. `limit` max 500 (vyšší se tiše
        ořízne) — vždy stránkujte podle `total`.

        `changed`/`created` jsou lokální čas serveru (Europe/Prague) bez pásma — `T`
        pro `changed_since` berte z hodin serveru a v dalším běhu ho o pár minut
        posuňte zpět, překryv nevadí (upsert podle `id` je idempotentní).

        Smazané úhrady vrací /invoice/get-deleted. Vyžaduje právo `read`.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: date_from, date_to, changed_since, cl_partners_book_id, cl_currencies_id, cl_payment_types_id, limit, offset'
                  example: '{"date_from":"2026-01-01","date_to":"2026-06-30","limit":500,"offset":0}'
      responses:
        '200':
          description: Payments, paginated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  total: { type: integer }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/InvoicePayment' }
        '400':
          description: Neplatné datum ve filtru
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - chybí právo read, nebo token na tenhle modul nedosáhne
  /invoice/get-deleted:
    post:
      tags: [Invoice]
      summary: Deleted invoices, items and payments since a time
      description: |
        Seznam faktur, položek, vratek a úhrad smazaných od `since` — pro inkrementální
        načítání do BI, kde `changed_since` smazání nepozná.

        **Pozor:** když se smaže celá faktura, její položky, vratky a úhrady maže databáze
        kaskádou a ty se v tomto výpisu **neobjeví**. Při `table = cl_invoice` smažte
        u sebe i všechny řádky s tímto `cl_invoice_id`. Převod faktur na zálohové
        (administrátorská operace) maže mimo evidenci — proto občas stáhněte vše znovu.

        `table` je `cl_invoice_items`/`cl_invoice_items_back` pro řádky get-all-items
        s `row_type=item`/`back` — položky a vratky mají oddělené řady `id`, u sebe je
        proto klíčujte dvojicí (`row_type`, `id`).

        `deleted_at` je lokální čas serveru (Europe/Prague) bez pásma — `since` berte
        z hodin serveru a v dalším běhu ho o pár minut posuňte zpět, překryv nevadí
        (mazání podle `id` je idempotentní).

        Omezeno jen na firmu. `since` je povinné. Řazeno podle pořadí smazání.
        Vyžaduje právo `read`.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: since (povinné), limit, offset'
                  example: '{"since":"2026-09-22 00:00:00","limit":500,"offset":0}'
      responses:
        '200':
          description: Deleted records, paginated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  total: { type: integer }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/DeletedRecord' }
        '400':
          description: Chybí nebo je neplatné since
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
        '403':
          description: Forbidden - chybí právo read, nebo token na tenhle modul nedosáhne
  /invoice/get-items/{id}:
    post:
      tags: [Invoice Items]
      summary: Invoice items only (lightweight get-one)
      description: 'Odlehčená verze get-one – jen items[] a items_back[], bez zbytku hlavičky. Vyžaduje právo `read`.'
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: Items
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: object
                    properties:
                      items:
                        type: array
                        items: { $ref: '#/components/schemas/InvoiceItem' }
                      items_back:
                        type: array
                        items: { $ref: '#/components/schemas/InvoiceItem' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Invoice not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/search-pricelist:
    post:
      tags: [Invoice Items]
      summary: Search pricelist for items to add
      description: |
        Hledání v ceníku – cena podle partnera (individuální ceník i cenová skupina),
        stav skladu a blokace pro zakázky. Ceník je omezený na cl_pricelist_group_id
        POBOČKY PŘIHLÁŠENÉHO UŽIVATELE (z identity), ne parametrem od klienta – appka
        nemůže prohlížet cizí pobočku jen tím, že pošle jiné cl_company_branch_id.
        Vyžaduje právo `read`.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    Povinné: `cl_storage_id`, `cl_currencies_id`. Nepovinné: `cl_partners_book_id`
                    (cena podle partnera), `cl_commission_id` a/nebo `cl_invoice_id` (vylučují
                    vlastní rezervaci zakázky z blokace), `q` (fulltext přes item_label/
                    identification/ean_code/order_code/search_tag), `limit` (výchozí 50, 1–500),
                    `offset` (výchozí 0).
                  example: '{"cl_storage_id":2,"cl_currencies_id":1,"q":"šroub"}'
      responses:
        '200':
          description: List
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/InvoicePricelistSearchItem' }
                  total: { type: integer }
        '400':
          description: 'Chybí cl_storage_id nebo cl_currencies_id'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: 'Storage/Currency/Partner/Commission/Invoice not found'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/check-availability:
    post:
      tags: [Invoice Items]
      summary: Available quantity of one pricelist item on a storage
      description: 'Dostupné množství = max(0, stav skladu - blokace), zaokrouhleno podle nastavení firmy. Vyžaduje právo `read`.'
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'Povinné: `cl_pricelist_id`, `cl_storage_id`. Nepovinné: `cl_commission_id`, `cl_invoice_id` (vyloučí vlastní rezervaci).'
                  example: '{"cl_pricelist_id":881,"cl_storage_id":2}'
      responses:
        '200':
          description: Availability
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/InvoiceAvailability' }
        '400':
          description: 'Chybí cl_pricelist_id nebo cl_storage_id'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: 'Pricelist item/Storage/Commission/Invoice not found'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/add-item:
    post:
      tags: [Invoice Items]
      summary: Add a sale item (add-item) – zakládá/aktualizuje výdejku jako vedlejší efekt
      description: |
        Vloží prodejní řádek – buď z ceníku (`cl_pricelist_id`), nebo volný text
        (`item_label` + `price_e` + `vat` bez cl_pricelist_id, např. doprava/práce).
        Vyžaduje právo `edit`. Vrací 409 na zamčenou fakturu/EET/uzávěrku DPH.

        Je-li firma v režimu invoice_to_store=1, uložení ceníkové položky založí
        NEBO znovupoužije výdejku faktury – appka o výdejku nežádá zvlášť, je to
        vedlejší efekt (viz store v odpovědi). Server vždy sám dopočítá price_e2/
        price_e2_vat/vat – klientem poslané hotové částky se přepočítají.

        Validace rozlišuje **kind="error"** (množství je záporné u ceníkové položky,
        tvrdá blokace pro zakázky, nebo zákaz výdeje do záporu – položka SE NEULOŽÍ,
        HTTP 409) a **kind="warning"** (měkká blokace – položka SE ULOŽÍ, hláška
        v poli warnings u HTTP 200).
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    Povinné: `cl_invoice_id`, `quantity`. Buď `cl_pricelist_id`, nebo (bez něj)
                    `item_label` + `price_e` + `vat`. Nepovinné: `cl_storage_id` (přebije
                    ceníkový sklad), `price_e`, `discount` (přebijí ceníkovou cenu/slevu),
                    `cl_commission_id` (ověřuje se na firmu; u vratných položek sloupec
                    v DB neexistuje a tiše se ignoruje), `description1`, `description2`, `units`.
                  example: '{"cl_invoice_id":141500,"cl_pricelist_id":881,"quantity":3,"cl_storage_id":2}'
      responses:
        '200':
          description: Added (i s kind="warning" v poli warnings)
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/InvoiceItemSaveResponse' }
        '400':
          description: 'Chybí cl_invoice_id/quantity, nebo (bez cl_pricelist_id) item_label/price_e/vat'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo edit'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: 'Invoice/Storage/Pricelist item/Commission not found'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: |
            Faktura je zamčená/EET/uzávěrka DPH, NEBO validace vrátila kind="error"
            (odpověď má tvar {status:error, message, warnings} – warnings se nesmí zahodit)
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/update-item:
    post:
      tags: [Invoice Items]
      summary: Update a sale item
      description: |
        Úprava prodejní položky – posílají se jen měněná pole (`quantity`, `price_e`,
        `discount`, `cl_storage_id`, `cl_commission_id`, `description1`, `description2`).
        Položka musí patřit firmě I dané faktuře (id nestačí, appka nesmí uhodnout
        cizí id a upravit tak cizí položku) – jinak 404. Stejná pravidla 409/kind
        jako add-item. Vyžaduje právo `edit`.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: '`cl_invoice_id` (povinné), `id` (povinné, id položky) + měněná pole'
                  example: '{"cl_invoice_id":141500,"id":55012,"quantity":5}'
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/InvoiceItemSaveResponse' }
        '400':
          description: 'Chybí cl_invoice_id nebo id'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo edit'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: 'Invoice not found, nebo Invoice item not found (neexistuje / cizí firma / patří jiné faktuře)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Faktura je zamčená/EET/uzávěrka DPH, nebo validace vrátila kind="error"'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/delete-item/{id}:
    post:
      tags: [Invoice Items]
      summary: Delete a sale item – vrací i skladový pohyb dárku
      description: |
        Smaže prodejní položku a vrátí navázaný skladový pohyb (výdejku) zpět. Má-li
        položka rozgenerovaný dárek/bonus, vrátí se **i jeho vlastní** pohyb, ne jen
        pohyb rodiče. Vyžaduje právo `edit` (ne erase – mazání řádku faktury je
        součást úpravy dokladu). Vrací 409 na zamčenou fakturu/EET/uzávěrku DPH,
        nebo pokud je položka navázaná na jiný doklad (cizí klíč).
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: '`cl_invoice_id` (povinné)'
                  example: '{"cl_invoice_id":141500}'
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: object
                    properties:
                      id: { type: integer }
                      invoice: { $ref: '#/components/schemas/InvoiceListItem' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo edit'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: 'Invoice not found, nebo Invoice item not found'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Faktura je zamčená/EET/uzávěrka DPH, nebo položku nelze smazat (navázaná na jiný doklad)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/add-item-back:
    post:
      tags: [Invoice Items]
      summary: Add a return item (dobropis množství) – zakládá/aktualizuje příjemku jako vedlejší efekt
      description: |
        Zrcadlo add-item pro vratné položky (cl_invoice_items_back) – zakládá/
        používá PŘÍJEMKU místo výdejky. `cl_commission_id` se tu nepoužívá (sloupec
        v tabulce neexistuje, pošle-li ho appka, tiše se ignoruje). Zákaz záporného
        množství platí i tady (vratná položka se záporným množstvím by fakticky
        vydávala ze skladu), kontroly „dost zboží na výdej" se ale na vratnou
        položku nevztahují – jde přece NA sklad. Vyžaduje právo `edit`.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'Stejná pole jako add-item, bez cl_commission_id'
                  example: '{"cl_invoice_id":141500,"cl_pricelist_id":881,"quantity":2,"cl_storage_id":2}'
      responses:
        '200':
          description: Added
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/InvoiceItemSaveResponse' }
        '400':
          description: 'Chybí cl_invoice_id/quantity, nebo (bez cl_pricelist_id) item_label/price_e/vat'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo edit'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: 'Invoice/Storage/Pricelist item not found'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Faktura je zamčená/EET/uzávěrka DPH, nebo validace vrátila kind="error" (záporné množství)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/update-item-back:
    post:
      tags: [Invoice Items]
      summary: Update a return item
      description: 'Zrcadlo update-item pro vratné položky. Stejná ownership kontrola (firma + patří dané faktuře) a stejná 404/409 pravidla. Vyžaduje právo `edit`.'
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: '`cl_invoice_id` (povinné), `id` (povinné) + měněná pole'
                  example: '{"cl_invoice_id":141500,"id":33001,"quantity":1}'
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/InvoiceItemSaveResponse' }
        '400':
          description: 'Chybí cl_invoice_id nebo id'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo edit'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: 'Invoice not found, nebo Invoice item not found'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Faktura je zamčená/EET/uzávěrka DPH, nebo validace vrátila kind="error"'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/delete-item-back/{id}:
    post:
      tags: [Invoice Items]
      summary: Delete a return item
      description: |
        Smaže vratnou položku. Odmítne se (409, „Z příjemky již bylo vydáváno,
        záznam není možné vymazat"), pokud z příjemky navázané na položku už bylo
        vydáváno (na to navazuje výdej), i přes vázané dárky. Vyžaduje právo `edit`.
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: '`cl_invoice_id` (povinné)'
                  example: '{"cl_invoice_id":141500}'
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: object
                    properties:
                      id: { type: integer }
                      invoice: { $ref: '#/components/schemas/InvoiceListItem' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo edit'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: 'Invoice not found, nebo Invoice item not found'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Faktura je zamčená/EET/uzávěrka DPH, nebo z příjemky už bylo vydáváno'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/reorder-items:
    post:
      tags: [Invoice Items]
      summary: Reorder invoice items (items or items_back)
      description: |
        Přeuspořádá pořadí (item_order) jedné sady položek najednou. `target` musí
        být přesně `items` nebo `items_back` – obě sady mají nezávislé číslování id
        i item_order. Celá dávka se ověřuje najednou (firma + patří dané faktuře);
        neznámé/cizí/duplicitní id v dávce → 404 a NIC se nezapíše. Vyžaduje právo `edit`.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: '`cl_invoice_id` (povinné), `target` (povinné, "items"/"items_back"), `items` (pole `{id, item_order}`, povinné, neprázdné)'
                  example: '{"cl_invoice_id":141500,"target":"items","items":[{"id":55012,"item_order":2},{"id":55013,"item_order":1}]}'
      responses:
        '200':
          description: Reordered
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: object
                    properties:
                      target: { type: string, enum: [items, items_back] }
                      items:
                        type: array
                        items: { $ref: '#/components/schemas/InvoiceItem' }
        '400':
          description: 'target není items/items_back, nebo items prázdné/chybí id či item_order'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo edit'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: 'Invoice not found, nebo některé id v dávce nepatří této faktuře/firmě'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/get-payments/{id}:
    post:
      tags: [Invoice Payments]
      summary: List invoice payments
      description: 'Výpis úhrad faktury, řazeno pay_date ASC, item_order ASC. Vyžaduje právo `read`.'
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: List
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: object
                    properties:
                      payments:
                        type: array
                        items: { $ref: '#/components/schemas/InvoicePayment' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Invoice not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/create-payment:
    post:
      tags: [Invoice Payments]
      summary: Create payment – hotovostní úhrada zakládá pokladní doklad jako vedlejší efekt
      description: |
        Vyžaduje právo `edit`. **Na rozdíl od ostatních zápisových akcí modulu
        NEKONTROLUJE zámek/EET/uzávěrku DPH** – úhrady jdou zapisovat i na
        zamčenou fakturu (měkký zámek uzávěrky DPH, shodně s webem).

        Hotovostní úhrada (`pay_type=1`) založí pokladní doklad jako vedlejší efekt
        přepočtu součtu faktury – appka ho dostane v `cash_document` (nebo null).

        Pole `used_cl_invoice_id` = čerpání zálohy: cílová faktura musí být ze
        stejného partnera, z daňové zálohové řady (form_use=invoice_tax) a mít
        nevyčerpaný zbytek, jinak 404 „Advance invoice not found". Při čerpání se
        `pay_type`/`pay_vat` vždy vynutí na 1 bez ohledu na klientův vstup a základ
        DPH zálohy se přepočte synchronně v téže transakci.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    Povinné: `cl_invoice_id`, `pay_price` (number), `pay_date`. Nepovinné:
                    `pay_type` (0/1), `pay_vat` (0/1), `cl_payment_types_id`, `cl_currencies_id`
                    (výchozí měna faktury), `used_cl_invoice_id` (čerpání zálohy), `cl_users_id`,
                    `pay_doc`.
                  example: '{"cl_invoice_id":141500,"pay_price":1500,"pay_date":"2026-08-08","pay_type":1}'
      responses:
        '200':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/InvoicePaymentSaveResponse' }
        '400':
          description: 'Chybí cl_invoice_id/pay_price/pay_date, neplatný formát data, pay_type/pay_vat mimo 0/1'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo edit'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: 'Invoice/Payment type/Currency/Advance invoice/User not found'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Úhradu se nepodařilo uložit (cizí klíč)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/update-payment:
    post:
      tags: [Invoice Payments]
      summary: Update payment (partial)
      description: |
        Částečná úprava úhrady – POUZE poslaná pole se mění (rozlišuje se
        array_key_exists, ne isset, takže explicitní `null` u `used_cl_invoice_id`
        nebo `cl_users_id` hodnotu VYPRÁZDNÍ). Úhrada musí patřit firmě I dané
        faktuře. Změní-li se `used_cl_invoice_id`, přepočtou se OBĚ zálohy (stará
        i nová). Vyžaduje právo `edit`. Nekontroluje zámek/EET/uzávěrku DPH (stejně jako create-payment).
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: '`cl_invoice_id` (povinné), `id` (povinné, id úhrady) + měněná pole (viz create-payment)'
                  example: '{"cl_invoice_id":141500,"id":9001,"pay_price":1600}'
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/InvoicePaymentSaveResponse' }
        '400':
          description: 'Chybí cl_invoice_id nebo id, neplatné hodnoty'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo edit'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: 'Invoice not found, nebo Payment not found (neexistuje / cizí firma / patří jiné faktuře)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Úhradu se nepodařilo uložit (cizí klíč)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/delete-payment/{id}:
    post:
      tags: [Invoice Payments]
      summary: Delete payment – uklidí i navázané doklady
      description: |
        Smaže úhradu a s ní: spárovaný pokladní doklad (má-li cl_cash_id), zrcadlovou
        úhradu na dodacím listu (cl_delivery_note_payments, s přepočtem dodacího
        listu), řádky bankovní transakce (cl_bank_trans_items, s přepočtem součtu
        transakce) a přepočte základ DPH použité zálohy (byla-li čerpaná). Vyžaduje
        právo `edit`.
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: '`cl_invoice_id` (povinné)'
                  example: '{"cl_invoice_id":141500}'
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: object
                    properties:
                      id: { type: integer }
                      invoice: { $ref: '#/components/schemas/InvoiceListItem' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo edit'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: 'Invoice not found, nebo Payment not found'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Úhradu nelze smazat – je navázaná na jiný doklad (cizí klíč), nebo má její pokladní doklad zaevidovanou tržbu EET'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/pay-full/{id}:
    post:
      tags: [Invoice Payments]
      summary: Pay the full remaining amount at once
      description: |
        Jednorázová úhrada celé zbývající částky (InvoiceManager::makePayment()).
        Nekontroluje zámek/EET/uzávěrku DPH. Nejde o čerpání zálohy – neprovádí
        přepočet used_cl_invoice_id. Vyžaduje právo `edit`.
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'Nepovinné: `pay_date` (výchozí dnešek)'
                  example: '{"pay_date":"2026-08-08"}'
      responses:
        '200':
          description: Paid
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: object
                    properties:
                      payment_id: { type: integer }
                      cash_document: { $ref: '#/components/schemas/InvoiceCashDocumentRef' }
                      invoice: { $ref: '#/components/schemas/InvoiceListItem' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo edit'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Invoice not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '409':
          description: 'Fakturu se nepodařilo uhradit (chyba úhrady nebo cizí klíč)'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/pdf/{id}:
    post:
      tags: [Invoice]
      summary: Invoice PDF (zamyká fakturu u firem s lock_ap=1)
      description: |
        Vrací PDF faktury (`FileResponse`). Vyžaduje právo `report`.

        **Vedlejší efekt:** má-li firma cl_company.lock_ap=1, vygenerování PDF
        fakturu ZAMKNE (locked=1) – shodně s tlačítkem PDF ve webu, bez ohledu na
        to, že appka jen chtěla dokument stáhnout. Šablona se volí podle inv_type
        (0/1 → invoicev2/v3, 2 → correction) – jiný typ (včetně 3 = zálohová faktura)
        nebo neznámý parametr `template` vrací 400. Jméno souboru má lomítka
        nahrazená pomlčkou (`Content-Disposition`).
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'Nepovinné: `template` (jen pro inv_type=2 – "correction")'
      responses:
        '200':
          description: PDF binary
          content:
            application/pdf:
              schema: { type: string, format: binary }
        '400':
          description: 'Tuto šablonu nelze pro daný typ dokladu použít, nebo tisk PDF pro tento typ dokladu API zatím nepodporuje'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo report'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Invoice not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/isdoc/{id}:
    post:
      tags: [Invoice]
      summary: Invoice ISDOC XML
      description: 'Vrací ISDOC XML faktury (`FileResponse`, application/xml). Vyžaduje právo `report`.'
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: ISDOC XML
          content:
            application/xml:
              schema: { type: string, format: binary }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo report'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: 'Invoice not found, nebo ISDOC se nepodařilo vygenerovat'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/get-files/{id}:
    post:
      tags: [Invoice Files]
      summary: List invoice attachments
      description: 'Vyžaduje právo `read`.'
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: List
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/FileItem' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Invoice not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/file-upload:
    post:
      tags: [Invoice Files]
      summary: Upload attachment (base64 data URL)
      description: |
        Vyžaduje právo `edit`. Jméno nahrávaného souboru se sanitizuje proti path
        traversal (odstraní se cesta, nulové bajty a úvodní tečky) – appka nemůže
        názvem souboru zapsat nic mimo datovou složku firmy. Skutečné uložené jméno
        (po sanitizaci a případné deduplikaci) vrací odpověď v `file_name`.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    `cl_invoice_id` (povinné), `file` (povinné) – objekt
                    `{name, type, size, dataUrl}`, dataUrl ve tvaru `data:<mime>;base64,<data>`
      responses:
        '200':
          description: Uploaded
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: object
                    properties:
                      id: { type: integer }
                      file_name: { type: string }
        '400':
          description: 'Chybí cl_invoice_id nebo file, nebo se nepodařilo dekódovat base64'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo edit'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Invoice not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/file-download/{id}:
    post:
      tags: [Invoice Files]
      summary: Download attachment
      description: 'Vyžaduje právo `read`. Příloha mimo dosah uživatele (jiná firma, nebo rodičovská faktura mimo scope) vrací 404, ne 403.'
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: File binary
          content:
            application/octet-stream:
              schema: { type: string, format: binary }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: 'File not found, nebo File not found on disk'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/file-delete/{id}:
    post:
      tags: [Invoice Files]
      summary: Delete attachment
      description: 'Vyžaduje právo `erase`.'
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo erase'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: File not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/get-statuses:
    post:
      tags: [Invoice Lookups]
      summary: Invoice statuses (status_use = invoice)
      description: 'Vrací jen stavy se status_use=invoice – opravné doklady (jiný status_use) se zde nevrací. Vyžaduje právo `read`.'
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: List
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/InvoiceStatus' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/get-invoice-types:
    post:
      tags: [Invoice Lookups]
      summary: Invoice types (inv_type = 1 only)
      description: |
        Vrací jen typy s inv_type=1 (běžná faktura vydaná) – NE opravné/zálohové
        doklady a NE typy patřící přijatým fakturám (ty sdílí stejné hodnoty
        inv_type v jiném kontextu). Vyžaduje právo `read`.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: List
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/InvoiceOutgoingType' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/get-payment-types:
    post:
      tags: [Invoice Lookups]
      summary: Payment types
      description: 'Číselník typů úhrady firmy. Nefiltruje use_for_sale ani not_active (shodně s webem pro fakturu) – not_active se vrací v datech, appka si filtruje sama. Vyžaduje právo `read`.'
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: List
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/InvoicePaymentType' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/get-currencies:
    post:
      tags: [Invoice Lookups]
      summary: Currencies
      description: 'Vyžaduje právo `read`.'
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: List
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/InvoiceCurrency' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/get-centers:
    post:
      tags: [Invoice Lookups]
      summary: Cost centers
      description: 'Vyžaduje právo `read`.'
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: List
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/InvoiceCenter' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/get-vat-rates:
    post:
      tags: [Invoice Lookups]
      summary: VAT rates valid to a given date
      description: |
        Bez parametru `date` vrací sazby platné k dnešku, s `date` k datu dokladu
        (přesně jak by je nabídl program na faktuře s daným DUZP). Filtruje se jen
        `valid_from <= date`; `valid_to` se ZÁMĚRNĚ ignoruje – věrná kopie chování
        webu (RatesVatManager::findAllValid()), ne nedodělek. Vyžaduje právo `read`.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'Nepovinné: `date` (Y-m-d)'
                  example: '{"date":"2023-06-15"}'
      responses:
        '200':
          description: List
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/InvoiceVatRate' }
        '400':
          description: Neplatný parametr date
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/get-number-series:
    post:
      tags: [Invoice Lookups]
      summary: Number series (form_use = invoice)
      description: 'Jen řady faktur vydaných (form_use=invoice) – jiná řada by po přiřazení posunula cizí čítač. Vyžaduje právo `read`.'
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: List
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/InvoiceNumberSeries' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/get-bank-accounts:
    post:
      tags: [Invoice Lookups]
      summary: Bank accounts
      description: 'Vyžaduje právo `read`.'
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: List
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/InvoiceBankAccount' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/get-storages:
    post:
      tags: [Invoice Lookups]
      summary: Storages (flat list)
      description: 'Plochý seznam skladů firmy – appka si strom sestaví sama přes cl_storage_id. Vyžaduje právo `read`.'
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
      responses:
        '200':
          description: List
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/InvoiceStorage' }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /invoice/get-commissions:
    post:
      tags: [Invoice Lookups]
      summary: Open commissions (zakázky) for assignment
      description: |
        Otevřené zakázky pro přiřazení k faktuře/položce – filtr otevřenosti není
        sloupec `storno` (jen 10 řádků v celé DB), ale stav zakázky
        (`s_storno=0 AND s_fin=0`, chybějící stav se počítá jako otevřený). Parametr
        `include_closed=1` filtr vypne úplně. Vyžaduje právo `read`.
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'Nepovinné: `search` (LIKE přes cm_number), `include_closed` (0/1), `limit` (výchozí 50, 1–500), `offset` (výchozí 0)'
      responses:
        '200':
          description: List
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/InvoiceCommission' }
                  total: { type: integer }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '403':
          description: 'Chybí přístup k modulu Faktury vydané nebo právo read'
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /partners/get-all:
    post:
      tags: [Partners]
      summary: List partners (jen deleted=0)
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
                dataJSON:
                  type: string
                  description: |
                    Filtry: search (company/ico/person), cl_partners_category_id,
                    supplier, customer, producer, active, limit (50), offset (0)
      responses:
        '200':
          description: List
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: array
                    items: { $ref: '#/components/schemas/PartnerListItem' }
                  total: { type: integer }
        '401':
          description: Unauthorized
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /partners/get-one/{id}:
    post:
      tags: [Partners]
      summary: Partner detail with nested contacts/branches
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
      responses:
        '200':
          description: Detail
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/PartnerDetail' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /partners/create:
    post:
      tags: [Partners]
      summary: Create partner
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
                dataJSON:
                  type: string
                  description: |
                    company (required) + volitelná pole: street, zip, city, ico, dic, icdph,
                    email, web, person, phone, cl_partners_category_id, supplier, customer,
                    producer, active, due_date, partner_code, ...
      responses:
        '200':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: object, properties: { id: { type: integer } } }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /partners/update:
    post:
      tags: [Partners]
      summary: Update partner (partial)
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
                dataJSON: { type: string, description: 'id (required) + měněná pole' }
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: object, properties: { id: { type: integer } } }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /partners/delete/{id}:
    post:
      tags: [Partners]
      summary: Delete partner (soft-delete, deleted=1)
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessResponse' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /partners/create-contact:
    post:
      tags: [Partner Contacts]
      summary: Create partner contact
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
                dataJSON:
                  type: string
                  description: |
                    cl_partners_book_id (required), worker_name (required) + volitelná pole:
                    worker_first_name, worker_position, worker_email, worker_phone, worker_skype,
                    worker_other, description_txt, item_order, cl_partners_branch_id
      responses:
        '200':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: object, properties: { id: { type: integer } } }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Partner not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /partners/update-contact:
    post:
      tags: [Partner Contacts]
      summary: Update partner contact (partial)
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
                dataJSON: { type: string, description: 'id (required) + měněná pole' }
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: object, properties: { id: { type: integer } } }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /partners/delete-contact/{id}:
    post:
      tags: [Partner Contacts]
      summary: Delete partner contact
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessResponse' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /partners/create-branch:
    post:
      tags: [Partner Branches]
      summary: Create partner branch
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
                dataJSON:
                  type: string
                  description: |
                    cl_partners_book_id (required), b_name (required) + volitelná pole:
                    b_title, b_type, b_street, b_city, b_zip, cl_countries_id, b_phone,
                    b_email, b_person, b_ico, b_dic, use_as_main, item_order
      responses:
        '200':
          description: Created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: object, properties: { id: { type: integer } } }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Partner not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /partners/update-branch:
    post:
      tags: [Partner Branches]
      summary: Update partner branch (partial)
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
                dataJSON: { type: string, description: 'id (required) + měněná pole' }
      responses:
        '200':
          description: Updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: object, properties: { id: { type: integer } } }
        '400':
          description: Validation error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /partners/delete-branch/{id}:
    post:
      tags: [Partner Branches]
      summary: Delete partner branch
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                bearer_token: { type: string }
                sync_token: { type: string, description: 'Alternativa k bearer_token – firemní sync token' }
      responses:
        '200':
          description: Deleted
          content:
            application/json:
              schema: { $ref: '#/components/schemas/SuccessResponse' }
        '404':
          description: Not found
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }
        '500':
          description: Internal error
          content:
            application/json:
              schema: { $ref: '#/components/schemas/ErrorResponse' }

  /tasks/get-messages/{id}:
    post:
      tags: [Task Messages]
      summary: List task chat messages
      description: Returns all chat messages (cl_chat) of the given task, ordered by created ASC.
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Task ID
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: List of task messages
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: array
                    items:
                      $ref: '#/components/schemas/TaskMessage'
        '404':
          description: Task not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/create-message:
    post:
      tags: [Task Messages]
      summary: Create task chat message
      description: |
        Creates a new chat message (cl_chat) linked to a task.
        `create_by` and `change_by` default to "API".

        `created` and `changed` are always set to **server time** at insert and returned
        in the response — client-sent values are ignored, so thread ordering never depends
        on the device clock or timezone. A reply (`cl_chat_id`) also bumps `changed` of the
        parent message, same as the web chat.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    JSON-encoded message data:
                    - `cl_task_id` (int, **required**) - parent task ID
                    - `message` (string, **required**) - message text
                    - `cl_users_id` (int) - author user ID
                    - `sender_name` (string) - sender display name
                    - `sender_email` (string) - sender email
                    - `cl_status_id` (int) - status ID
                    - `cl_chat_id` (int) - parent message ID (thread/reply)
                    - `sent_partner` (int) - 0/1 sent to partner flag, defaults to 0
                  example: '{"cl_task_id": 123, "message": "Prosím o upřesnění zadání."}'
      responses:
        '200':
          description: Message created
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 5001
                      created:
                        type: string
                        description: Server-side insert time
                        example: '2026-07-12 14:30:00'
                      changed:
                        type: string
                        description: Server-side insert time (same as created)
                        example: '2026-07-12 14:30:00'
        '400':
          description: Validation error
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'
              examples:
                missingTaskId:
                  value: { status: error, message: 'cl_task_id is required' }
                missingMessage:
                  value: { status: error, message: 'message is required' }
        '404':
          description: Task not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/update-message:
    post:
      tags: [Task Messages]
      summary: Update task chat message
      description: |
        Updates an existing chat message (cl_chat). Only provided fields are updated (partial update).
        `change_by` defaults to "API".
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token, dataJSON]
              properties:
                bearer_token:
                  type: string
                dataJSON:
                  type: string
                  description: |
                    JSON-encoded update data:
                    - `id` (int, **required**) - message ID to update
                    - `message` (string) - message text
                    - `cl_status_id` (int) - status ID
                    - `cl_chat_id` (int) - parent message ID (thread/reply)
                    - `cl_users_id` (int) - author user ID
                    - `sender_name` (string) - sender display name
                    - `sender_email` (string) - sender email
                    - `sent_partner` (int) - 0/1 sent to partner flag
                  example: '{"id": 5001, "message": "Upravená zpráva."}'
      responses:
        '200':
          description: Message updated
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum: [ok]
                  data:
                    type: object
                    properties:
                      id:
                        type: integer
                        example: 5001
        '404':
          description: Message not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/delete-message/{id}:
    post:
      tags: [Task Messages]
      summary: Delete task chat message
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
          description: Message ID to delete
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [bearer_token]
              properties:
                bearer_token:
                  type: string
      responses:
        '200':
          description: Message deleted
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/SuccessResponse'
        '404':
          description: Message not found
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ErrorResponse'

  /tasks/mark-read/{id}:
    post:
      tags: [Task Messages]
      summary: Mark all task messages as read
      description: |
        Nastaví last_read timestamp uživatele pro úkol (R9). Appka volá při otevření chatu.
        Zprávy s `created` novějším než last_read se pak počítají do `unread_count` v get-all.
      parameters:
        - name: id
          in: path
          required: true
          schema: { type: integer }
          description: Task ID
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [sync_token, dataJSON]
              properties:
                sync_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: `cl_users_id` (int, required) - musí být aktivní člen firmy'
                  example: '{"cl_users_id":17}'
      responses:
        '200':
          description: Marked read
        '403':
          description: Invalid cl_users_id
        '404':
          description: Task not found

  /pos/register-device:
    post:
      tags: [Pos Devices]
      summary: Pair a POS device with a pairing code
      security: []
      description: |
        Spáruje pokladní appku s firmou. Bez uživatelského přihlášení - autorizace
        je samotný `pairing_code`, vygenerovaný v ERP (`Prodej → Pokladní zařízení`).
        Kód je jednorázový a platí 10 minut od vygenerování.

        Opakovaná registrace téhož `device_uuid` (appka o tom neví, jde o ruční
        znovu-spárování): vystaví se nový `device_token` (starý přestane platit),
        `active` se nastaví na `1` a konfigurace se přepíše z nového kódu -
        `vs_prefix` zůstává zachovaný, aby se nerozjel číslování dokladů.

        **Licence Prodejny (spec `2026-09-29-pos-sale-license-seats-design.md`,
        rozhodnutí 1).** Párování kontroluje jen to, že **firma** má zaplacenou a
        neprošlou licenci s modulem Prodejna, nebo jí modul bez licence povoluje
        tarif/zkušební doba (`SaleLicense::companyAllows()`) - bez ohledu na to,
        kolik míst v licenci zbývá a kdo párovací kód vygeneroval. **Párování
        samo místo v licenci neobsazuje** - to udělá až první doklad, který na
        spárovaném zařízení zapíše obsluha (`/sale/create`, `/sale/create-correction`
        nebo `/cash/create` s `device_token`). Dřív (`UserManager::isModuleAllowed()`)
        párování kontrolovalo a zároveň obsazovalo místo uživateli, který kód
        vygeneroval, i kdyby Prodejnu sám nikdy nepoužil.

        Chyby: problem+json (`type`, `title`, `status`, `detail`, `error`, viz
        schéma `PosProblem`), stejný tvar jako `PosDeviceAuthTrait`
        (`invalid_device_token`).
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [dataJSON]
              properties:
                dataJSON:
                  type: string
                  description: |
                    JSON s párovacím kódem a identitou zařízení. Všechna pole povinná:

                    - `pairing_code` - 8 znaků z abecedy bez záměnitelných znaků
                      (`ABCDEFGHJKLMNPQRSTUVWXYZ23456789`); mezery/pomlčky se ignorují,
                      malá/velká písmena nerozlišují (`PairingCode::normalize()`);
                    - `device_uuid` - `^[0-9a-fA-F-]{36}$`, ukládá se lowercase;
                    - `name` - 1 až 60 znaků po `trim()`, název zařízení v ERP přehledu.
                  example: '{"pairing_code":"AB3D-7Q9K","device_uuid":"11111111-2222-3333-4444-555555555555","name":"Tablet 1"}'
      responses:
        '200':
          description: Spárováno.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/PosDeviceConfig' }
        '400':
          description: |
            `invalid_input` - neplatný nebo chybějící `pairing_code`, `device_uuid`
            mimo formát, nebo `name` prázdné/delší než 60 znaků.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }
        '401':
          description: |
            `invalid_pairing_code` - kód neexistuje, je už použitý, prošlý (10 minut),
            nebo prohrál souběh se souběžným párováním stejným kódem.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }
        '403':
          description: |
            `module_not_licensed` - firma nemá aktivní modul Prodejna (`SaleLicense::companyAllows()`
            nad firemní licencí, ne uživatelem, který kód vygeneroval). Nevrací se za plnou licenci -
            místo v ní párování neobsazuje ani nekontroluje, viz popis výše.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }
        '409':
          description: |
            `eet_not_configured` (s polem `missing` - podmnožina `[eic, id_jednotky,
            id_pokl, certificate]`) - EET 2.0 firmy/pobočky není kompletně nastavené;
            `number_series_missing` (s polem `missing` - podmnožina `[sale_series,
            correction_series]`) - pobočka zařízení ani firma nemá číselnou řadu prodejek
            nebo oprav prodejek (prodej i storno by dostaly číslo "0"); zařízení se
            nespáruje, kód zůstane nepoužitý;
            `no_vs_prefix` - firma vyčerpala prefixy variabilních symbolů pokladních
            zařízení (901-999); `pairing_conflict` - souběžné párování téhož zařízení
            nebo vs_prefixu, zkusit znovu.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }
        '500':
          description: '`server_error` - neočekávaná chyba, zalogováno (`Debugger::log`, kanál `pos`).'
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }

  /pos/unregister-device:
    post:
      tags: [Pos Devices]
      summary: Unpair a POS device
      security:
        - deviceToken: []
      description: |
        Odvolá spárování zařízení (appka se sama odhlašuje, nebo reaguje na 401
        z jiného volání). Idempotentní - i odvolané zařízení (`active = 0`) může
        volání zopakovat, autorizace tokenem projde (`allowRevoked: true`) a
        odpověď je znovu 200. Po odvolání token přestane platit u ostatních
        endpointů (`invalid_device_token`).
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [device_token]
              properties:
                device_token: { type: string }
      responses:
        '200':
          description: Odhlášeno (nebo bylo odhlášené už dřív).
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
        '401':
          description: |
            `invalid_device_token` - token chybí nebo neexistuje, nebo firma zařízení
            už neexistuje (`PosDeviceGuard::deny()`; odvolané zařízení tady projde -
            `allowRevoked: true`).
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }

  /pos/get-catalog:
    post:
      tags: [Pos Devices]
      summary: Download the POS device's offline sales catalog (full or delta)
      security:
        - deviceToken: []
      description: |
        Katalog pro offline prodej: ceník s cenami a orientačním stavem skladu,
        skupiny ceníku, rychlé volby, sazby DPH, formy úhrady, obsluha s hashem
        PINu a konfigurace zařízení (`PosCatalogService::catalog()`).

        **Plná vs. delta:** bez `changed_since` v `dataJSON` (nebo prázdné) jde o
        plnou synchronizaci (`full: true` v odpovědi). S `changed_since` jde o
        deltu, ale server ji sám přepne zpět na plnou, když se od `changed_since`
        změnilo zařízení, jeho pobočka, nastavení firmy nebo některá měna firmy
        (`cl_pos_devices.changed`, `cl_company_branch.changed`, `cl_company.changed`,
        `cl_currencies.changed`) - typicky změna skladu, skupiny ceníku pobočky,
        měny, plátcovství DPH nebo pevného kurzu. `changed_since` v
        budoucnosti se bere jako aktuální čas serveru.

        **Stránkování:** stránkuje se jen `pricelist` (`after_id`/`next_after_id`).
        Chybějící `after_id`, nebo `after_id: 0`, znamená první stránku (id ceníku
        jsou vždy kladná) - teprve první stránka nese malé sekce, konfiguraci a
        `stock`, viz popis schématu `PosCatalog`. Příští `changed_since` appka
        vezme ze `server_time` PRVNÍ stránky série, ne poslední.

        Chyby: problem+json (schéma `PosProblem`), stejný tvar jako
        `PosDeviceAuthTrait`/`register-device`.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [device_token]
              properties:
                device_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    JSON objekt, všechna pole volitelná:

                    - `changed_since` - `"Y-m-d H:i:s"`. Neplatné datum (i kalendářně,
                      např. `2026-02-31`) -> 400. Vynecháno/prázdné = plná synchronizace.
                    - `after_id` - nezáporné celé číslo, id poslední položky z
                      předchozí stránky (`next_after_id`). `0` nebo vynecháno = první
                      stránka. Záporné nebo nečíselné -> 400.
                    - `limit` - celé číslo, počet položek ceníku na stránku, výchozí 500.
                      Hodnota mimo 1-1000 se ořízne do tohoto rozsahu; necelé číslo nebo
                      text -> 400.
                  example: '{"changed_since":"2026-09-28 08:00:00","after_id":92473,"limit":500}'
      responses:
        '200':
          description: Katalog (plný nebo delta) sestaven.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/PosCatalog' }
        '400':
          description: '`invalid_input` - neplatný `changed_since` (formát nebo kalendářně neplatné datum), neplatný `after_id` (nečíselný nebo záporný), nebo `limit`, který není celé číslo.'
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }
        '401':
          description: '`invalid_device_token` - token chybí, neexistuje, nebo je zařízení odvolané (`PosDeviceGuard::deny()`).'
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }
        '500':
          description: '`server_error` - neočekávaná chyba, zalogováno (`Debugger::log`, kanál `pos`).'
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }

  /sale/create:
    post:
      tags: [Pos Sale]
      summary: Create a sale from the POS device (offline cart)
      security:
        - deviceToken: []
      description: |
        Přijme hotový prodej z offline košíku pokladny (`SalePresenter::actionCreate()`,
        `PosSaleService`). Ukládá se stejně, jako by ho uzavřela webová prodejna:
        hlavička + položky + `SaleCloseService::close()` (číslo z řady, součty,
        pokladní doklad, skladový výdej) v jedné transakci `READ COMMITTED`. EET se
        odesílá **až po commitu** (`EetSaleService::sendForSale()`), protože evidence
        je nevratná.

        **Server prodej nikdy neodmítá kvůli skladu ani rozdílu součtů** - obojí se
        vrátí jako varování (`warnings`) a doklad se přesto uloží.

        **Pořadí zpracování:** autorizace tokenem zařízení -> tvar `api_request_key`
        (neplatný -> 400 `invalid_input`) -> **opakovaný požadavek (replay)** -> teprve
        pak validace zbytku těla (sazby DPH k datu, `var_symb` proti `vs_prefix`,
        forma úhrady) a ověření obsluhy. Už jednou přijatý prodej tak na opakování
        dostane uloženou odpověď, i kdyby obsluhu mezitím deaktivovali nebo jí
        odebrali právo, nebo se změnily sazby či prefix zařízení - opakování je appky
        vlastní potvrzení doručení, ne nový pokus o autorizaci.

        **Obsluha** (`cl_users_id`, povinné pole): ověří `PosCashier::check()` -
        člen firmy zařízení (`cl_access_company`), `not_active = 0` a `erased = 0`,
        právo zápisu do Prodejny (`SaleWriteRight`, stejně jako web). Neprojde (neaktivní,
        smazaná, bez práva, cizí firma) -> **403** `invalid_user`. Licence (`SaleLicense`)
        zápis nezamítá - viz níže. Po ověření se nastaví jako jednající uživatel (`create_by`/
        `change_by` na prodejce, položkách, výdejce, pohybech i pokladním dokladu).

        **Licence Prodejny - obsazení místa (spec `2026-09-29-pos-sale-license-seats-design.md`).**
        Obsluha bez místa, kterou by jinak `PosCashier` propustil, ale v licenci
        (`cl_users_license` obsluhy, ne firmy) není volné místo, se **nezamítá** -
        doklad se založí a odpověď nese varování `license_exceeded` (`PosWarning`).
        Obsluha bez platné licence (modul Prodejna v licenci chybí nebo mu prošel `exp`,
        licence prošlá / nezaplacená a tarif ani zkušební doba Prodejnu nepovolí) se také
        **nezamítá** - offline prodej mohl proběhnout ještě za platné licence; doklad se
        založí bez obsazení místa a odpověď nese varování `license_expired`. To ale
        jen, když prodej (`sale_time`) vznikl ještě za platnosti licence (dřívější
        z `license_end` do konce dne a `exp` modulu Prodejna, bez zaplacené licence
        konec zkušební doby) - po jejím konci -> **403** `license_ended`.
        Má-li obsluha místo v licenci, nebo je volné, transakce zápisu jí ho
        obsadí (zamkne řádek licence `FOR UPDATE`, `SaleLicense::claim()`), pokud
        ho ještě neměla - stejně jako když poprvé otevře Prodejnu na webu.

        **Idempotence** (vzor `/cash/create`, `api_request_key` je tu ale **povinný**):
        - stejný `api_request_key` a stejný otisk obsahu -> **200** +
          `Idempotent-Replayed: true`, odpověď se sestaví **znovu z databáze**
          (`PosSaleService::replayResponse()`) - aktuální stav EET (může se mezitím
          zaevidovat i doposlat), ale **`warnings` jsou vždy prázdné**: appka si
          varování z prvního zpracování drží sama, server je znovu neposílá;
        - prodejka je uložená, ale mezi commitem a odesláním do EET proces spadl (bez
          `cl_eet_id`, tržba evidovat měla) -> opakování ji **odešle teď**, se
          `sale_time` z prvního požadavku (fronta doposílání vidí jen `cl_eet`, takže
          bez tohohle by tržba nikdy neodešla); `EetSaleService::sendForSale()` má
          vlastní ochranu proti dvojí evidenci (`GET_LOCK` na prodejku + `cl_eet_id`
          znovu z DB pod zámkem), takže souběh dvou opakování pošle tržbu jen jednou;
        - stejný klíč, jiný otisk -> **409** `request_key_conflict`, nic se nezaloží;
        - souběh dvou prvních požadavků se stejným klíčem: prohraný narazí na unikátní
          index `(cl_company_id, api_request_key)`, transakce se vrátí a odpoví se
          jako opakování (druhá větev výše).

        **Deadlock / čekání na zámek při souběžném prodeji téže položky** (zámek šarží
        je pro API sale zapnutý vždy, aby dva souběžné prodeje téže položky
        neztratily úbytek skladu): server **celou transakci zopakuje** s náhodnou
        pauzou - při deadlocku (1213) až do 3 pokusů celkem, při vypršení čekání na
        zámek (1205, trvá `innodb_lock_wait_timeout`) jen jednou (2 pokusy). Potom
        **500** `server_error` - appka
        zopakuje požadavek se stejným `api_request_key`, což je bezpečné (transakce
        byla celá vrácená, včetně čísla z řady).

        **Zmizelá položka ceníku se neodmítá.** Nemá-li firma `cl_pricelist_id`
        z položky (smazaná/přesunutá mezi synchronizacemi appky), řádek se uloží jako
        **volná položka** - `cl_pricelist_id = null`, popisek z `item_label` (jinak
        „Položka #<id>“), bez skladového výdeje - a odpověď nese varování
        `unknown_item` s `item_index` a `cl_pricelist_id`.

        **`discount`** na hlavičce je **procento** (sloupec `cl_sale.discount`) - sleva
        pobočky (`cl_company_branch`) se u prodeje z pokladny **neuplatní**, jen sleva
        poslaná appkou.

        **Deaktivovaná forma úhrady** (`cl_payment_types.not_active = 1`) firmy
        zařízení se **přijme** - prodej u pultu už proběhl, odmítnutí by ho nešlo znovu
        uložit; cizí forma úhrady (jiné firmy nebo neexistující) -> 400 `invalid_input`.

        **Číslo prodejky** přidělí `SaleCloseService::close()` z číselné řady **pobočky**
        zařízení, jinak z výchozí řady prodejek firmy - stejně jako web. Řada uložená
        u zařízení (`cl_pos_devices.cl_number_series_id`) se nepoužívá.

        **Známé omezení:** nemá-li pobočka ani firma číselnou řadu pro prodej (`sale`),
        prodej se uloží s číslem `"0"` (stávající chování webu), **bez pokladního
        dokladu**, a odeslání do EET selže (`porad_cis`, `eet_pending` s trvalou
        chybou). Odpověď nese varování `no_number_series`. Náprava je na straně
        administrátora firmy (nastavit řadu prodejek pobočce nebo firmě), ne appky.

        Chyby: problem+json (schéma `PosProblem`, pole `error`), stejný tvar jako
        `/api/pos/*` a `/cash/create` s `device_token`. `item_index` je u chyb
        konkrétní položky (`invalid_input` na položce, `invalid_vat_rate`).
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [device_token, dataJSON]
              properties:
                device_token: { type: string }
                dataJSON:
                  description: Prodejka - viz schéma `PosSaleCreate`.
                  allOf:
                    - $ref: '#/components/schemas/PosSaleCreate'
                  example: '{"api_request_key":"a1b2c3d4-1234-4a5b-8c9d-0e1f2a3b4c5d","sale_time":"2026-09-28 14:32:07","cl_users_id":89,"cl_payment_types_id":1,"cash_rec":1000,"client_total":888,"discount":0,"var_symb":null,"items":[{"cl_pricelist_id":253510,"quantity":1,"price_e":149.5,"vat":21,"discount":0},{"cl_pricelist_id":null,"item_label":"Vratná záloha","quantity":1,"price_e":50,"vat":0,"discount":0}]}'
      responses:
        '200':
          description: |
            Prodejka založena, nebo (`Idempotent-Replayed: true`) opakovaný požadavek
            vrátil odpověď sestavenou znovu z databáze - viz popis idempotence výše.
          headers:
            Idempotent-Replayed:
              description: '`true`, když jde o opakovaný požadavek se stejným `api_request_key` a otiskem - prodejka nevznikla znovu.'
              schema: { type: string, enum: ['true'] }
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/PosSaleResult' }
        '400':
          description: |
            `invalid_input` - chybí nebo je neplatný `api_request_key`, neplatný nebo
            chybějící `sale_time` (formát, nebo víc než 5 minut v budoucnosti),
            neplatná/chybějící `cl_users_id`/`cl_payment_types_id` (nekladné číslo),
            cizí nebo neexistující `cl_payment_types_id` (pozor: deaktivovaná forma
            úhrady firmy zařízení se **přijme**, jen cizí/neexistující je chyba),
            prázdné `items`, nebo položka bez číselného `quantity`/`price_e`
            (s `item_index`).

            `invalid_var_symb` - `var_symb` není přesně 10 číslic začínajících
            `vs_prefix` zařízení.

            `invalid_vat_rate` (s `item_index`) - sazba položky není mezi platnými
            sazbami k datu `sale_time` (neplátce DPH smí jen `0`).
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }
        '401':
          description: '`invalid_device_token` - token chybí, neexistuje, nebo je zařízení odvolané (`PosDeviceGuard::deny()`).'
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }
        '403':
          description: |
            `invalid_user` - obsluha `cl_users_id` není členem firmy zařízení, je
            neaktivní/smazaná nebo nemá právo zápisu do Prodejny. **Licenční důvody
            403 nevracejí** - jinak platná obsluha bez místa v plné licenci se přijme
            s varováním `license_exceeded`, bez platné licence (licence obsluhy nemá
            modul Prodejna, modul Prodejna v licenci prošel (`exp`), licence prošla,
            zkušební doba skončila, tarif Prodejnu neumožňuje) s varováním
            `license_expired` (`PosWarning`) - jen doklad z doby platnosti licence.

            `license_ended` - obsluha nemá platnou licenci Prodejny a doklad
            (`sale_time`) vznikl až po konci její platnosti (dřívější z `license_end`
            do konce dne a `exp` modulu Prodejna; bez zaplacené licence konec
            zkušební doby; licence bez modulu Prodejna nikdy nebyla platná). Čas
            rovný konci platnosti je ještě platný. Neaktivní / cizí obsluha dostane
            `invalid_user` přednostně; opakovaný požadavek (replay) přijatého dokladu
            vrací uloženou odpověď. Trvalé odmítnutí - opakování nepomůže.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }
        '409':
          description: '`request_key_conflict` - `api_request_key` už byl ve firmě použit pro požadavek s jiným obsahem. Nic se nezaložilo ani nezaevidovalo.'
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }
        '500':
          description: |
            `server_error` - založení selhalo i po 3 pokusech (deadlock/zámek), sestavení
            odpovědi selhalo (prodejka je ale uložená - opakování se stejným klíčem ji
            vrátí jako replay), nebo jiná neočekávaná chyba. Zalogováno bez tokenu a PINu
            (`Debugger::log`, kanál `pos`).
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }

  /sale/create-correction:
    post:
      tags: [Pos Sale]
      summary: Create a sale correction (storno) from the POS device
      security:
        - deviceToken: []
      description: |
        Stornuje prodejku odeslanou pokladnou, i offline a později
        (`SalePresenter::actionCreateCorrection()`, `SaleCorrectionService`, krok 6).
        Položky vrácení určí appka pořadím v původním `/sale/create` (`item_index`,
        0-based). Server vytvoří opravnou prodejku (`cl_sale.sale_type = 1`) a
        v jedné transakci `READ COMMITTED` (stejný vzor jako `/sale/create`):
        vrátí položky na sklad (příjemka), založí záporný pokladní doklad stejnou
        formou úhrady jako původní prodejka a zapíše vratku do původních řádků
        (`quantity_in`). EET se odesílá **až po commitu**, jako samostatná záporná
        tržba s časem storna (`sale_time` tohoto požadavku, ne času původního
        prodeje).

        **Pořadí zpracování** je stejné jako `/sale/create`: autorizace tokenem
        zařízení -> tvar `api_request_key` -> **opakovaný požadavek (replay)** ->
        validace zbytku těla -> ověření obsluhy -> `SaleCorrectionService`.

        **Obsluha** (`cl_users_id`) - stejné ověření `PosCashier::check()` jako
        `/sale/create` (právo zápisu do Prodejny, ne do Pokladny), včetně obsazení
        místa v licenci, varování `license_exceeded` u plné licence a `license_expired`
        bez platné licence (jen storno z doby platnosti licence, jinak **403** `license_ended`).
        Neprojde z jiného než licenčního důvodu -> **403** `invalid_user`.

        **Idempotence** - stejné jako `/sale/create` (storno je taky řádek
        `cl_sale`, sdílí unikátní index `(cl_company_id, api_request_key)`):
        - stejný klíč a stejný otisk -> **200** + `Idempotent-Replayed: true`,
          odpověď znovu z databáze, `warnings` prázdné;
        - stejný klíč, jiný otisk (i klíč použitý dřív pro prodej) -> **409**
          `request_key_conflict`;
        - souběh dvou prvních požadavků se stejným klíčem: `SaleCorrectionService`
          po zamčení původní prodejky (`SELECT … FOR UPDATE`) ještě pod zámkem
          znovu zkontroluje `cl_sale.api_request_key` téže firmy - druhý požadavek
          tak dostane replay i tehdy, když v okamžiku vstupu do transakce první
          požadavek ještě neexistoval (čekal na zámek, ne na unikátní index).

        **Kontroly nad zamčenou původní prodejkou** (`CorrectionCheck::decide()`),
        v tomto pořadí:
        - prodejka neexistuje, patří jiné firmě, je sama stornem (`sale_type = 1`),
          nebo nemá číslo -> **409** `invalid_sale`;
        - prodejka nemá žádný řádek s kladným množstvím (jen vratky/nuly) -> **409**
          `invalid_sale` (není co stornovat);
        - všechny řádky **s kladným množstvím** mají už vráceno vše (součet ze
          **všech** storen prodejky, i z jiných zařízení, ne `quantity_in`, které
          web plní jen svým vlastním stornem) -> **409** `already_cancelled`;
        - poslaný `item_index` neodpovídá žádnému řádku původní prodejky -> **400**
          `invalid_input` s `item_index` (hodnota `item_index`, ne pozice v poli
          `items`);
        - poslaný `item_index` míří na řádek se záporným nebo nulovým množstvím
          (vratka v původní prodejce) -> **400** `invalid_input` s `item_index`
          („Řádek se záporným nebo nulovým množstvím nejde stornovat.“);
        - součet dosavadních vratek řádku a tohoto požadavku přesahuje prodané
          množství -> **409** `quantity_exceeded` s `item_index`.

        Záruka proti vrácení víc, než se prodalo, platí pro storna z pokladen
        navzájem a pro storno z pokladny po už uloženém webovém stornu. Webové
        storno běží bez zámku, takže souběžně se stornem z pokladny může vrátit víc
        (dosavadní omezení webu, `ODPOVED-backend-pokladna.md` L2).

        Řádky storna mají `item_order` od 0 podle **vzestupného `item_index`**, ne
        podle pořadí v poli `items`. U prodejky z webu se stejným `item_order` na
        více řádcích jde adresovat jen první z nich (L12). Rekapitulace DPH storna
        (`vat_summary`) je ze sazeb platných k datu storna, stejně jako na webu (L13).

        **`var_symb` se nenastavuje.** Storno má vždy prázdný `var_symb` - stejně
        jako webové storno. Odvození z čísla stornující prodejky (jako u
        `/sale/create`) by kolidovalo s VS **původní** prodejky (`OPR-260025` ->
        `260025` je VS `PR-260025`) a párování banky jde jen podle VS. Appka
        poslaný `var_symb` (pokud nějaký pošle) tiše ignoruje.

        **Forma úhrady, pobočka, sklad, partner, sleva a měna** hlavičky storna se
        přebírají z **původní prodejky** (`SaleCorrectionService::insertHeader()`),
        appka je neposílá.

        **Číslo storna** je z číselné řady **oprav pobočky původní prodejky**
        (`cl_company_branch.cl_number_series_id_correction`), jinak z výchozí řady
        firmy `sale_correction` - na rozdíl od `/sale/create` ne z řady zařízení
        ani přihlášeného uživatele (API žádnou identitu uživatele nemá). Chybí-li
        řada, storno se přesto uloží s číslem `"0"`, **bez pokladního dokladu**,
        a odpověď nese varování `no_number_series` (vlastní text pro řadu oprav).

        **Příjemka vzniká, jen když je co přijmout.** Vrácený řádek s položkou
        ceníku, ale bez skladu (ani na hlavičce), se na sklad nevrátí a odpověď
        nese varování `no_storage` - prodej takový řádek bez skladu ani nevydal.
        Řádky bez `cl_pricelist_id` (volné položky) se nikdy nepřijímají. Neobsahuje-li
        storno žádný řádek k příjmu, příjemka nevznikne vůbec (na rozdíl od
        webového storna, které příjemku zakládá vždy).

        **`client_total`** musí být `<= 0`. Appka má poslat **skutečně vrácenou
        částku**, ne nulu jako zástupný údaj - `0` s nenulovým serverovým součtem
        se vezme doslova: `price_e2_vat` se přepíše na `0`, rozdíl jde do
        `price_correction` a u hotovostní úhrady vznikne záporný pokladní doklad
        na **`0 Kč`** (`total_mismatch` ve `warnings`).

        Chyby: problem+json (schéma `PosProblem`, pole `error`), stejný tvar jako
        `/sale/create`. `item_index` má u této akce dva různé významy podle chyby -
        viz popis pole `item_index` schématu `PosProblem`.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [device_token, dataJSON]
              properties:
                device_token: { type: string }
                dataJSON:
                  description: Storno - viz schéma `PosSaleCorrection`.
                  allOf:
                    - $ref: '#/components/schemas/PosSaleCorrection'
                  example: '{"api_request_key":"b2c3d4e5-2345-4a5b-8c9d-0e1f2a3b4c5e","cl_sale_id":31474,"cl_users_id":89,"sale_time":"2026-09-28 15:10:00","items":[{"item_index":0,"quantity":1}],"client_total":-339}'
      responses:
        '200':
          description: |
            Storno založeno, nebo (`Idempotent-Replayed: true`) opakovaný požadavek
            vrátil odpověď sestavenou znovu z databáze - viz popis idempotence výše.
          headers:
            Idempotent-Replayed:
              description: '`true`, když jde o opakovaný požadavek se stejným `api_request_key` a otiskem - storno nevzniklo znovu.'
              schema: { type: string, enum: ['true'] }
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    description: Tvar `PosSaleResult`, navíc `correction_cl_sale_id`.
                    allOf:
                      - $ref: '#/components/schemas/PosSaleResult'
                      - type: object
                        properties:
                          correction_cl_sale_id:
                            type: integer
                            description: Id původní (stornované) prodejky.
                            example: 31474
        '400':
          description: |
            `invalid_input` - chybí nebo je neplatný `api_request_key`, neplatný
            nebo chybějící `sale_time`/`cl_sale_id`/`cl_users_id`, prázdné `items`,
            položka `items` bez číselného `item_index`/`quantity` (`item_index` =
            pozice v poslaném poli), kladný `client_total`, `item_index`, který
            neodpovídá žádnému řádku původní prodejky, nebo `item_index` řádku se
            záporným či nulovým množstvím, který nejde stornovat (u obou posledních
            `item_index` = hodnota z požadavku, pořadí řádku v původní prodejce).
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }
        '401':
          description: '`invalid_device_token` - token chybí, neexistuje, nebo je zařízení odvolané (`PosDeviceGuard::deny()`).'
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }
        '403':
          description: |
            `invalid_user` - obsluha `cl_users_id` není členem firmy zařízení, je
            neaktivní/smazaná nebo nemá právo zápisu do Prodejny. **Licenční důvody
            403 nevracejí** - jinak platná obsluha bez místa v plné licenci se přijme
            s varováním `license_exceeded`, bez platné licence (licence obsluhy nemá
            modul Prodejna, modul Prodejna v licenci prošel (`exp`), licence prošla,
            zkušební doba skončila, tarif Prodejnu neumožňuje) s varováním
            `license_expired` (`PosWarning`) - jen doklad z doby platnosti licence.

            `license_ended` - obsluha nemá platnou licenci Prodejny a doklad
            (`sale_time`) vznikl až po konci její platnosti (dřívější z `license_end`
            do konce dne a `exp` modulu Prodejna; bez zaplacené licence konec
            zkušební doby; licence bez modulu Prodejna nikdy nebyla platná). Čas
            rovný konci platnosti je ještě platný. Neaktivní / cizí obsluha dostane
            `invalid_user` přednostně; opakovaný požadavek (replay) přijatého dokladu
            vrací uloženou odpověď. Trvalé odmítnutí - opakování nepomůže.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }
        '409':
          description: |
            `request_key_conflict` - `api_request_key` byl ve firmě už použit pro
            požadavek s jiným obsahem (i pro prodej). Nic se nezaložilo.

            `invalid_sale` - `cl_sale_id` neexistuje, patří jiné firmě, je sama
            stornem, nemá číslo, nebo nemá žádný řádek s kladným množstvím (není co
            stornovat).

            `already_cancelled` - prodejka je už celá stornovaná (součet ze všech
            storen, i z jiných zařízení; počítají se jen řádky s kladným množstvím).

            `quantity_exceeded` (s `item_index` = pořadí řádku v původní prodejce) -
            storno by vrátilo víc, než se u tohoto řádku prodalo a dosud nevrátilo.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }
        '500':
          description: |
            `server_error` - založení selhalo i po opakování (deadlock/zámek),
            sestavení odpovědi selhalo (storno je ale uložené - opakování se stejným
            klíčem ho vrátí jako replay), nebo jiná neočekávaná chyba. Zalogováno
            bez tokenu a PINu (`Debugger::log`, kanál `pos`).
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }
  /sale/get-status:
    post:
      tags: [Pos Sale]
      summary: Bank payment status of sale receipts for the POS history
      security:
        - deviceToken: []
      description: |
        Stav bankovní platby prodejek pro historii pokladny („platba přijata“),
        `SalePresenter::actionGetStatus()`, `PosSaleStatus` (zadání párování QR
        prodejek, oddíl 6). Jen čtení, nic nezapisuje.

        - Vrací jen prodejky **firmy zařízení**. Id jiné firmy i neexistující id
          jsou v `not_found` - odpověď neprozradí, jestli id jinde existuje.
        - `price_payed` a `bank_pay_date` plní párování bankovních plateb
          (`BankTransManager`, automaticky při importu výpisu i ručně v Bance).
        - `bank_pay_skipped: true` - obsluha na webu označila prodejku
          „Platbu neřešit“; prodejka už na platbu z banky nečeká.
        - Pořadí `sales` odpovídá pořadí v požadavku, duplicitní id se vrací jednou.
      requestBody:
        required: true
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              required: [device_token, dataJSON]
              properties:
                device_token: { type: string }
                dataJSON:
                  type: object
                  required: [cl_sale_ids]
                  properties:
                    cl_sale_ids:
                      type: array
                      minItems: 1
                      maxItems: 200
                      items: { type: integer, minimum: 1 }
                      description: Id prodejek (`cl_sale.id` z odpovědi `/sale/create`), nejvýš 200.
                  example: '{"cl_sale_ids":[31543,31544]}'
      responses:
        '200':
          description: Stav plateb nalezených prodejek.
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data:
                    type: object
                    properties:
                      sales:
                        type: array
                        items:
                          type: object
                          properties:
                            id: { type: integer, example: 31543 }
                            price_payed:
                              type: number
                              description: Uhrazeno z banky (součet spárovaných plateb), v měně prodejky.
                              example: 245.0
                            bank_pay_date:
                              type: string
                              format: date
                              nullable: true
                              description: Datum poslední spárované bankovní platby, `null` = zatím žádná.
                              example: '2026-09-30'
                            bank_pay_skipped:
                              type: boolean
                              description: Prodejka označená na webu „Platbu neřešit“.
                      not_found:
                        type: array
                        items: { type: integer }
                        description: Id, která firma zařízení nemá.
                        example: [999]
        '400':
          description: |
            `invalid_input` - chybí nebo je prázdné `cl_sale_ids`, není to pole,
            obsahuje víc než 200 id, nebo id, které není kladné celé číslo.
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }
        '401':
          description: '`invalid_device_token` - neplatný nebo odvolaný token zařízení.'
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }
        '500':
          description: '`server_error` - dotaz selhal; zalogováno bez tokenu (`Debugger::log`, kanál `pos`).'
          content:
            application/problem+json:
              schema: { $ref: '#/components/schemas/PosProblem' }

  /oro/get-all:
    post:
      tags: [ORO]
      summary: Seznam oznámení ORO
      description: |
        Stránkovaný seznam oznámení ORO, od nejnovějšího `oznameni_za_den`. Rozsah jako na webu
        (firma, role „jen vlastní záznamy“). Právo `read` k modulu ORO; sync_token se odmítá.
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    JSON objekt, vše nepovinné:
                    - `date_from`, `date_to` (YYYY-MM-DD) – období podle `oznameni_za_den`, včetně krajních dnů
                    - `changed_since` (YYYY-MM-DD nebo YYYY-MM-DD HH:MM:SS) – založené nebo změněné od
                    - `typ_podani` – přesná shoda
                    - `search` – část `id_oznameni` nebo popisu
                    - `limit` (1–500, výchozí 50), `offset`
                  example: '{"date_from":"2026-09-01","limit":100}'
      responses:
        '200':
          description: Stránka oznámení
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  total: { type: integer }
                  data: { type: array, items: { $ref: '#/components/schemas/OroListItem' } }
        '400': { $ref: '#/components/responses/ReadBadRequest' }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /oro/get-one/{id}:
    post:
      tags: [ORO]
      summary: Detail oznámení ORO s položkami a výrobky
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody: { $ref: '#/components/requestBodies/ReadOneRequest' }
      responses:
        '200':
          description: Oznámení
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/OroDetail' }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '404': { $ref: '#/components/responses/ReadNotFound' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /oro/get-statuses:
    post:
      tags: [ORO]
      summary: Stavy oznámení ORO firmy
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      requestBody: { $ref: '#/components/requestBodies/ReadOneRequest' }
      responses:
        '200':
          description: Stavy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: array, items: { $ref: '#/components/schemas/StatusListItem' } }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /oro/get-deleted:
    post:
      tags: [ORO]
      summary: Smazaná oznámení, položky a výrobky od času since
      description: |
        Tabulky `cl_oro`, `cl_oro_items`, `cl_oro_products`. Položky a výrobky smazané spolu
        s celým oznámením (kaskádou) se tu neobjeví – při `table = cl_oro` zahoďte i jeho řádky.
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      requestBody: { $ref: '#/components/requestBodies/ReadDeletedRequest' }
      responses:
        '200':
          description: Smazané záznamy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  total: { type: integer }
                  data: { type: array, items: { $ref: '#/components/schemas/DeletedRecord' } }
        '400': { $ref: '#/components/responses/ReadBadRequest' }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /coccz/get-all:
    post:
      tags: [COCCZ]
      summary: Seznam exportů COCCZ
      description: |
        Stránkovaný seznam exportů pro Coca-Cola HBC, od nejnovějšího `export_date`.
        Právo `read` k modulu Coca-Cola; sync_token se odmítá.
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    JSON objekt, vše nepovinné:
                    - `date_from`, `date_to` (YYYY-MM-DD) – období podle `export_date`, včetně krajních dnů
                    - `changed_since` (YYYY-MM-DD nebo YYYY-MM-DD HH:MM:SS)
                    - `sent` – `all` (výchozí), `sent`, `unsent`, `error`
                    - `limit` (1–500, výchozí 50), `offset`
                  example: '{"sent":"error"}'
      responses:
        '200':
          description: Stránka exportů
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  total: { type: integer }
                  data: { type: array, items: { $ref: '#/components/schemas/CocczListItem' } }
        '400': { $ref: '#/components/responses/ReadBadRequest' }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /coccz/get-one/{id}:
    post:
      tags: [COCCZ]
      summary: Detail exportu COCCZ se souhrnem (bez řádků)
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody: { $ref: '#/components/requestBodies/ReadOneRequest' }
      responses:
        '200':
          description: Export
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/CocczDetail' }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '404': { $ref: '#/components/responses/ReadNotFound' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /coccz/get-items/{id}:
    post:
      tags: [COCCZ]
      summary: Řádky exportu COCCZ po stránkách
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: 'JSON: limit (1–500, výchozí 50), offset; řazení item_order, id'
                  example: '{"limit":500,"offset":1000}'
      responses:
        '200':
          description: Stránka řádků
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  total: { type: integer }
                  data: { type: array, items: { $ref: '#/components/schemas/CocczItem' } }
        '400': { $ref: '#/components/responses/ReadBadRequest' }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '404': { $ref: '#/components/responses/ReadNotFound' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /coccz/get-statuses:
    post:
      tags: [COCCZ]
      summary: Stavy exportů COCCZ firmy
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      requestBody: { $ref: '#/components/requestBodies/ReadOneRequest' }
      responses:
        '200':
          description: Stavy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: array, items: { $ref: '#/components/schemas/StatusListItem' } }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /coccz/get-deleted:
    post:
      tags: [COCCZ]
      summary: Smazané exporty a řádky od času since
      description: |
        Tabulky `cl_coccz`, `cl_coccz_items`. Řádky smazané spolu s celým exportem (kaskádou)
        se tu neobjeví – při `table = cl_coccz` zahoďte i jeho řádky.
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      requestBody: { $ref: '#/components/requestBodies/ReadDeletedRequest' }
      responses:
        '200':
          description: Smazané záznamy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  total: { type: integer }
                  data: { type: array, items: { $ref: '#/components/schemas/DeletedRecord' } }
        '400': { $ref: '#/components/responses/ReadBadRequest' }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /transports/get-all:
    post:
      tags: [Transports]
      summary: Seznam jízd Dopravy
      description: |
        Stránkovaný seznam jízd, od nejnovějšího `transport_date`. Rozsah jako na webu (firma,
        „jen vlastní záznamy“, pobočka uživatele). Právo `read` k modulu Doprava; sync_token se odmítá.
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    JSON objekt, vše nepovinné:
                    - `date_from`, `date_to` (YYYY-MM-DD) – období podle `transport_date`, včetně krajních dnů
                    - `changed_since` (YYYY-MM-DD nebo YYYY-MM-DD HH:MM:SS)
                    - `status` – `all` (výchozí), `open`, `done`
                    - `transport_type_id` – typ dopravy
                    - `search` – část čísla jízdy
                    - `limit` (1–500, výchozí 50), `offset`
                  example: '{"status":"open"}'
      responses:
        '200':
          description: Stránka jízd
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  total: { type: integer }
                  data: { type: array, items: { $ref: '#/components/schemas/TransportListItem' } }
        '400': { $ref: '#/components/responses/ReadBadRequest' }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /transports/get-one/{id}:
    post:
      tags: [Transports]
      summary: Detail jízdy s dodacími listy
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody: { $ref: '#/components/requestBodies/ReadOneRequest' }
      responses:
        '200':
          description: Jízda
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/TransportDetail' }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '404': { $ref: '#/components/responses/ReadNotFound' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /transports/get-statuses:
    post:
      tags: [Transports]
      summary: Stavy jízd firmy
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      requestBody: { $ref: '#/components/requestBodies/ReadOneRequest' }
      responses:
        '200':
          description: Stavy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: array, items: { $ref: '#/components/schemas/StatusListItem' } }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /transports/get-deleted:
    post:
      tags: [Transports]
      summary: Smazané jízdy a jejich dodací listy od času since
      description: |
        Tabulky `cl_transport`, `cl_transport_docs`. Řádky smazané spolu s celou jízdou (kaskádou)
        se tu neobjeví – při `table = cl_transport` zahoďte i její řádky.
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      requestBody: { $ref: '#/components/requestBodies/ReadDeletedRequest' }
      responses:
        '200':
          description: Smazané záznamy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  total: { type: integer }
                  data: { type: array, items: { $ref: '#/components/schemas/DeletedRecord' } }
        '400': { $ref: '#/components/responses/ReadBadRequest' }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /deliveries/get-all:
    post:
      tags: [Deliveries]
      summary: Seznam dodávek
      description: |
        Stránkovaný seznam dodávek od dodavatelů, od nejnovějšího `delivery_date`. Rozsah jako na webu
        (firma, „jen vlastní záznamy“). Právo `read` k modulu Dodávky; sync_token se odmítá.
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    JSON objekt, vše nepovinné:
                    - `date_from`, `date_to` (YYYY-MM-DD) – období podle `delivery_date`, včetně krajních dnů
                    - `changed_since` (YYYY-MM-DD nebo YYYY-MM-DD HH:MM:SS)
                    - `status` – `all` (výchozí), `open`, `done` (done = označeno hotovo nebo stav hotovo)
                    - `partner_id` – dodavatel
                    - `search` – část čísla dodávky, názvu dodavatele na dodávce nebo v adresáři
                    - `limit` (1–500, výchozí 50), `offset`
                  example: '{"status":"open","partner_id":5}'
      responses:
        '200':
          description: Stránka dodávek
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  total: { type: integer }
                  data: { type: array, items: { $ref: '#/components/schemas/DeliveryListItem' } }
        '400': { $ref: '#/components/responses/ReadBadRequest' }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /deliveries/get-one/{id}:
    post:
      tags: [Deliveries]
      summary: Detail dodávky s položkami a kontrolou množství
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody: { $ref: '#/components/requestBodies/ReadOneRequest' }
      responses:
        '200':
          description: Dodávka
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/DeliveryDetail' }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '404': { $ref: '#/components/responses/ReadNotFound' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /deliveries/get-statuses:
    post:
      tags: [Deliveries]
      summary: Stavy dodávek firmy
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      requestBody: { $ref: '#/components/requestBodies/ReadOneRequest' }
      responses:
        '200':
          description: Stavy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: array, items: { $ref: '#/components/schemas/StatusListItem' } }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /deliveries/get-deleted:
    post:
      tags: [Deliveries]
      summary: Smazané dodávky a položky od času since
      description: |
        Tabulky `cl_delivery`, `cl_delivery_items`. Položky smazané spolu s celou dodávkou (kaskádou)
        se tu neobjeví – při `table = cl_delivery` zahoďte i její položky.
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      requestBody: { $ref: '#/components/requestBodies/ReadDeletedRequest' }
      responses:
        '200':
          description: Smazané záznamy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  total: { type: integer }
                  data: { type: array, items: { $ref: '#/components/schemas/DeletedRecord' } }
        '400': { $ref: '#/components/responses/ReadBadRequest' }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /deliverynotein/get-all:
    post:
      tags: [Delivery Notes In]
      summary: Seznam dodacích listů přijatých
      description: |
        Stránkovaný seznam DL přijatých, od nejnovějšího `issue_date`. Rozsah jako na webu (firma,
        „jen vlastní záznamy“). Právo `read` k modulu DL přijaté; sync_token se odmítá.
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    JSON objekt, vše nepovinné:
                    - `date_from`, `date_to` (YYYY-MM-DD) – období podle `issue_date`, včetně krajních dnů
                    - `changed_since` (YYYY-MM-DD nebo YYYY-MM-DD HH:MM:SS)
                    - `status` – `all` (výchozí), `open`, `done`, `storno` (storno = příznak storno dokladu)
                    - `partner_id` – dodavatel, `storage_id` – sklad
                    - `search` – část čísla DL, čísla DL dodavatele, názvu nebo dodavatele
                    - `limit` (1–500, výchozí 50), `offset`
                  example: '{"status":"open","storage_id":2}'
      responses:
        '200':
          description: Stránka DL přijatých
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  total: { type: integer }
                  data: { type: array, items: { $ref: '#/components/schemas/DeliveryNoteInListItem' } }
        '400': { $ref: '#/components/responses/ReadBadRequest' }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /deliverynotein/get-one/{id}:
    post:
      tags: [Delivery Notes In]
      summary: Detail DL přijatého s rozpisem DPH, položkami a vratkami
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody: { $ref: '#/components/requestBodies/ReadOneRequest' }
      responses:
        '200':
          description: DL přijatý
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/DeliveryNoteInDetail' }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '404': { $ref: '#/components/responses/ReadNotFound' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /deliverynotein/get-statuses:
    post:
      tags: [Delivery Notes In]
      summary: Stavy DL přijatých firmy
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      requestBody: { $ref: '#/components/requestBodies/ReadOneRequest' }
      responses:
        '200':
          description: Stavy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: array, items: { $ref: '#/components/schemas/StatusListItem' } }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /deliverynotein/get-deleted:
    post:
      tags: [Delivery Notes In]
      summary: Smazané DL přijaté, položky a vratky od času since
      description: |
        Tabulky `cl_delivery_note_in`, `cl_delivery_note_in_items`, `cl_delivery_note_in_items_back`.
        Řádky smazané spolu s celým DL (kaskádou) se tu neobjeví – při `table = cl_delivery_note_in`
        zahoďte i jeho položky a vratky.
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      requestBody: { $ref: '#/components/requestBodies/ReadDeletedRequest' }
      responses:
        '200':
          description: Smazané záznamy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  total: { type: integer }
                  data: { type: array, items: { $ref: '#/components/schemas/DeletedRecord' } }
        '400': { $ref: '#/components/responses/ReadBadRequest' }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /sales/get-all:
    post:
      tags: [Sales]
      summary: Seznam prodejek
      description: |
        Stránkovaný seznam prodejek, od nejnovějšího `inv_date`. Rozsah jako na webu (firma,
        „jen vlastní záznamy“, pobočka uživatele). Právo `read` k modulu Prodejna; sync_token se odmítá.
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    JSON objekt, vše nepovinné:
                    - `date_from`, `date_to` (YYYY-MM-DD) – období podle `inv_date`, včetně krajních dnů
                    - `changed_since` (YYYY-MM-DD nebo YYYY-MM-DD HH:MM:SS)
                    - `type` – `all` (výchozí), `sale`, `correction`
                    - `status` – `all` (výchozí), `open`, `done`, `storno`
                    - `cash_id` – pokladna, `payment_type_id` – forma úhrady, `partner_id` – odběratel
                    - `search` – část čísla prodejky, VS nebo názvu odběratele
                    - `limit` (1–500, výchozí 50), `offset`
                  example: '{"date_from":"2026-10-01","cash_id":2}'
      responses:
        '200':
          description: Stránka prodejek
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  total: { type: integer }
                  data: { type: array, items: { $ref: '#/components/schemas/SaleListItem' } }
        '400': { $ref: '#/components/responses/ReadBadRequest' }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /sales/get-one/{id}:
    post:
      tags: [Sales]
      summary: Detail prodejky s rozpisem DPH, položkami a platbami
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody: { $ref: '#/components/requestBodies/ReadOneRequest' }
      responses:
        '200':
          description: Prodejka
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/SaleDetail' }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '404': { $ref: '#/components/responses/ReadNotFound' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /sales/get-statuses:
    post:
      tags: [Sales]
      summary: Stavy prodejek firmy
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      requestBody: { $ref: '#/components/requestBodies/ReadOneRequest' }
      responses:
        '200':
          description: Stavy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: array, items: { $ref: '#/components/schemas/StatusListItem' } }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /sales/get-deleted:
    post:
      tags: [Sales]
      summary: Smazané prodejky, položky a platby od času since
      description: |
        Tabulky `cl_sale`, `cl_sale_items`, `cl_sale_payments`. Řádky smazané spolu s celou prodejkou
        (kaskádou) se tu neobjeví – při `table = cl_sale` zahoďte i její položky a platby.
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      requestBody: { $ref: '#/components/requestBodies/ReadDeletedRequest' }
      responses:
        '200':
          description: Smazané záznamy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  total: { type: integer }
                  data: { type: array, items: { $ref: '#/components/schemas/DeletedRecord' } }
        '400': { $ref: '#/components/responses/ReadBadRequest' }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /purchaseorders/get-all:
    post:
      tags: [Purchase Orders]
      summary: Seznam objednávek u dodavatelů
      description: |
        Stránkovaný seznam objednávek, od nejnovějšího `od_date`. Rozsah jako na webu (firma,
        „jen vlastní záznamy“, pobočka uživatele). Právo `read` k modulu Objednávky; sync_token se odmítá.
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      requestBody:
        content:
          application/x-www-form-urlencoded:
            schema:
              type: object
              properties:
                api_token: { type: string }
                bearer_token: { type: string }
                dataJSON:
                  type: string
                  description: |
                    JSON objekt, vše nepovinné:
                    - `date_from`, `date_to` (YYYY-MM-DD) – období podle `od_date`, včetně krajních dnů
                    - `changed_since` (YYYY-MM-DD nebo YYYY-MM-DD HH:MM:SS)
                    - `status` – `all` (výchozí), `open` (nedodané), `delivered` (má datum dodání), `stocked` (naskladněné), `storno`
                    - `partner_id` – dodavatel
                    - `search` – část čísla, názvu objednávky nebo dodavatele
                    - `limit` (1–500, výchozí 50), `offset`
                  example: '{"status":"open"}'
      responses:
        '200':
          description: Stránka objednávek
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  total: { type: integer }
                  data: { type: array, items: { $ref: '#/components/schemas/PurchaseOrderListItem' } }
        '400': { $ref: '#/components/responses/ReadBadRequest' }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /purchaseorders/get-one/{id}:
    post:
      tags: [Purchase Orders]
      summary: Detail objednávky s položkami a stavem příjmu
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      parameters:
        - { name: id, in: path, required: true, schema: { type: integer } }
      requestBody: { $ref: '#/components/requestBodies/ReadOneRequest' }
      responses:
        '200':
          description: Objednávka
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { $ref: '#/components/schemas/PurchaseOrderDetail' }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '404': { $ref: '#/components/responses/ReadNotFound' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /purchaseorders/get-statuses:
    post:
      tags: [Purchase Orders]
      summary: Stavy objednávek firmy
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      requestBody: { $ref: '#/components/requestBodies/ReadOneRequest' }
      responses:
        '200':
          description: Stavy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  data: { type: array, items: { $ref: '#/components/schemas/StatusListItem' } }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '500': { $ref: '#/components/responses/ReadServerError' }

  /purchaseorders/get-deleted:
    post:
      tags: [Purchase Orders]
      summary: Smazané objednávky a položky od času since
      description: |
        Tabulky `cl_order`, `cl_order_items`. Položky smazané spolu s celou objednávkou (kaskádou)
        se tu neobjeví – při `table = cl_order` zahoďte i její položky.
      security: [ { bearerToken: [] }, { apiToken: [] } ]
      requestBody: { $ref: '#/components/requestBodies/ReadDeletedRequest' }
      responses:
        '200':
          description: Smazané záznamy
          content:
            application/json:
              schema:
                type: object
                properties:
                  status: { type: string, enum: [ok] }
                  total: { type: integer }
                  data: { type: array, items: { $ref: '#/components/schemas/DeletedRecord' } }
        '400': { $ref: '#/components/responses/ReadBadRequest' }
        '401': { $ref: '#/components/responses/ReadUnauthorized' }
        '403': { $ref: '#/components/responses/ReadForbidden' }
        '500': { $ref: '#/components/responses/ReadServerError' }
