diff --git a/models/mixins.py b/models/mixins.py index 9d74d57..6e434a3 100644 --- a/models/mixins.py +++ b/models/mixins.py @@ -23,6 +23,14 @@ class CreatedAtMixin: ) +class LastModifiedAtMixin: + last_modified_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), + default=lambda: datetime.now(timezone.utc), + nullable=False, + ) + + class PriceMixin: price: Mapped[float] = mapped_column(Numeric(12, 2), comment="Стоимость") diff --git a/modules/clients/__init__.py b/modules/clients/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/clients/models/__init__.py b/modules/clients/models/__init__.py new file mode 100644 index 0000000..500fdd9 --- /dev/null +++ b/modules/clients/models/__init__.py @@ -0,0 +1 @@ +from .client import Client as Client, ClientDetails as ClientDetails diff --git a/modules/clients/models/client.py b/modules/clients/models/client.py new file mode 100644 index 0000000..032b0ef --- /dev/null +++ b/modules/clients/models/client.py @@ -0,0 +1,51 @@ +from datetime import datetime +from typing import Optional, TYPE_CHECKING + +from sqlalchemy import ForeignKey +from sqlalchemy.orm import relationship, Mapped, mapped_column + +from models.base import BaseModel +from models.mixins import IdMixin, CreatedAtMixin, SoftDeleteMixin, LastModifiedAtMixin + +if TYPE_CHECKING: + from models import Product + + +class Client(BaseModel, IdMixin, CreatedAtMixin, SoftDeleteMixin): + __tablename__ = "clients" + + name: Mapped[str] = mapped_column(unique=True, comment="Название клиента") + + company_name: Mapped[str] = mapped_column( + nullable=False, + comment="Название компании", + ) + + products: Mapped[list["Product"]] = relationship( + back_populates="client", lazy="noload" + ) + + details: Mapped["ClientDetails"] = relationship( + uselist=False, + back_populates="client", + cascade="all, delete", + lazy="joined", + ) + + comment: Mapped[Optional[str]] = mapped_column(comment="Комментарий") + + +class ClientDetails(BaseModel, IdMixin, LastModifiedAtMixin): + __tablename__ = "client_details" + + client_id: Mapped[int] = mapped_column( + ForeignKey("clients.id"), unique=True, comment="ID клиента" + ) + client: Mapped[Client] = relationship( + back_populates="details", cascade="all, delete", uselist=False + ) + + telegram: Mapped[Optional[str]] = mapped_column() + phone_number: Mapped[Optional[str]] = mapped_column() + inn: Mapped[Optional[str]] = mapped_column() + email: Mapped[Optional[str]] = mapped_column() diff --git a/modules/clients/repositories/__init__.py b/modules/clients/repositories/__init__.py new file mode 100644 index 0000000..c257e18 --- /dev/null +++ b/modules/clients/repositories/__init__.py @@ -0,0 +1 @@ +from .client import ClientRepository as ClientRepository diff --git a/modules/clients/repositories/client.py b/modules/clients/repositories/client.py new file mode 100644 index 0000000..4228470 --- /dev/null +++ b/modules/clients/repositories/client.py @@ -0,0 +1,47 @@ +from datetime import timezone, datetime + +from sqlalchemy import update + +from modules.clients.models import Client, ClientDetails +from modules.clients.schemas.client import UpdateClientSchema, CreateClientSchema +from repositories.mixins import * + + +class ClientRepository( + BaseRepository, + RepGetAllMixin[Client], + RepDeleteMixin[Client], + RepUpdateMixin[Client, UpdateClientSchema], + RepGetByIdMixin[Client], +): + entity_class = Client + entity_not_found_msg = "Клиент не найден" + + def _process_get_all_stmt(self, stmt: Select) -> Select: + return stmt.where(Client.is_deleted.is_(False)).order_by(Client.created_at) + + async def create(self, data: CreateClientSchema) -> int: + details = ClientDetails(**data.details.model_dump()) + data_dict = data.model_dump() + data_dict["details"] = details + + client = Client(**data_dict) + self.session.add(client) + await self.session.commit() + await self.session.refresh(client) + return client.id + + async def update(self, client: Client, data: UpdateClientSchema) -> Client: + if data.details is not None: + stmt = ( + update(ClientDetails) + .where(ClientDetails.client_id == client.id) + .values( + **data.details.model_dump(), + last_modified_at=datetime.now(timezone.utc), + ) + ) + await self.session.execute(stmt) + del data.details + + return await self._apply_update_data_to_model(client, data, True) diff --git a/modules/clients/routers/__init__.py b/modules/clients/routers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/modules/clients/routers/client.py b/modules/clients/routers/client.py new file mode 100644 index 0000000..ce66565 --- /dev/null +++ b/modules/clients/routers/client.py @@ -0,0 +1,55 @@ +from fastapi import APIRouter, Path + +from backend.dependecies import SessionDependency +from modules.clients.schemas.client import * +from modules.clients.services import ClientService + +router = APIRouter(tags=["client"]) + + +@router.get( + "/", + response_model=GetClientsResponse, + operation_id="get_clients", +) +async def get_clients( + session: SessionDependency, +): + return await ClientService(session).get_all() + + +@router.post( + "/", + response_model=CreateClientResponse, + operation_id="create_client", +) +async def create_client( + session: SessionDependency, + request: CreateClientRequest, +): + return await ClientService(session).create(request) + + +@router.patch( + "/{pk}", + response_model=UpdateClientResponse, + operation_id="update_client", +) +async def update_client( + session: SessionDependency, + request: UpdateClientRequest, + pk: int = Path(), +): + return await ClientService(session).update(pk, request) + + +@router.delete( + "/{pk}", + response_model=DeleteClientResponse, + operation_id="delete_client", +) +async def delete_product( + session: SessionDependency, + pk: int = Path(), +): + return await ClientService(session).delete(pk) diff --git a/modules/clients/schemas/client.py b/modules/clients/schemas/client.py new file mode 100644 index 0000000..054e2a0 --- /dev/null +++ b/modules/clients/schemas/client.py @@ -0,0 +1,74 @@ +from typing import Optional + +from schemas.base import BaseSchema, BaseResponse + + +# region Entities + + +class ClientDetailsSchema(BaseSchema): + telegram: str + phone_number: str + inn: str + email: str + + +class CreateClientSchema(BaseSchema): + name: str + company_name: str + comment: Optional[str] = "" + details: ClientDetailsSchema + + +class ClientSchema(CreateClientSchema): + id: int + + +class UpdateClientSchema(BaseSchema): + name: Optional[str] = None + company_name: Optional[str] = None + comment: Optional[str] = None + details: Optional[ClientDetailsSchema] = None + + +# endregion + +# region Requests + + +class ClientUpdateDetailsRequest(BaseSchema): + client_id: int + details: ClientDetailsSchema + + +class CreateClientRequest(BaseSchema): + entity: CreateClientSchema + + +class UpdateClientRequest(BaseSchema): + entity: UpdateClientSchema + + +# endregion + + +# region Responses + + +class GetClientsResponse(BaseSchema): + items: list[ClientSchema] + + +class UpdateClientResponse(BaseResponse): + pass + + +class CreateClientResponse(BaseResponse): + pass + + +class DeleteClientResponse(BaseResponse): + pass + + +# endregion diff --git a/modules/clients/services/__init__.py b/modules/clients/services/__init__.py new file mode 100644 index 0000000..a93805b --- /dev/null +++ b/modules/clients/services/__init__.py @@ -0,0 +1 @@ +from .client import ClientService as ClientService diff --git a/modules/clients/services/client.py b/modules/clients/services/client.py new file mode 100644 index 0000000..1d46248 --- /dev/null +++ b/modules/clients/services/client.py @@ -0,0 +1,26 @@ +from modules.clients.models import Client +from modules.clients.repositories import ClientRepository +from modules.clients.schemas.client import ( + ClientSchema, + CreateClientRequest, + UpdateClientRequest, +) +from services.mixins import * + + +class ClientService( + ServiceGetAllMixin[Client, ClientSchema], + ServiceCreateMixin[Client, CreateClientRequest, ClientSchema], + ServiceUpdateMixin[Client, UpdateClientRequest], + ServiceDeleteMixin[Client], +): + schema_class = ClientSchema + entity_deleted_msg = "Клиент успешно удален" + entity_updated_msg = "Клиент успешно обновлен" + entity_created_msg = "Клиент успешно создан" + + def __init__(self, session: AsyncSession): + self.repository = ClientRepository(session) + + async def is_soft_delete(self, client: ClientSchema) -> bool: + return True diff --git a/modules/fulfillment_base/models/product.py b/modules/fulfillment_base/models/product.py index 9ac2e95..eb4fa8a 100644 --- a/modules/fulfillment_base/models/product.py +++ b/modules/fulfillment_base/models/product.py @@ -5,6 +5,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship from models.base import BaseModel from models.mixins import IdMixin, SoftDeleteMixin +from modules.clients.models import Client from modules.fulfillment_base.models import ( ProductBarcode, BarcodeTemplate, @@ -28,6 +29,9 @@ class Product(BaseModel, IdMixin, SoftDeleteMixin): default="", comment="Дополнительная информация" ) + client_id: Mapped[int] = mapped_column(ForeignKey("clients.id")) + client: Mapped["Client"] = relationship(back_populates="products") + images: Mapped[list["ProductImage"]] = relationship( "ProductImage", back_populates="product",