This commit is contained in:
JiriUhlir
2026-06-18 15:59:55 +02:00
parent a5795fe344
commit e3dccc46d3
3 changed files with 656 additions and 113 deletions
+226 -82
View File
@@ -1,10 +1,33 @@
import logging
import os
from typing import Any
from typing import Any, Optional, Type
from fastapi import Body, Depends, FastAPI, Header, HTTPException, Query, Request
from pydantic import BaseModel, ConfigDict, Field, create_model
from app.models import CompanyData, CreateCompanyResponse
from app.models import (
BusinessCaseData,
CompanyData,
CreateResponse,
DetailResponse,
EmailData,
EventData,
InvoiceData,
LeadData,
LetterData,
ListResponse,
MeetingData,
OfferData,
PersonData,
PhoneCallData,
PriceListData,
ProductData,
ProjectData,
SalesOrderData,
SimpleResponse,
TaskData,
WebhookData,
)
from app.raynet_client import (
RaynetAuthError,
RaynetClient,
@@ -19,8 +42,7 @@ APP_NAME = os.getenv("APP_NAME", "raynet")
APP_VERSION = os.getenv("APP_VERSION", "1.0.0")
ROOT_PATH = os.getenv("ROOT_PATH", "")
# Popis se zobrazí nahoře ve Swaggeru (/docs) přihlašovací údaje jsou
# společné pro celé připojení a posílají se v hlavičkách u KAŽDÉHO requestu.
# Zobrazí se nahoře ve Swaggeru (/docs).
API_DESCRIPTION = """
Stateless proxy nad **RAYNET CRM API v2**.
@@ -29,19 +51,24 @@ Stateless proxy nad **RAYNET CRM API v2**.
Každý endpoint vyžaduje tři hlavičky. Jsou stejné pro všechna volání:
| Hlavička | Co to je | Kde to vzít |
|-------------------|--------------------|-------------|
| **`X-Api-Key`** | API klíč (secret) | V RAYNET CRM: **Nastavení → Klíč k API** (vygeneruj / zkopíruj). |
| **`X-Raynet-Email`** | Email uživatele | Přihlašovací email do RAYNET (tvoří dvojici s API klíčem pro Basic Auth). |
| **`X-Instance-Name`** | Název instance | Identifikátor tvé RAYNET instance (subdoména účtu). |
|-----------------------|--------------------|-------------|
| **`X-Api-Key`** | API klíč (secret) | V RAYNET CRM: **Nastavení → Klíč k API**. |
| **`X-Raynet-Email`** | Email uživatele | Přihlašovací email (tvoří s API klíčem Basic Auth). |
| **`X-Instance-Name`** | Název instance | Identifikátor RAYNET instance (subdoména účtu). |
`X-Api-Key` je **secret** nikdy se neloguje ani nevrací z endpointů.
## Endpointy
- Typované zkratky pro firmy: `POST/GET/PUT/DELETE /company...`
- Generický průchod na celé API: `/api/{resource}` podporuje libovolnou
RAYNET entitu (`company`, `person`, `lead`, `businessCase`, `activity`,
`product`, `offer`, `order`, `project`, ...).
Typované entity (vidíš strukturu objektu i odpovědi ve Swaggeru) pro každou
platí `POST` (vytvořit), `GET` (seznam), `GET /{id}` (detail), `PUT /{id}`
(úprava), `DELETE /{id}`:
`company`, `person`, `lead`, `businessCase`, `task`, `product`, `offer`,
`salesOrder`, `invoice`.
Generický průchod na **zbytek API** (např. `userAccount`, `project`,
`priceList`, `email`, `file`, číselníky): `/api/{resource}`.
Plná dokumentace RAYNET: <https://app.raynetcrm.com/api/doc/index-en.html>
"""
@@ -58,15 +85,9 @@ app = FastAPI(
# Sdílená credentials dependency 3 hlavičky pro celé připojení.
# --------------------------------------------------------------------------- #
def get_client(
x_api_key: str = Header(
..., alias="X-Api-Key", description="RAYNET API klíč (secret)."
),
x_raynet_email: str = Header(
..., alias="X-Raynet-Email", description="Email uživatele RAYNET."
),
x_instance_name: str = Header(
..., alias="X-Instance-Name", description="Název RAYNET instance."
),
x_api_key: str = Header(..., alias="X-Api-Key", description="RAYNET API klíč (secret)."),
x_raynet_email: str = Header(..., alias="X-Raynet-Email", description="Email uživatele RAYNET."),
x_instance_name: str = Header(..., alias="X-Instance-Name", description="Název RAYNET instance."),
):
"""Vytvoří RaynetClient z hlaviček a po dokončení requestu uvolní session."""
client = RaynetClient(
@@ -113,76 +134,138 @@ def version():
# --------------------------------------------------------------------------- #
# Typované zkratky pro firmy (company)
# Generátor typovaných CRUD endpointů pro entitu
# --------------------------------------------------------------------------- #
@app.post("/company", response_model=CreateCompanyResponse, tags=["company"])
def create_company(
company: CompanyData,
client: RaynetClient = Depends(get_client),
):
"""Vytvoří firmu v RAYNET CRM (validovaná data dle CompanyInsertDto)."""
data = company.model_dump(exclude_none=True)
result = _run(client.create_company, data)
return CreateCompanyResponse(id=result.get("id"), success=True, raw=result)
def _make_patch_model(model: Type[BaseModel]) -> Type[BaseModel]:
"""Z Insert modelu vyrobí model pro částečnou úpravu (vše volitelné)."""
fields = {
fname: (Optional[finfo.annotation], None)
for fname, finfo in model.model_fields.items()
}
return create_model(
f"{model.__name__}Patch",
__config__=ConfigDict(extra="allow"),
**fields,
)
@app.get("/company", tags=["company"])
def list_companies(
client: RaynetClient = Depends(get_client),
offset: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=1000),
fulltext: str | None = Query(None, description="Fulltextové hledání"),
):
"""Vrátí seznam firem s podporou stránkování a fulltextu."""
params = {"offset": offset, "limit": limit}
if fulltext:
params["fulltext"] = fulltext
return _run(client.list_companies, **params)
def register_crud(path: str, model: Type[BaseModel], tag: str) -> None:
"""Zaregistruje typované CRUD endpointy pro danou RAYNET entitu."""
patch_model = _make_patch_model(model)
@app.post(
f"/{path}",
response_model=CreateResponse,
tags=[tag],
operation_id=f"{path}_create",
summary=f"Vytvořit ({path})",
)
def _create(payload: model, client: RaynetClient = Depends(get_client)): # type: ignore[valid-type]
data = payload.model_dump(mode="json", exclude_none=True)
res = _run(client.create_record, path, data)
return CreateResponse(success=res.get("success", True), id=res.get("id"), data=res.get("data"))
@app.get("/company/{company_id}", tags=["company"])
def get_company(company_id: int, client: RaynetClient = Depends(get_client)):
"""Vrátí detail firmy."""
return _run(client.get_company, company_id)
@app.put("/company/{company_id}", tags=["company"])
def update_company(
company_id: int,
data: dict = Body(..., description="Pole firmy ke změně"),
client: RaynetClient = Depends(get_client),
):
"""Upraví firmu."""
return _run(client.update_company, company_id, data)
@app.delete("/company/{company_id}", tags=["company"])
def delete_company(company_id: int, client: RaynetClient = Depends(get_client)):
"""Smaže firmu."""
return _run(client.delete_company, company_id)
# --------------------------------------------------------------------------- #
# Generický průchod na CELÉ RAYNET API
# resource = libovolná entita: company, person, lead, businessCase, activity,
# product, offer, order, project, ...
# --------------------------------------------------------------------------- #
@app.get("/api/{resource}", tags=["generic"])
def api_list(
resource: str,
@app.get(
f"/{path}",
response_model=ListResponse,
tags=[tag],
operation_id=f"{path}_list",
summary=f"Seznam ({path})",
)
def _list(
request: Request,
client: RaynetClient = Depends(get_client),
):
"""Seznam záznamů entity. Všechny query parametry se předávají do RAYNET
(např. `offset`, `limit`, `fulltext`, `name`, ...)."""
offset: int = Query(0, ge=0, description="Posun ve výsledcích"),
limit: int = Query(50, ge=1, le=1000, description="Počet záznamů"),
fulltext: Optional[str] = Query(None, description="Fulltextové hledání"),
):
# Předáme všechny query parametry do RAYNET; doplníme výchozí offset/limit.
params = dict(request.query_params)
params.setdefault("offset", offset)
params.setdefault("limit", limit)
res = _run(client.list_records, path, params)
return ListResponse(
success=res.get("success", True),
totalCount=res.get("totalCount"),
data=res.get("data") or [],
)
@app.get(
f"/{path}/{{record_id}}",
response_model=DetailResponse,
tags=[tag],
operation_id=f"{path}_detail",
summary=f"Detail ({path})",
)
def _detail(record_id: int, client: RaynetClient = Depends(get_client)):
res = _run(client.get_record, path, record_id)
return DetailResponse(success=res.get("success", True), data=res.get("data"))
@app.put(
f"/{path}/{{record_id}}",
response_model=SimpleResponse,
tags=[tag],
operation_id=f"{path}_update",
summary=f"Upravit ({path})",
)
def _update(
record_id: int,
payload: patch_model, # type: ignore[valid-type]
client: RaynetClient = Depends(get_client),
):
data = payload.model_dump(mode="json", exclude_none=True)
res = _run(client.update_record, path, record_id, data)
return SimpleResponse(success=res.get("success", True))
@app.delete(
f"/{path}/{{record_id}}",
response_model=SimpleResponse,
tags=[tag],
operation_id=f"{path}_delete",
summary=f"Smazat ({path})",
)
def _delete(record_id: int, client: RaynetClient = Depends(get_client)):
res = _run(client.delete_record, path, record_id)
return SimpleResponse(success=res.get("success", True))
# Registrace typovaných entit (resource path = tag).
ENTITIES: list[tuple[str, Type[BaseModel]]] = [
("company", CompanyData),
("person", PersonData),
("lead", LeadData),
("businessCase", BusinessCaseData),
("offer", OfferData),
("salesOrder", SalesOrderData),
("invoice", InvoiceData),
("product", ProductData),
("priceList", PriceListData),
("project", ProjectData),
("task", TaskData),
("email", EmailData),
("event", EventData),
("meeting", MeetingData),
("phoneCall", PhoneCallData),
("letter", LetterData),
("webhook", WebhookData),
]
for _path, _model in ENTITIES:
register_crud(_path, _model, _path)
# --------------------------------------------------------------------------- #
# Generický průchod na zbytek RAYNET API (netypované entity)
# resource = libovolná entita: userAccount, project, priceList, email, file, ...
# --------------------------------------------------------------------------- #
@app.get("/api/{resource}", tags=["generic"])
def api_list(resource: str, request: Request, client: RaynetClient = Depends(get_client)):
"""Seznam záznamů entity. Query parametry se předávají do RAYNET."""
params = dict(request.query_params)
return _run(client.list_records, resource, params or None)
@app.get("/api/{resource}/{record_id}", tags=["generic"])
def api_detail(
resource: str, record_id: str, client: RaynetClient = Depends(get_client)
):
def api_detail(resource: str, record_id: str, client: RaynetClient = Depends(get_client)):
"""Detail jednoho záznamu."""
return _run(client.get_record, resource, record_id)
@@ -209,8 +292,69 @@ def api_update(
@app.delete("/api/{resource}/{record_id}", tags=["generic"])
def api_delete(
resource: str, record_id: str, client: RaynetClient = Depends(get_client)
):
def api_delete(resource: str, record_id: str, client: RaynetClient = Depends(get_client)):
"""Smaže záznam."""
return _run(client.delete_record, resource, record_id)
# --------------------------------------------------------------------------- #
# Vnořené zdroje a speciální akce
# Pokrývá např.: /company/{id}/address/, /company/{id}/lock,
# /company/{id}/merge/{srcId}/, /invoice/{id}/cancel, /invoice/{id}/pdfExport,
# /invoice/{id}/payment/, /offer/{id}/item/ atd.
# `sub` je zbytek cesty za /{resource}/{id}/ uveď přesně jak chce RAYNET
# (vč. koncového lomítka u kolekcí, např. "address/" nebo "payment/5/").
# --------------------------------------------------------------------------- #
@app.get("/api/{resource}/{record_id}/{sub:path}", tags=["generic"])
def api_sub_get(
resource: str, record_id: str, sub: str, request: Request,
client: RaynetClient = Depends(get_client),
):
"""GET vnořeného zdroje / akce (např. pdfExport, seznam adres)."""
return _run(client.call, "GET", f"/{resource}/{record_id}/{sub}", dict(request.query_params) or None)
@app.post("/api/{resource}/{record_id}/{sub:path}", tags=["generic"])
def api_sub_post(
resource: str, record_id: str, sub: str,
data: Optional[dict] = Body(None, description="Volitelné tělo (akce nemusí mít žádné)"),
client: RaynetClient = Depends(get_client),
):
"""POST vnořeného zdroje / akce (např. lock, cancel, setPrimary, update)."""
return _run(client.call, "POST", f"/{resource}/{record_id}/{sub}", None, data)
@app.put("/api/{resource}/{record_id}/{sub:path}", tags=["generic"])
def api_sub_put(
resource: str, record_id: str, sub: str,
data: Optional[dict] = Body(None, description="Tělo nového vnořeného záznamu"),
client: RaynetClient = Depends(get_client),
):
"""PUT vnořeného zdroje (create v kolekci, např. address/, payment/, item/)."""
return _run(client.call, "PUT", f"/{resource}/{record_id}/{sub}", None, data)
@app.delete("/api/{resource}/{record_id}/{sub:path}", tags=["generic"])
def api_sub_delete(
resource: str, record_id: str, sub: str,
client: RaynetClient = Depends(get_client),
):
"""DELETE vnořeného zdroje (např. address/5/, payment/3/)."""
return _run(client.call, "DELETE", f"/{resource}/{record_id}/{sub}")
# --------------------------------------------------------------------------- #
# Raw escape-hatch dosáhne na JAKOUKOLI cestu RAYNET API libovolnou metodou.
# Pro speciální případy mimo výše uvedené vzory (např. PUT /invoice/creditNote).
# --------------------------------------------------------------------------- #
class RawRequest(BaseModel):
method: str = Field(..., examples=["GET", "POST", "PUT", "DELETE"])
path: str = Field(..., description="Cesta za base URL, např. /invoice/creditNote", examples=["/company/"])
params: Optional[dict] = Field(default=None, description="Query parametry")
data: Optional[dict] = Field(default=None, description="JSON tělo")
@app.post("/raw", tags=["generic"], summary="Raw volání libovolného RAYNET endpointu")
def raw_call(req: RawRequest, client: RaynetClient = Depends(get_client)):
"""Univerzální volání pokrývá 100 % RAYNET API včetně netypických cest."""
return _run(client.call, req.method, req.path, req.params, req.data)
+360 -13
View File
@@ -1,7 +1,12 @@
"""Pydantic modely pro RAYNET endpointy.
Modely odpovídají RAYNET CRM API v2 (CompanyInsertDto). Slouží zároveň jako
zdroj pro Swagger/OpenAPI dokumentaci.
Modely odpovídají RAYNET CRM API v2 (Insert DTO jednotlivých entit) a slouží
zároveň jako zdroj pro Swagger/OpenAPI dokumentaci ve Swaggeru je tak vidět
přesná struktura objektů, povinná pole i tvar odpovědí.
POZN.: Názvy polí jsou v ``camelCase`` (tak je očekává RAYNET JSON tělo).
Nevyjmenovaná pole jsou povolená (``extra="allow"``), takže lze poslat i pole,
která zde nejsou explicitně uvedená.
"""
from enum import Enum
@@ -10,6 +15,9 @@ from typing import Optional
from pydantic import BaseModel, Field
# --------------------------------------------------------------------------- #
# Číselníkové enumy (company)
# --------------------------------------------------------------------------- #
class Rating(str, Enum):
A = "A"
B = "B"
@@ -35,6 +43,9 @@ class TaxPayer(str, Enum):
NO = "NO"
# --------------------------------------------------------------------------- #
# Sdílené pod-objekty
# --------------------------------------------------------------------------- #
class Address(BaseModel):
name: Optional[str] = Field(default=None, examples=["Sídlo klienta"])
street: Optional[str] = Field(default=None, examples=["Francouzská 6167/5"])
@@ -64,21 +75,21 @@ class CompanyAddress(BaseModel):
contactInfo: Optional[ContactInfo] = None
# --------------------------------------------------------------------------- #
# Entitní modely (request body pro create) CamelCase dle RAYNET API
# --------------------------------------------------------------------------- #
class CompanyData(BaseModel):
"""Tělo požadavku pro vytvoření firmy (CompanyInsertDto).
"""Firma / klient (RAYNET CompanyInsertDto). Povinná: name, rating, state, role."""
Povinná pole: ``name``, ``rating``, ``state``, ``role``.
"""
model_config = {"extra": "allow"}
# Povinná pole
name: str = Field(..., examples=["ACME s.r.o."])
rating: Rating = Field(..., examples=[Rating.A])
state: CompanyState = Field(..., examples=[CompanyState.A_POTENTIAL])
role: CompanyRole = Field(..., examples=[CompanyRole.A_SUBSCRIBER])
# Volitelná pole
regNumber: Optional[str] = Field(default=None, examples=["12345678"])
taxNumber: Optional[str] = Field(default=None, examples=["CZ12345678"])
regNumber: Optional[str] = Field(default=None, examples=["12345678"], description="IČO")
taxNumber: Optional[str] = Field(default=None, examples=["CZ12345678"], description="DIČ")
taxNumber2: Optional[str] = None
taxPayer: Optional[TaxPayer] = None
bankAccount: Optional[str] = None
@@ -95,11 +106,347 @@ class CompanyData(BaseModel):
tags: Optional[list[str]] = None
customFields: Optional[dict] = None
# Umožní i pole, která zde nejsou explicitně vyjmenovaná (RAYNET jich má víc).
class PersonData(BaseModel):
"""Kontaktní osoba (RAYNET PersonInsertDto). Povinné: lastName."""
model_config = {"extra": "allow"}
lastName: str = Field(..., description="Příjmení")
titleBefore: Optional[str] = None
firstName: Optional[str] = None
titleAfter: Optional[str] = None
securityLevel: Optional[int] = None
owner: Optional[int] = None
category: Optional[int] = None
personClassification1: Optional[int] = None
personClassification2: Optional[int] = None
personClassification3: Optional[int] = None
salutation: Optional[str] = None
birthday: Optional[str] = None
language: Optional[int] = None
maritalStatus: Optional[int] = None
gender: Optional[str] = None
contactInfo: Optional[dict] = None
socialNetworkContact: Optional[dict] = None
privateAddress: Optional[dict] = None
notice: Optional[str] = None
relationship: Optional[dict] = None
tags: Optional[list[str]] = None
keyman: Optional[bool] = None
originLead: Optional[int] = None
class CreateCompanyResponse(BaseModel):
id: Optional[int] = Field(default=None, description="ID nově vytvořené firmy")
class LeadData(BaseModel):
"""Lead / poptávka (RAYNET LeadInsertDto). Povinné: topic, priority."""
model_config = {"extra": "allow"}
topic: str = Field(..., description="Předmět")
priority: str = Field(..., description="Priorita (číselník)")
companyName: Optional[str] = None
regNumber: Optional[str] = None
firstName: Optional[str] = None
lastName: Optional[str] = None
titleBefore: Optional[str] = None
titleAfter: Optional[str] = None
securityLevel: Optional[int] = None
owner: Optional[int] = None
contactSource: Optional[int] = None
category: Optional[int] = None
notice: Optional[str] = None
leadPhase: Optional[int] = None
tags: Optional[str] = None
territory: Optional[int] = None
leadPerson: Optional[bool] = None
contactInfo: Optional[dict] = None
address: Optional[dict] = None
socialNetworkContact: Optional[dict] = None
customFields: Optional[dict] = None
notificationMessage: Optional[str] = None
notificationEmailAddresses: Optional[list[str]] = None
class BusinessCaseData(BaseModel):
"""Obchodní případ (RAYNET BusinessCaseInsertDto). Povinné: name, company."""
model_config = {"extra": "allow"}
name: str = Field(..., description="Předmět")
company: int = Field(..., description="ID klienta")
securityLevel: Optional[int] = None
owner: Optional[int] = None
person: Optional[int] = None
project: Optional[int] = None
totalAmount: Optional[float] = None
estimatedValue: Optional[float] = None
probability: Optional[int] = None
validFrom: Optional[str] = None
description: Optional[str] = None
currency: Optional[int] = None
exchangeRate: Optional[float] = None
category: Optional[int] = None
source: Optional[int] = None
businessCaseClassification1: Optional[int] = None
businessCaseClassification2: Optional[int] = None
businessCaseClassification3: Optional[int] = None
businessCasePhase: Optional[int] = None
originalLead: Optional[int] = None
tags: Optional[list[str]] = None
customFields: Optional[dict] = None
class TaskData(BaseModel):
"""Úkol = aktivita typu task (RAYNET TaskInsertDto, endpoint /task/).
Povinné: title, priority, owner, resolver, deadline.
"""
model_config = {"extra": "allow"}
title: str = Field(..., description="Předmět")
priority: str = Field(..., description="Priorita")
owner: int = Field(..., description="ID kontaktní osoby vlastník úkolu")
resolver: int = Field(..., description="ID kontaktní osoby řešitel úkolu")
deadline: str = Field(..., description="Deadline (datetime)")
category: Optional[int] = None
person: Optional[int] = None
company: Optional[int] = None
businessCase: Optional[int] = None
offer: Optional[int] = None
salesOrder: Optional[int] = None
project: Optional[int] = None
activity: Optional[int] = None
lead: Optional[int] = None
securityLevel: Optional[int] = None
scheduledFrom: Optional[str] = None
scheduledTill: Optional[str] = None
completed: Optional[str] = None
description: Optional[str] = None
solution: Optional[str] = None
tags: Optional[list[str]] = None
class ProductData(BaseModel):
"""Produkt (RAYNET ProductInsertDto). Povinné: code, name."""
model_config = {"extra": "allow"}
code: str = Field(..., description="Kód")
name: str = Field(..., description="Název")
unit: Optional[str] = None
description: Optional[str] = None
taxRate: Optional[float] = None
category: Optional[int] = Field(default=None, description="ID z číselníku ProductCategory")
productLine: Optional[int] = Field(default=None, description="ID z číselníku ProductLine")
cost: Optional[float] = None
price: Optional[float] = None
tags: Optional[list[str]] = None
class OfferData(BaseModel):
"""Nabídka (RAYNET OfferInsertDto). Povinné: name, company, businessCase."""
model_config = {"extra": "allow"}
name: str = Field(..., description="Předmět")
company: int = Field(..., description="ID klienta")
businessCase: int = Field(..., description="ID obchodního případu")
securityLevel: Optional[int] = None
owner: Optional[int] = None
person: Optional[int] = None
totalAmount: Optional[float] = None
estimatedValue: Optional[float] = None
validFrom: Optional[str] = None
validTill: Optional[str] = None
expirationDate: Optional[str] = None
description: Optional[str] = None
category: Optional[int] = None
offerStatus: Optional[int] = None
class SalesOrderData(BaseModel):
"""Objednávka (RAYNET SalesOrderInsertDto, endpoint /salesOrder/).
Povinné: name, company, businessCase.
"""
model_config = {"extra": "allow"}
name: str = Field(..., description="Předmět")
company: int = Field(..., description="ID klienta")
businessCase: int = Field(..., description="ID obchodního případu")
securityLevel: Optional[int] = None
owner: Optional[int] = None
person: Optional[int] = None
offer: Optional[int] = None
totalAmount: Optional[float] = None
estimatedValue: Optional[float] = None
validFrom: Optional[str] = None
validTill: Optional[str] = None
expirationDate: Optional[str] = None
requestDeliveryDate: Optional[str] = None
description: Optional[str] = None
category: Optional[int] = None
salesOrderStatus: Optional[int] = None
deliveryAddress: Optional[dict] = None
class InvoiceData(BaseModel):
"""Faktura (RAYNET InvoiceInsertDto, endpoint /invoice/).
Povinné: company, currency, dueDate, issueDate, invoiceType, paymentType,
taxableSupplyDate.
"""
model_config = {"extra": "allow"}
company: int = Field(..., description="ID klienta, kterému se fakturuje")
currency: int = Field(..., description="ID měny (číselník Currency)")
dueDate: str = Field(..., description="Datum splatnosti")
issueDate: str = Field(..., description="Datum vystavení")
invoiceType: str = Field(..., description="Typ faktury")
paymentType: str = Field(..., description="Způsob úhrady")
taxableSupplyDate: str = Field(..., description="Datum zdanitelného plnění")
securityLevel: Optional[int] = None
title: Optional[str] = None
constantSymbol: Optional[str] = None
specificSymbol: Optional[str] = None
paymentTermDays: Optional[int] = None
businessCase: Optional[int] = None
note: Optional[str] = None
deliveryNote: Optional[str] = None
privateNote: Optional[str] = None
class ProjectData(BaseModel):
"""Projekt (RAYNET ProjectInsertDto). Povinné: name, company."""
model_config = {"extra": "allow"}
name: str = Field(..., description="Předmět")
company: int = Field(..., description="ID klienta")
securityLevel: Optional[int] = None
owner: Optional[int] = None
person: Optional[int] = None
totalAmount: Optional[int] = None
avgValueTotalAmount: Optional[float] = None
minValueTotalAmount: Optional[float] = None
maxValueTotalAmount: Optional[float] = None
validFrom: Optional[str] = None
validTill: Optional[str] = None
scheduledEnd: Optional[str] = None
description: Optional[str] = None
category: Optional[int] = None
projectStatus: Optional[int] = None
tags: Optional[list[str]] = None
customFields: Optional[dict] = None
class PriceListData(BaseModel):
"""Ceník (RAYNET PriceListInsertDto). Povinné: name, code, currency, validFrom."""
model_config = {"extra": "allow"}
name: str = Field(..., description="Název ceníku")
code: str = Field(..., description="Kód ceníku")
currency: int = Field(..., description="ID měny (číselník Currency)")
validFrom: str = Field(..., description="Platnost od")
securityLevel: Optional[int] = None
owner: Optional[int] = None
category: Optional[int] = None
validTill: Optional[str] = None
description: Optional[str] = None
# --- Aktivity (sdílí strukturu: title, priority, owner povinné) ----------- #
class _ActivityBase(BaseModel):
"""Společná pole aktivit RAYNET (task/email/event/meeting/phoneCall/letter)."""
model_config = {"extra": "allow"}
title: str = Field(..., description="Předmět")
priority: str = Field(..., description="Priorita")
owner: int = Field(..., description="ID kontaktní osoby vlastník")
category: Optional[int] = None
person: Optional[int] = None
company: Optional[int] = None
businessCase: Optional[int] = None
offer: Optional[int] = None
salesOrder: Optional[int] = None
project: Optional[int] = None
activity: Optional[int] = None
lead: Optional[int] = None
securityLevel: Optional[int] = None
scheduledFrom: Optional[str] = None
scheduledTill: Optional[str] = None
completed: Optional[str] = None
description: Optional[str] = None
tags: Optional[list[str]] = None
class EmailData(_ActivityBase):
"""E-mailová aktivita (RAYNET EmailInsertDto)."""
class EventData(_ActivityBase):
"""Událost (RAYNET EventInsertDto)."""
class MeetingData(_ActivityBase):
"""Schůzka (RAYNET MeetingInsertDto)."""
solution: Optional[str] = None
class PhoneCallData(_ActivityBase):
"""Telefonát (RAYNET PhonecallInsertDto)."""
solution: Optional[str] = None
class LetterData(_ActivityBase):
"""Dopis (RAYNET LetterInsertDto)."""
class WebhookData(BaseModel):
"""Webhook (RAYNET WebhookInsertDto). Povinné: url, events."""
model_config = {"extra": "allow"}
url: str = Field(..., description="Cílová adresa (http/https)")
events: list[str] = Field(
...,
description="Typy událostí: record.created, record.updated, record.deleted",
)
secretToken: Optional[str] = Field(
default=None,
description="Token zaslaný v hlavičce X-RAYNETCRM-Token",
)
# --------------------------------------------------------------------------- #
# Obálky odpovědí (RAYNET vrací data zabalená do success/data/totalCount)
# --------------------------------------------------------------------------- #
class CreateResponse(BaseModel):
success: bool = True
id: Optional[int] = Field(default=None, description="ID nově vytvořeného záznamu")
data: Optional[dict] = Field(default=None, description="Surová data z RAYNET, pokud byla vrácena")
class DetailResponse(BaseModel):
success: bool = True
data: Optional[dict] = Field(
default=None,
description="Objekt záznamu vč. serverových polí (id, rowInfo, ...)",
)
class ListResponse(BaseModel):
success: bool = True
totalCount: Optional[int] = Field(default=None, description="Celkový počet záznamů")
data: list[dict] = Field(default_factory=list, description="Pole záznamů")
class SimpleResponse(BaseModel):
success: bool = True
raw: Optional[dict] = Field(default=None, description="Surová odpověď RAYNET API")
+71 -19
View File
@@ -36,17 +36,55 @@ Příklady entit: `company`, `person`, `lead`, `businessCase`, `activity`,
Veřejně přes AppFactory reverse proxy: `https://services.csbot.cz/apps/<app-id>/...`
### Typované zkratky pro firmy
### Typované entity
Pro každou z těchto entit existuje plné CRUD se **strukturou objektu i odpovědí
viditelnou ve Swaggeru** (request modely, povinná pole, validace):
| Cesta | Entita | Povinná pole |
|---------------------------|--------------------|--------------|
| `/company` | Firma / klient | `name`, `rating`, `state`, `role` |
| `/person` | Kontaktní osoba | `lastName` |
| `/lead` | Lead / poptávka | `topic`, `priority` |
| `/businessCase` | Obchodní případ | `name`, `company` |
| `/offer` | Nabídka | `name`, `company`, `businessCase` |
| `/salesOrder` | Objednávka | `name`, `company`, `businessCase` |
| `/invoice` | Faktura | `company`, `currency`, `dueDate`, `issueDate`, `invoiceType`, `paymentType`, `taxableSupplyDate` |
| `/product` | Produkt | `code`, `name` |
| `/priceList` | Ceník | `name`, `code`, `currency`, `validFrom` |
| `/project` | Projekt | `name`, `company` |
| `/task` | Úkol | `title`, `priority`, `owner`, `resolver`, `deadline` |
| `/email` | E-mail (aktivita) | `title`, `priority`, `owner` |
| `/event` | Událost | `title`, `priority`, `owner` |
| `/meeting` | Schůzka | `title`, `priority`, `owner` |
| `/phoneCall` | Telefonát | `title`, `priority`, `owner` |
| `/letter` | Dopis | `title`, `priority`, `owner` |
| `/webhook` | Webhook | `url`, `events` |
Operace u každé entity:
| Metoda | Cesta | Popis |
|--------|-------------------------|-------------------------------|
| POST | `/company` | Vytvoří firmu (validace) |
| GET | `/company` | Seznam firem (offset/limit/fulltext) |
| GET | `/company/{id}` | Detail firmy |
| PUT | `/company/{id}` | Úprava firmy |
| DELETE | `/company/{id}` | Smazání firmy |
|--------|--------------------|--------------------------------------|
| POST | `/{entita}` | Vytvoří záznam (validovaný objekt) |
| GET | `/{entita}` | Seznam (`offset`, `limit`, `fulltext` + libovolné RAYNET filtry) |
| GET | `/{entita}/{id}` | Detail |
| PUT | `/{entita}/{id}` | Částečná úprava (všechna pole volitelná) |
| DELETE | `/{entita}/{id}` | Smazání |
### Generický průchod na celé API
Enumy firmy: `rating` = `A`/`B`/`C`; `state` = `A_POTENTIAL`/`B_ACTUAL`/`C_DEFERRED`/`D_UNATTRACTIVE`;
`role` = `A_SUBSCRIBER`/`B_PARTNER`/`C_SUPPLIER`/`D_RIVAL`; `taxPayer` = `YES`/`NO`.
> Pole jsou v `camelCase` (tak je očekává RAYNET). Nevyjmenovaná pole jsou
> povolená (`extra="allow"`), takže lze poslat i pole, která model neuvádí.
### Generický průchod na zbytek API
Pro entity bez typovaného modelu zejména **uživatelé** (`userAccount`),
soubory (`file`), DMS dokumenty/složky, GDPR, hromadné e-maly a všechny
**číselníky** (`currency`, `legalForm`, `paymentTerm`, `taxRate`, `territory`,
`contactSource`, `companyCategory`, `productCategory`, `leadPhase`,
`businessCasePhase`, `offerStatus`, `salesOrderStatus`, `projectStatus`,
`maritalStatus`, `employeesNumber`, `economyActivity`, klasifikace 1/2/3, …):
| Metoda | Cesta | Mapuje se na RAYNET |
|--------|-----------------------------|-------------------------------|
@@ -56,19 +94,33 @@ Veřejně přes AppFactory reverse proxy: `https://services.csbot.cz/apps/<app-i
| PUT | `/api/{resource}/{id}` | `POST /{resource}/{id}/` (update) |
| DELETE | `/api/{resource}/{id}` | `DELETE /{resource}/{id}/` |
## Vytvoření firmy povinná pole
### Vnořené zdroje a speciální akce
| Pole | Typ | Hodnoty |
|----------|--------|------------------------------------------------------------|
| `name` | string | název firmy |
| `rating` | enum | `A`, `B`, `C` |
| `state` | enum | `A_POTENTIAL`, `B_ACTUAL`, `C_DEFERRED`, `D_UNATTRACTIVE` |
| `role` | enum | `A_SUBSCRIBER`, `B_PARTNER`, `C_SUPPLIER`, `D_RIVAL` |
Pro kolekce a akce nad záznamem (`sub` = zbytek cesty, přesně dle RAYNET vč.
koncového lomítka u kolekcí):
Volitelná: `regNumber` (IČO), `taxNumber` (DIČ), `taxPayer` (`YES`/`NO`),
`bankAccount`, `notice`, `owner`, `category`, `legalForm`, `paymentTerm`,
`turnover`, `territory`, `addresses`, `tags`, `customFields` a další dle
RAYNET `CompanyInsertDto`.
| Metoda | Cesta | Příklad cíle |
|--------|----------------------------------------|--------------|
| GET | `/api/{resource}/{id}/{sub}` | `/invoice/{id}/pdfExport`, `/company/{id}/relationship/` |
| POST | `/api/{resource}/{id}/{sub}` | `/invoice/{id}/cancel`, `/company/{id}/lock`, `/company/{id}/merge/{srcId}/` |
| PUT | `/api/{resource}/{id}/{sub}` | `/company/{id}/address/`, `/invoice/{id}/payment/`, `/offer/{id}/item/` |
| DELETE | `/api/{resource}/{id}/{sub}` | `/company/{id}/address/5/` |
### Raw 100 % pokrytí
Pro jakoukoli cestu / metodu (i netypické, např. `PUT /invoice/creditNote`):
```text
POST /raw
{ "method": "PUT", "path": "/invoice/creditNote", "params": {...}, "data": {...} }
```
### Tvar odpovědí
- Create → `{ "success": true, "id": 123, "data": {...} }`
- Detail → `{ "success": true, "data": { ...objekt vč. id, rowInfo } }`
- Seznam → `{ "success": true, "totalCount": N, "data": [ ... ] }`
- Update / Delete → `{ "success": true }`
### Příklad těla `POST /company`