feat(backend): add FastAPI backend — procurement, inventory domain, Casdoor auth, PM arrival endpoints
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
"""Invoice / proforma attachments — metadata in `quote_attachments`, bytes in MinIO.
|
||||
|
||||
Multi-file contract: a quote may hold any number of attachments per kind
|
||||
(invoice / proforma). Every upload creates a new DB row and a new object
|
||||
(object key `{quote_id}/{kind}/{attachment_id}` inside the MinIO bucket).
|
||||
"""
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.files import QuoteAttachment
|
||||
from app.services import storage
|
||||
from app.utils.errors import NotFoundError
|
||||
|
||||
|
||||
def _object_key(quote_id: uuid.UUID, kind: str, attachment_id: uuid.UUID) -> str:
|
||||
return f"{quote_id}/{kind}/{attachment_id}"
|
||||
|
||||
|
||||
async def create(
|
||||
db: AsyncSession,
|
||||
quote_id: uuid.UUID,
|
||||
kind: str,
|
||||
file_name: str,
|
||||
content: bytes,
|
||||
mime_type: str,
|
||||
) -> QuoteAttachment:
|
||||
"""Upload the bytes to MinIO, then persist the metadata row."""
|
||||
attachment_id = uuid.uuid4()
|
||||
object_key = _object_key(quote_id, kind, attachment_id)
|
||||
await storage.put_object(object_key, content, mime_type)
|
||||
attachment = QuoteAttachment(
|
||||
id=attachment_id,
|
||||
quote_id=quote_id,
|
||||
kind=kind,
|
||||
file_name=file_name,
|
||||
mime_type=mime_type,
|
||||
size_bytes=len(content),
|
||||
object_key=object_key,
|
||||
)
|
||||
db.add(attachment)
|
||||
try:
|
||||
await db.flush()
|
||||
except Exception:
|
||||
# Metadata insert failed — best-effort cleanup of the orphan object.
|
||||
await storage.remove_object(object_key)
|
||||
raise
|
||||
return attachment
|
||||
|
||||
|
||||
async def list_for(db: AsyncSession, quote_id: uuid.UUID, kind: str) -> list[QuoteAttachment]:
|
||||
result = await db.execute(
|
||||
select(QuoteAttachment)
|
||||
.where(QuoteAttachment.quote_id == quote_id, QuoteAttachment.kind == kind)
|
||||
.order_by(QuoteAttachment.uploaded_at)
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
|
||||
|
||||
async def get(db: AsyncSession, attachment_id: uuid.UUID) -> QuoteAttachment:
|
||||
attachment = await db.get(QuoteAttachment, attachment_id)
|
||||
if attachment is None:
|
||||
raise NotFoundError("فایل پیوست یافت نشد")
|
||||
return attachment
|
||||
|
||||
|
||||
async def read_bytes(attachment: QuoteAttachment) -> bytes:
|
||||
"""Stream the object bytes from MinIO."""
|
||||
return await storage.get_object_bytes(attachment.object_key)
|
||||
|
||||
|
||||
async def remove(db: AsyncSession, attachment: QuoteAttachment) -> None:
|
||||
"""Delete the object from MinIO (best-effort) and the metadata row."""
|
||||
await storage.remove_object(attachment.object_key)
|
||||
await db.delete(attachment)
|
||||
Reference in New Issue
Block a user