diff --git a/app/database.py b/app/database.py index 4b34c60..55fb45e 100755 --- a/app/database.py +++ b/app/database.py @@ -3,16 +3,16 @@ from sqlalchemy.orm import sessionmaker, declarative_base from contextlib import asynccontextmanager from dotenv import load_dotenv import os - +from collections.abc import AsyncGenerator load_dotenv() DATABASE_URL = os.getenv("DATABASE_URL") -async_engine = create_async_engine(DATABASE_URL, echo=False, future=True) -AsyncSessionLocal = sessionmaker(bind=async_engine, class_=AsyncSession, expire_on_commit=False) +engine = create_async_engine(DATABASE_URL) +AsyncSessionLocal = sessionmaker(bind=engine, class_=AsyncSession, expire_on_commit=False) Base = declarative_base() -@asynccontextmanager -async def get_session(): +async def get_session() -> AsyncGenerator[AsyncSession, None]: async with AsyncSessionLocal() as session: yield session + diff --git a/app/main.py b/app/main.py index 393f33b..4df182b 100644 --- a/app/main.py +++ b/app/main.py @@ -1,11 +1,11 @@ +import asyncio import uuid -from fastapi import FastAPI, Request, UploadFile, File +from fastapi import FastAPI, HTTPException, Request, UploadFile, File from fastapi.templating import Jinja2Templates from fastapi.responses import HTMLResponse, JSONResponse from fastapi.staticfiles import StaticFiles import os, shutil from sqlalchemy import text -from fastapi import Depends from fastapi.responses import StreamingResponse from io import BytesIO import pandas as pd @@ -20,6 +20,12 @@ from app.processor import ( limpar_arquivos_processados ) from app.parametros import router as parametros_router +from fastapi.responses import FileResponse +from app.models import Fatura, SelicMensal, ParametrosFormula +from datetime import date +from app.utils import avaliar_formula + + app = FastAPI() templates = Jinja2Templates(directory="app/templates") @@ -61,16 +67,6 @@ def upload_page(request: Request): def relatorios_page(request: Request): return templates.TemplateResponse("relatorios.html", {"request": request}) -@app.get("/parametros", response_class=HTMLResponse) -async def parametros_page(request: Request): - async with AsyncSessionLocal() as session: - result = await session.execute(select(ParametrosFormula)) - parametros = result.scalars().first() - return templates.TemplateResponse("parametros.html", { - "request": request, - "parametros": parametros or {} - }) - @app.post("/upload-files") async def upload_files(files: list[UploadFile] = File(...)): for file in files: @@ -123,45 +119,115 @@ async def clear_all(): @app.get("/export-excel") async def export_excel(): async with AsyncSessionLocal() as session: - result = await session.execute(select(Fatura)) - faturas = result.scalars().all() + # 1. Coletar faturas e tabela SELIC + faturas_result = await session.execute(select(Fatura)) + faturas = faturas_result.scalars().all() + + selic_result = await session.execute(select(SelicMensal)) + selic_tabela = selic_result.scalars().all() + + # 2. Criar mapa {(ano, mes): percentual} + selic_map = {(s.ano, s.mes): float(s.percentual) for s in selic_tabela} + hoje = date.today() + + def calcular_fator_selic(ano_inicio, mes_inicio): + fator = 1.0 + ano, mes = ano_inicio, mes_inicio + while (ano < hoje.year) or (ano == hoje.year and mes <= hoje.month): + percentual = selic_map.get((ano, mes)) + if percentual: + fator *= (1 + percentual / 100) + mes += 1 + if mes > 12: + mes = 1 + ano += 1 + return fator + + # 3. Buscar fórmulas exatas por nome + formula_pis_result = await session.execute( + select(ParametrosFormula.formula).where( + ParametrosFormula.nome == "Cálculo PIS sobre ICMS", + ParametrosFormula.ativo == True + ).limit(1) + ) + formula_cofins_result = await session.execute( + select(ParametrosFormula.formula).where( + ParametrosFormula.nome == "Cálculo COFINS sobre ICMS", + ParametrosFormula.ativo == True + ).limit(1) + ) + + formula_pis = formula_pis_result.scalar_one_or_none() + formula_cofins = formula_cofins_result.scalar_one_or_none() + + # 4. Montar dados + mes_map = { + 'JAN': 1, 'FEV': 2, 'MAR': 3, 'ABR': 4, 'MAI': 5, 'JUN': 6, + 'JUL': 7, 'AGO': 8, 'SET': 9, 'OUT': 10, 'NOV': 11, 'DEZ': 12 + } dados = [] for f in faturas: - dados.append({ - "Nome": f.nome, - "UC": f.unidade_consumidora, - "Referência": f.referencia, - "Nota Fiscal": f.nota_fiscal, - "Valor Total": f.valor_total, - "ICMS (%)": f.icms_aliq, - "ICMS (R$)": f.icms_valor, - "Base ICMS": f.icms_base, - "PIS (%)": f.pis_aliq, - "PIS (R$)": f.pis_valor, - "Base PIS": f.pis_base, - "COFINS (%)": f.cofins_aliq, - "COFINS (R$)": f.cofins_valor, - "Base COFINS": f.cofins_base, - "Consumo (kWh)": f.consumo, - "Tarifa": f.tarifa, - "Cidade": f.cidade, - "Estado": f.estado, - "Distribuidora": f.distribuidora, - "Data Processamento": f.data_processamento, - }) + try: + if "/" in f.referencia: + mes_str, ano_str = f.referencia.split("/") + mes = mes_map.get(mes_str.strip().upper()) + ano = int(ano_str) + if not mes or not ano: + raise ValueError("Mês ou ano inválido") + else: + ano = int(f.referencia[:4]) + mes = int(f.referencia[4:]) + + fator = calcular_fator_selic(ano, mes) + periodo = f"{mes:02d}/{ano} à {hoje.month:02d}/{hoje.year}" + + contexto = f.__dict__ + valor_pis_icms = avaliar_formula(formula_pis, contexto) if formula_pis else None + valor_cofins_icms = avaliar_formula(formula_cofins, contexto) if formula_cofins else None + dados.append({ + "Nome": f.nome, + "UC": f.unidade_consumidora, + "Referência": f.referencia, + "Nota Fiscal": f.nota_fiscal, + "Valor Total": f.valor_total, + "ICMS (%)": f.icms_aliq, + "ICMS (R$)": f.icms_valor, + "Base ICMS": f.icms_base, + "PIS (%)": f.pis_aliq, + "PIS (R$)": f.pis_valor, + "Base PIS": f.pis_base, + "COFINS (%)": f.cofins_aliq, + "COFINS (R$)": f.cofins_valor, + "Base COFINS": f.cofins_base, + "Consumo (kWh)": f.consumo, + "Tarifa": f.tarifa, + "Cidade": f.cidade, + "Estado": f.estado, + "Distribuidora": f.distribuidora, + "Data Processamento": f.data_processamento, + "Fator SELIC acumulado": fator, + "Período SELIC usado": periodo, + "PIS sobre ICMS": valor_pis_icms, + "Valor Corrigido PIS (ICMS)": valor_pis_icms * fator if valor_pis_icms else None, + "COFINS sobre ICMS": valor_cofins_icms, + "Valor Corrigido COFINS (ICMS)": valor_cofins_icms * fator if valor_cofins_icms else None, + }) + except Exception as e: + print(f"Erro ao processar fatura {f.nota_fiscal}: {e}") df = pd.DataFrame(dados) + output = BytesIO() - df.to_excel(output, index=False, sheet_name="Faturas") + with pd.ExcelWriter(output, engine="xlsxwriter") as writer: + df.to_excel(writer, index=False, sheet_name="Faturas Corrigidas") + output.seek(0) - - return StreamingResponse( - output, - media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", - headers={"Content-Disposition": "attachment; filename=relatorio_faturas.xlsx"} - ) - + return StreamingResponse(output, media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", headers={ + "Content-Disposition": "attachment; filename=faturas_corrigidas.xlsx" + }) + + from app.parametros import router as parametros_router app.include_router(parametros_router) @@ -185,4 +251,34 @@ async def limpar_faturas(): if os.path.isfile(caminho): os.remove(caminho) - return {"message": "Faturas e arquivos apagados com sucesso."} \ No newline at end of file + return {"message": "Faturas e arquivos apagados com sucesso."} + +@app.get("/erros/download") +async def download_erros(): + zip_path = os.path.join("app", "uploads", "erros", "faturas_erro.zip") + if os.path.exists(zip_path): + response = FileResponse(zip_path, filename="faturas_erro.zip", media_type="application/zip") + # ⚠️ Agendar exclusão após resposta + asyncio.create_task(limpar_erros()) + return response + else: + raise HTTPException(status_code=404, detail="Arquivo de erro não encontrado.") + +@app.get("/erros/log") +async def download_log_erros(): + txt_path = os.path.join("app", "uploads", "erros", "erros.txt") + if os.path.exists(txt_path): + response = FileResponse(txt_path, filename="erros.txt", media_type="text/plain") + # ⚠️ Agendar exclusão após resposta + asyncio.create_task(limpar_erros()) + return response + else: + raise HTTPException(status_code=404, detail="Log de erro não encontrado.") + +async def limpar_erros(): + await asyncio.sleep(5) # Aguarda 5 segundos para garantir que o download inicie + pasta = os.path.join("app", "uploads", "erros") + for nome in ["faturas_erro.zip", "erros.txt"]: + caminho = os.path.join(pasta, nome) + if os.path.exists(caminho): + os.remove(caminho) diff --git a/app/models.py b/app/models.py index adb09e8..9feb83d 100755 --- a/app/models.py +++ b/app/models.py @@ -5,20 +5,17 @@ import uuid from datetime import datetime from app.database import Base from sqlalchemy import Boolean +from sqlalchemy import Column, Integer, String, Numeric class ParametrosFormula(Base): __tablename__ = "parametros_formula" __table_args__ = {"schema": "faturas"} - id = Column(Integer, primary_key=True, index=True) - tipo = Column(String(20)) + id = Column(Integer, primary_key=True) + nome = Column(String(50)) formula = Column(Text) - ativo = Column(Boolean) - aliquota_icms = Column(Float) - incluir_icms = Column(Integer) - incluir_pis = Column(Integer) - incluir_cofins = Column(Integer) + ativo = Column(Boolean, default=True) class Fatura(Base): __tablename__ = "faturas" @@ -74,13 +71,12 @@ class AliquotaUF(Base): id = Column(Integer, primary_key=True, autoincrement=True) uf = Column(String) exercicio = Column(String) - aliquota = Column(Float) + aliq_icms = Column(Numeric(6, 4)) class SelicMensal(Base): __tablename__ = "selic_mensal" __table_args__ = {'schema': 'faturas'} - id = Column(Integer, primary_key=True, autoincrement=True) - ano = Column(Integer) - mes = Column(Integer) - fator = Column(Float) + ano = Column(Integer, primary_key=True) + mes = Column(Integer, primary_key=True) + percentual = Column(Numeric(6, 4)) diff --git a/app/parametros.py b/app/parametros.py index 5e9420d..3f93418 100644 --- a/app/parametros.py +++ b/app/parametros.py @@ -1,6 +1,5 @@ # parametros.py -from fastapi import APIRouter, Request, HTTPException, Depends -from sqlalchemy.orm import Session +from fastapi import APIRouter, Request, Depends, Form from sqlalchemy.ext.asyncio import AsyncSession from app.database import get_session from app.models import AliquotaUF, ParametrosFormula, SelicMensal @@ -10,6 +9,19 @@ import datetime from fastapi.templating import Jinja2Templates from sqlalchemy.future import select from app.database import AsyncSessionLocal +from fastapi.responses import RedirectResponse +from app.models import Fatura +from fastapi import Body +from app.database import engine +import httpx +from app.models import SelicMensal +from sqlalchemy.dialects.postgresql import insert as pg_insert +import io +import csv +from fastapi.responses import StreamingResponse +import pandas as pd +from io import BytesIO + router = APIRouter() @@ -17,83 +29,202 @@ router = APIRouter() class AliquotaUFSchema(BaseModel): uf: str exercicio: int - aliquota: float + aliq_icms: float class Config: - orm_mode = True + from_attributes = True class ParametrosFormulaSchema(BaseModel): nome: str formula: str - campos: str + ativo: bool = True class Config: - orm_mode = True + from_attributes = True + class SelicMensalSchema(BaseModel): mes: str # 'YYYY-MM' fator: float class Config: - orm_mode = True + from_attributes = True # === Rotas === - -router = APIRouter() templates = Jinja2Templates(directory="app/templates") @router.get("/parametros") async def parametros_page(request: Request): async with AsyncSessionLocal() as session: - result = await session.execute(select(ParametrosFormula).where(ParametrosFormula.ativo == True)) - parametros = result.scalars().first() + # Consulta das fórmulas + result = await session.execute(select(ParametrosFormula)) + parametros = result.scalars().all() + + # Consulta da tabela selic_mensal + selic_result = await session.execute( + select(SelicMensal).order_by(SelicMensal.ano.desc(), SelicMensal.mes.desc()) + ) + selic_dados = selic_result.scalars().all() + + # Pega última data + ultima_data_selic = "-" + if selic_dados: + ultima = selic_dados[0] + ultima_data_selic = f"{ultima.mes:02d}/{ultima.ano}" + + # Campos numéricos da fatura + campos = [ + col.name for col in Fatura.__table__.columns + if col.type.__class__.__name__ in ['Integer', 'Float', 'Numeric'] + ] + return templates.TemplateResponse("parametros.html", { "request": request, - "parametros": parametros or {} + "lista_parametros": parametros, + "parametros": {}, + "campos_fatura": campos, + "selic_dados": selic_dados, + "ultima_data_selic": ultima_data_selic }) +@router.post("/parametros/editar/{param_id}") +async def editar_parametro(param_id: int, request: Request): + data = await request.json() + async with AsyncSessionLocal() as session: + param = await session.get(ParametrosFormula, param_id) + if param: + param.tipo = data.get("tipo", param.tipo) + param.formula = data.get("formula", param.formula) + await session.commit() + return {"success": True} + return {"success": False, "error": "Não encontrado"} + +@router.post("/parametros/testar") +async def testar_formula(db: AsyncSession = Depends(get_session), data: dict = Body(...)): + formula = data.get("formula") + + exemplo = await db.execute(select(Fatura).limit(1)) + fatura = exemplo.scalar_one_or_none() + if not fatura: + return {"success": False, "error": "Sem dados para teste."} + + try: + contexto = {col.name: getattr(fatura, col.name) for col in Fatura.__table__.columns} + resultado = eval(formula, {}, contexto) + return {"success": True, "resultado": resultado} + except Exception as e: + return {"success": False, "error": str(e)} + + @router.get("/parametros/aliquotas", response_model=List[AliquotaUFSchema]) -def listar_aliquotas(db: AsyncSession = Depends(get_session)): - return db.query(AliquotaUF).all() +async def listar_aliquotas(db: AsyncSession = Depends(get_session)): + result = await db.execute(select(AliquotaUF).order_by(AliquotaUF.uf, AliquotaUF.exercicio)) + return result.scalars().all() @router.post("/parametros/aliquotas") -def adicionar_aliquota(aliq: AliquotaUFSchema, db: AsyncSession = Depends(get_session)): - existente = db.query(AliquotaUF).filter_by(uf=aliq.uf, exercicio=aliq.exercicio).first() +async def adicionar_aliquota(aliq: AliquotaUFSchema, db: AsyncSession = Depends(get_session)): + result = await db.execute( + select(AliquotaUF).filter_by(uf=aliq.uf, exercicio=aliq.exercicio) + ) + existente = result.scalar_one_or_none() + if existente: - existente.aliquota = aliq.aliquota + existente.aliq_icms = aliq.aliq_icms # atualizado else: novo = AliquotaUF(**aliq.dict()) db.add(novo) - db.commit() - return {"status": "ok"} + + await db.commit() + return RedirectResponse(url="/parametros?ok=true&msg=Alíquota salva com sucesso", status_code=303) + @router.get("/parametros/formulas", response_model=List[ParametrosFormulaSchema]) -def listar_formulas(db: AsyncSession = Depends(get_session)): - return db.query(ParametrosFormula).all() +async def listar_formulas(db: AsyncSession = Depends(get_session)): + result = await db.execute(select(ParametrosFormula).order_by(ParametrosFormula.tipo)) + return result.scalars().all() @router.post("/parametros/formulas") -def salvar_formula(form: ParametrosFormulaSchema, db: AsyncSession = Depends(get_session)): - existente = db.query(ParametrosFormula).filter_by(nome=form.nome).first() +async def salvar_formula(form: ParametrosFormulaSchema, db: AsyncSession = Depends(get_session)): + result = await db.execute( + select(ParametrosFormula).filter_by(tipo=form.tipo) + ) + existente = result.scalar_one_or_none() + if existente: existente.formula = form.formula existente.campos = form.campos else: novo = ParametrosFormula(**form.dict()) db.add(novo) - db.commit() - return {"status": "ok"} + + await db.commit() + return RedirectResponse(url="/parametros?ok=true&msg=Parâmetro salvo com sucesso", status_code=303) @router.get("/parametros/selic", response_model=List[SelicMensalSchema]) -def listar_selic(db: AsyncSession = Depends(get_session)): - return db.query(SelicMensal).order_by(SelicMensal.mes.desc()).all() +async def listar_selic(db: AsyncSession = Depends(get_session)): + result = await db.execute(select(SelicMensal).order_by(SelicMensal.mes.desc())) + return result.scalars().all() + + +@router.post("/parametros/selic/importar") +async def importar_selic(request: Request, data_maxima: str = Form(None)): + try: + hoje = datetime.date.today() + inicio = datetime.date(hoje.year - 5, 1, 1) + fim = datetime.datetime.strptime(data_maxima, "%Y-%m-%d").date() if data_maxima else hoje + + url = ( + f"https://api.bcb.gov.br/dados/serie/bcdata.sgs.4390/dados?" + f"formato=json&dataInicial={inicio.strftime('%d/%m/%Y')}&dataFinal={fim.strftime('%d/%m/%Y')}" + ) + + async with httpx.AsyncClient() as client: + response = await client.get(url) + response.raise_for_status() + dados = response.json() + + registros = [] + for item in dados: + data = datetime.datetime.strptime(item['data'], "%d/%m/%Y") + ano, mes = data.year, data.month + percentual = float(item['valor'].replace(',', '.')) + registros.append({"ano": ano, "mes": mes, "percentual": percentual}) + + async with engine.begin() as conn: + stmt = pg_insert(SelicMensal.__table__).values(registros) + upsert_stmt = stmt.on_conflict_do_update( + index_elements=['ano', 'mes'], + set_={'percentual': stmt.excluded.percentual} + ) + await conn.execute(upsert_stmt) + + return RedirectResponse("/parametros?aba=selic", status_code=303) + + except Exception as e: + return RedirectResponse(f"/parametros?erro=1&msg={str(e)}", status_code=303) + +@router.get("/parametros/aliquotas/template") +def baixar_template_excel(): + df = pd.DataFrame(columns=["UF", "Exercício", "Alíquota"]) + df.loc[0] = ["SP", "2025", "18"] # exemplo opcional + df.loc[1] = ["MG", "2025", "12"] # exemplo opcional + + output = BytesIO() + with pd.ExcelWriter(output, engine='openpyxl') as writer: + df.to_excel(writer, sheet_name='Template', index=False) + + # Adiciona instrução como observação na célula A5 (linha 5) + sheet = writer.sheets['Template'] + sheet.cell(row=5, column=1).value = ( + "⚠️ Após preencher, salve como CSV (.csv separado por vírgulas) para importar no sistema." + ) + + output.seek(0) + return StreamingResponse( + output, + media_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + headers={"Content-Disposition": "attachment; filename=template_aliquotas.xlsx"} + ) + + -@router.post("/parametros/selic") -def salvar_selic(selic: SelicMensalSchema, db: AsyncSession = Depends(get_session)): - existente = db.query(SelicMensal).filter_by(mes=selic.mes).first() - if existente: - existente.fator = selic.fator - else: - novo = SelicMensal(**selic.dict()) - db.add(novo) - db.commit() - return {"status": "ok"} diff --git a/app/processor.py b/app/processor.py index 27a20d0..885350b 100644 --- a/app/processor.py +++ b/app/processor.py @@ -2,6 +2,7 @@ import logging import os import shutil import asyncio +import httpx from sqlalchemy.future import select from app.utils import extrair_dados_pdf from app.database import AsyncSessionLocal @@ -9,6 +10,9 @@ from app.models import Fatura, LogProcessamento import time import traceback import uuid +from app.models import SelicMensal +from sqlalchemy import select +from zipfile import ZipFile logger = logging.getLogger(__name__) @@ -28,6 +32,9 @@ def remover_arquivo_temp(caminho_pdf): logger.warning(f"Falha ao remover arquivo temporário: {e}") def salvar_em_uploads(caminho_pdf_temp, nome_original, nota_fiscal): + ERROS_DIR = os.path.join("app", "uploads", "erros") + os.makedirs(ERROS_DIR, exist_ok=True) + erros_detectados = [] try: extensao = os.path.splitext(nome_original)[1].lower() nome_destino = f"{nota_fiscal}_{uuid.uuid4().hex[:6]}{extensao}" @@ -35,6 +42,17 @@ def salvar_em_uploads(caminho_pdf_temp, nome_original, nota_fiscal): shutil.copy2(caminho_pdf_temp, destino_final) return destino_final except Exception as e: + # Copiar o arquivo com erro + extensao = os.path.splitext(nome_original)[1].lower() + nome_arquivo = f"{uuid.uuid4().hex[:6]}_erro{extensao}" + caminho_pdf = caminho_pdf_temp + + shutil.copy2(caminho_pdf, os.path.join(ERROS_DIR, nome_arquivo)) + + mensagem = f"{nome_arquivo}: {str(e)}" + + erros_detectados.append(mensagem) + logger.error(f"Erro ao salvar em uploads: {e}") return caminho_pdf_temp @@ -61,6 +79,10 @@ async def process_single_file(caminho_pdf_temp: str, nome_original: str): "dados": dados, "tempo": f"{duracao}s" } + + data_comp = dados.get("competencia") + if data_comp: + await garantir_selic_para_competencia(session, data_comp.year, data_comp.month) # Salva arquivo final caminho_final = salvar_em_uploads(caminho_pdf_temp, nome_original, dados['nota_fiscal']) @@ -97,7 +119,6 @@ async def process_single_file(caminho_pdf_temp: str, nome_original: str): "trace": erro_str } - async def processar_em_lote(): import traceback # para exibir erros resultados = [] @@ -128,9 +149,49 @@ async def processar_em_lote(): }) print(f"Erro ao processar {item['nome_original']}: {e}") print(traceback.format_exc()) + # Após o loop, salvar TXT com erros + erros_txt = [] + for nome, status in status_arquivos.items(): + if status['status'] == 'Erro': + erros_txt.append(f"{nome} - {status.get('mensagem', 'Erro desconhecido')}") + + if erros_txt: + with open(os.path.join(UPLOADS_DIR, "erros", "erros.txt"), "w", encoding="utf-8") as f: + f.write("\n".join(erros_txt)) + + # Compacta PDFs com erro + with ZipFile(os.path.join(UPLOADS_DIR, "erros", "faturas_erro.zip"), "w") as zipf: + for nome in status_arquivos: + if status_arquivos[nome]['status'] == 'Erro': + caminho = os.path.join(UPLOADS_DIR, "temp", nome) + if os.path.exists(caminho): + zipf.write(caminho, arcname=nome) return resultados def limpar_arquivos_processados(): status_arquivos.clear() while not fila_processamento.empty(): fila_processamento.get_nowait() + +async def garantir_selic_para_competencia(session, ano, mes): + # Verifica se já existe + result = await session.execute(select(SelicMensal).filter_by(ano=ano, mes=mes)) + existente = result.scalar_one_or_none() + if existente: + return # já tem + + # Busca na API do Banco Central + url = ( + f"https://api.bcb.gov.br/dados/serie/bcdata.sgs.4390/dados?" + f"formato=json&dataInicial=01/{mes:02d}/{ano}&dataFinal=30/{mes:02d}/{ano}" + ) + async with httpx.AsyncClient() as client: + resp = await client.get(url) + resp.raise_for_status() + dados = resp.json() + + if dados: + percentual = float(dados[0]["valor"].replace(",", ".")) + novo = SelicMensal(ano=ano, mes=mes, fator=percentual) + session.add(novo) + await session.commit() \ No newline at end of file diff --git a/app/templates/parametros.html b/app/templates/parametros.html index 4215bf7..940f472 100755 --- a/app/templates/parametros.html +++ b/app/templates/parametros.html @@ -9,15 +9,16 @@