MINOR (F2 review): the module docstring claimed FiscalResult/TributoLinha 'are NOT re-mirrored here' but the file defines FiscalResultPayload/ TributoLinhaPayload right below it. Corrects the prose to describe reality: they ARE re-mirrored, deliberately kept separate from the lib's own pydantic classes (this module is the wire contract the service owns), converted via emission.service._to_fiscal_result/_to_tributo. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
178 lines
6.5 KiB
Python
178 lines
6.5 KiB
Python
"""Task 5: `EmissaoRequest` -- the pydantic mirror of `sowai_fiscal.
|
|
xml_builder.DadosEmissao` (+ `sowai_fiscal.resolver.FiscalResult`/
|
|
`TributoLinha`, which the lib itself already defines as pydantic
|
|
`BaseModel`s -- FIX 6, F2 review: these two ARE re-mirrored here, as
|
|
`FiscalResultPayload`/`TributoLinhaPayload` below, kept SEPARATE from the
|
|
lib's own classes on purpose -- this module is the wire contract this
|
|
SERVICE owns (a field the lib adds/renames should never silently change
|
|
what a caller across the network is allowed to send), while the lib's
|
|
`FiscalResult`/`TributoLinha` are what `sowai_fiscal.xml_builder.build_nfe`
|
|
actually consumes internally. `emission.service._to_fiscal_result`/
|
|
`_to_tributo` convert Payload -> lib class (`FiscalResult(**payload.
|
|
model_dump())`, not the payload objects themselves), adapted per the porte
|
|
table and design spec:
|
|
|
|
* `tenant_ref`/`branch_ref` (opaque strings, decision #4) replace what in
|
|
the auto was implicit (`sale.branch_id`).
|
|
* `document_model`/`serie` select WHICH `FiscalSeries` to allocate from
|
|
(Task 4) -- `chave_acesso`/`numero`/`cnf`/`dh_emi` are DROPPED from this
|
|
payload entirely (unlike the lib's own `DadosEmissao`, which expects
|
|
them pre-computed): this SERVICE computes all four itself (`cNF do
|
|
serviço, persistido`, plan Task 5) -- a caller can never inject its own
|
|
chave/número, closing the exact class of bug the auto's own emission
|
|
code had to defend against for `branch_id` (C1, "auditoria Fable
|
|
2026-07-16", `auto/backend/app/modules/fiscal/emissao.py`'s module
|
|
docstring).
|
|
* `ver_proc` is REQUIRED (no default) -- design spec's "EMENDA F1": the
|
|
lib's own `DadosEmissao.ver_proc` defaults to `"sowai-auto/1b.1"`, a
|
|
default that made sense when this code lived IN the auto and is
|
|
actively WRONG for every other product calling this shared service. A
|
|
plain (no-default) pydantic field is already "required, 422 if absent"
|
|
-- no extra validator needed.
|
|
* `fiscal_result` per item is the motor de regras' OUTPUT (design spec
|
|
decision #3: "O serviço NÃO resolve imposto (FiscalResult vem no
|
|
payload)") -- this service treats it as opaque data to embed in the
|
|
XML, never recomputes it."""
|
|
import uuid
|
|
from datetime import datetime
|
|
from decimal import Decimal
|
|
from typing import Literal
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|
|
|
|
|
class TributoLinhaPayload(BaseModel):
|
|
tax_domain: str
|
|
cst: str | None = None
|
|
csosn: str | None = None
|
|
base_calc: Decimal
|
|
base_calc_percent: Decimal
|
|
aliquota: Decimal | None = None
|
|
valor: Decimal
|
|
mva: Decimal | None = None
|
|
aliquota_st: Decimal | None = None
|
|
fcp_percent: Decimal | None = None
|
|
codigo_beneficio: str | None = None
|
|
rule_id: uuid.UUID
|
|
|
|
|
|
class FiscalResultPayload(BaseModel):
|
|
cfop: str
|
|
cst: str | None = None
|
|
csosn: str | None = None
|
|
origem: str | None = None
|
|
consumidor_final: bool
|
|
indicador_ie: str
|
|
tributos: list[TributoLinhaPayload] = Field(default_factory=list)
|
|
|
|
|
|
class EmitenteDataPayload(BaseModel):
|
|
cnpj: str = Field(min_length=14, max_length=14)
|
|
razao_social: str
|
|
nome_fantasia: str | None = None
|
|
ie: str
|
|
crt: str = Field(min_length=1, max_length=1)
|
|
address_street: str
|
|
address_number: str
|
|
address_complement: str | None = None
|
|
address_district: str
|
|
address_city: str
|
|
address_state: str = Field(min_length=2, max_length=2)
|
|
address_zip: str
|
|
address_city_ibge_code: str
|
|
fone: str | None = None
|
|
|
|
|
|
class DestinatarioDataPayload(BaseModel):
|
|
"""`None` no `EmissaoRequest.destinatario` == consumidor final não
|
|
identificado -- espelha `sowai_fiscal.xml_builder.DestinatarioData`."""
|
|
|
|
nome: str
|
|
cnpj: str | None = None
|
|
cpf: str | None = None
|
|
indicador_ie: str
|
|
ie: str | None = None
|
|
address_street: str | None = None
|
|
address_number: str | None = None
|
|
address_complement: str | None = None
|
|
address_district: str | None = None
|
|
address_city: str | None = None
|
|
address_state: str | None = None
|
|
address_zip: str | None = None
|
|
address_city_ibge_code: str | None = None
|
|
email: str | None = None
|
|
|
|
|
|
class ItemDataPayload(BaseModel):
|
|
codigo: str
|
|
descricao: str
|
|
ncm: str
|
|
cfop: str
|
|
unidade_comercial: str
|
|
unidade_tributavel: str
|
|
quantidade: Decimal = Field(gt=0)
|
|
valor_unitario: Decimal = Field(gt=0)
|
|
fiscal_result: FiscalResultPayload
|
|
gtin: str | None = None
|
|
cest: str | None = None
|
|
peso_liquido_kg: Decimal | None = None
|
|
peso_bruto_kg: Decimal | None = None
|
|
|
|
|
|
class PagamentoDataPayload(BaseModel):
|
|
tpag: str
|
|
valor: Decimal = Field(gt=0)
|
|
indpag: str = "0"
|
|
|
|
|
|
class EmissaoRequest(BaseModel):
|
|
tenant_ref: str = Field(max_length=64)
|
|
branch_ref: str = Field(max_length=64)
|
|
# Only "55" for now -- see `series.schemas.FiscalSeriesCreate`'s
|
|
# docstring for why (`xml_builder.build_nfe` hardcodes `mod="55"`).
|
|
document_model: Literal["55"] = "55"
|
|
serie: int = Field(ge=0)
|
|
emitente: EmitenteDataPayload
|
|
itens: list[ItemDataPayload] = Field(min_length=1)
|
|
pagamento: PagamentoDataPayload
|
|
ambiente: Literal["homologacao", "producao"]
|
|
uf_destino_tipo: Literal["interna", "interestadual"]
|
|
destinatario: DestinatarioDataPayload | None = None
|
|
nat_op: str = "Venda"
|
|
tp_emis: str = Field(default="1", min_length=1, max_length=1)
|
|
ind_final: str = Field(default="1", min_length=1, max_length=1)
|
|
ind_pres: str = Field(default="1", min_length=1, max_length=1)
|
|
fin_nfe: str = Field(default="1", min_length=1, max_length=1)
|
|
# OBRIGATÓRIO -- ver o docstring do módulo (EMENDA F1). Nenhum default:
|
|
# ausente no payload -> 422 na borda do FastAPI, antes de qualquer
|
|
# lógica de negócio rodar.
|
|
ver_proc: str = Field(min_length=1, max_length=20)
|
|
|
|
|
|
class FiscalDocumentRead(BaseModel):
|
|
"""NUNCA inclui `xml_assinado` -- o XML sai só por `GET /v1/documentos/
|
|
{id}/xml` (`Response(media_type="application/xml")`), mesmo racional de
|
|
`FiscalCertificateRead` nunca incluir o binário do certificado. Sem
|
|
`sale_id`/`service_order_id` (porte table: este serviço não conhece o
|
|
domínio do produto chamador)."""
|
|
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: uuid.UUID
|
|
product_id: uuid.UUID
|
|
tenant_ref: str
|
|
branch_ref: str
|
|
series_id: uuid.UUID
|
|
document_model: str
|
|
serie: int
|
|
numero: int
|
|
chave_acesso: str
|
|
codigo_numerico: str
|
|
status: str
|
|
ambiente: str
|
|
rejeicao_codigo: str | None
|
|
rejeicao_motivo: str | None
|
|
protocolo: str | None
|
|
autorizada_em: datetime | None
|
|
created_at: datetime
|