Compare commits

...

55 Commits

Author SHA1 Message Date
34ac2a0a69 fix: using uv 2025-10-19 21:44:02 +04:00
79a1dff720 refactor: refactored Board model 2025-10-19 12:55:29 +04:00
44f00b1057 feat: default name for groups 2025-10-19 12:22:47 +04:00
ffee658349 feat: deal tags 2025-10-19 12:12:08 +04:00
d7c7d1775f feat: routers client and version prefixes 2025-10-17 21:40:54 +04:00
6b1b4109c6 feat: groups 2025-10-17 19:48:51 +04:00
35869e2ea5 feat: marketplaces endpoints 2025-10-13 12:48:06 +04:00
d8eba188c9 feat: products quantity and total price in deal schemas 2025-10-11 16:22:38 +04:00
636821e74a feat: statuses colors 2025-10-11 12:15:17 +04:00
fbb0c72bce refactor: removed nullable in models 2025-10-11 10:34:05 +04:00
bd4f4138be fix: fixed get deal product by id 2025-10-10 23:01:10 +04:00
4c871e1e1b feat: barcode printing 2025-10-10 23:00:39 +04:00
6b0f8a1aa5 feat: product endpoints changes for products table 2025-10-08 22:30:43 +04:00
7d6155ff6c feat: client endpoints fixes for client tab in deal editor 2025-10-05 12:05:03 +04:00
986712d5b7 feat: client endpoints for clients page 2025-10-04 18:12:13 +04:00
66b50fb951 feat: barcode templates 2025-10-04 10:13:24 +04:00
9c9b3f4706 fix: service schemas fixed 2025-10-03 09:06:01 +04:00
c2594f9d55 fix: service update fixed 2025-09-28 12:45:19 +04:00
fbadddeada feat: service categories endpoints and service creation endpoint 2025-09-27 18:21:20 +04:00
8cf589c54e fix: services kit create and update fix 2025-09-25 09:43:03 +04:00
22b8428035 feat: a few tabs for module 2025-09-21 09:48:55 +04:00
6b3d124adf feat: deal status history table 2025-09-20 10:07:56 +04:00
44f315b4a0 fix: moved add_services_kit repo method 2025-09-19 18:20:32 +04:00
1df57c69c1 fix: cascade delete for deal products and product services 2025-09-19 17:11:34 +04:00
7eeb24f8ff fix: default values for product 2025-09-18 20:12:31 +04:00
8794241541 feat: module dependencies 2025-09-18 17:54:30 +04:00
1a9dbd857a feat: adding services kit to deal 2025-09-16 18:13:19 +04:00
98d3026e0d refactor: entity not found exceptions handler 2025-09-16 16:56:10 +04:00
276626d6f7 feat: modules, products, services, services kits 2025-09-16 10:54:10 +04:00
be8052848c refactor: crud mixins for repositories and services 2025-09-08 18:00:34 +04:00
d73748deab refactor: mixins for services 2025-09-08 10:59:06 +04:00
67634836dc refactor: update repository mixin 2025-09-07 21:28:06 +04:00
7a76da4058 fix: project_id in board schema and fixed get mixin 2025-09-06 11:28:48 +04:00
7990e7d460 refactor: repository get all mixin 2025-09-05 11:13:49 +04:00
c1d3ac98f0 refactor: repository get by id mixin 2025-09-05 09:53:16 +04:00
e5be35be35 refactor: repository create mixin 2025-09-05 00:04:09 +04:00
c632fb8037 refactor: repository delete mixin 2025-09-04 20:53:44 +04:00
fbab70d6c1 fix: only soft delete for deals 2025-09-02 18:02:11 +04:00
404a58735d feat: deal's status and board update 2025-09-02 14:42:11 +04:00
de5ffed7de Revert "feat: status and board in deal schema"
This reverts commit b9ae3bc18a.
2025-09-01 20:39:20 +04:00
b9ae3bc18a feat: status and board in deal schema 2025-09-01 20:33:40 +04:00
57c3ada2fa feat: deals filters 2025-09-01 17:54:45 +04:00
93141da22c feat: common style for getters 2025-08-29 23:51:38 +04:00
5fbd6d6185 feat: pagination and query params for a deal end-point 2025-08-28 20:24:24 +04:00
4c7a997be6 feat: deal status history and default created_at in db 2025-08-26 18:12:28 +04:00
b776ad6758 feat: datetimes with timezones 2025-08-24 14:53:50 +04:00
b4b29d448b refactor: enabled importing all from module in ruff 2025-08-24 12:58:46 +04:00
dd1f4145ae feat: deals create, update, delete 2025-08-24 12:51:16 +04:00
c862544ae0 feat: common style for crud endpoints 2025-08-23 10:37:42 +04:00
c5e4dea52c feat: ordering for getters 2025-08-22 21:58:39 +04:00
5e20da8356 feat: projects create, update, delete 2025-08-13 15:01:22 +04:00
71c0901909 fix: status creating fix 2025-08-08 11:28:56 +04:00
3b1b6f0523 feat: create and delete status endpoints 2025-08-07 15:47:07 +04:00
2fed828768 feat: board deletion endpoint 2025-08-07 10:12:54 +04:00
734099165b feat: board creation endpoint 2025-08-07 09:18:23 +04:00
137 changed files with 6252 additions and 865 deletions

Binary file not shown.

BIN
assets/fonts/DejaVuSans.ttf Normal file

Binary file not shown.

View File

@ -4,8 +4,10 @@ from fastapi import Depends
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from backend.session import get_session from backend.session import get_session
from schemas.base import PaginationSchema from schemas.base import PaginationSchema, SortingSchema
from utils.pagination import pagination_parameters from utils.pagination import pagination_parameters
from utils.sorting import sorting_parameters
SessionDependency = Annotated[AsyncSession, Depends(get_session)] SessionDependency = Annotated[AsyncSession, Depends(get_session)]
PaginationDependency = Annotated[PaginationSchema, Depends(pagination_parameters)] PaginationDependency = Annotated[PaginationSchema, Depends(pagination_parameters)]
SortingDependency = Annotated[SortingSchema, Depends(sorting_parameters)]

18
main.py
View File

@ -1,18 +1,20 @@
from fastapi import FastAPI from fastapi import FastAPI, Request
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.gzip import GZipMiddleware
from fastapi.responses import ORJSONResponse from fastapi.responses import ORJSONResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastapi.middleware.gzip import GZipMiddleware from starlette.responses import JSONResponse
from fastapi_endpoints import auto_include_routers
import routers import routers
from utils.auto_include_routers import auto_include_routers
from utils.exceptions import ObjectNotFoundException
origins = ["http://localhost:3000"] origins = ["http://localhost:3000"]
app = FastAPI( app = FastAPI(
separate_input_output_schemas=True, separate_input_output_schemas=True,
default_response_class=ORJSONResponse, default_response_class=ORJSONResponse,
root_path="/api" root_path="/api",
) )
app.add_middleware( app.add_middleware(
@ -27,6 +29,12 @@ app.add_middleware(
minimum_size=1_000, minimum_size=1_000,
) )
auto_include_routers(app, routers)
@app.exception_handler(ObjectNotFoundException)
async def unicorn_exception_handler(request: Request, exc: ObjectNotFoundException):
return JSONResponse(status_code=404, content={"detail": exc.name})
auto_include_routers(app, routers, True)
app.mount("/static", StaticFiles(directory="static"), name="static") app.mount("/static", StaticFiles(directory="static"), name="static")

View File

@ -1,9 +1,21 @@
from sqlalchemy.orm import configure_mappers from sqlalchemy.orm import configure_mappers
from modules.fulfillment_base.models import * # noqa: F401
from .base import BaseModel as BaseModel from .base import BaseModel as BaseModel
from .board import Board as Board from .board import Board as Board
from .built_in_module import ( # noqa: F401
BuiltInModule as BuiltInModule,
project_built_in_module as project_built_in_module,
built_in_module_dependencies as built_in_module_dependencies,
)
from .deal import Deal as Deal from .deal import Deal as Deal
from .deal_group import DealGroup as DealGroup
from .deal_tag import (
DealTag as DealTag,
DealTagColor as DealTagColor,
deals_deal_tags as deals_deal_tags,
)
from .project import Project as Project from .project import Project as Project
from .status import Status as Status from .status import Status as Status, DealStatusHistory as DealStatusHistory
configure_mappers() configure_mappers()

View File

@ -13,10 +13,10 @@ if TYPE_CHECKING:
class Board(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin): class Board(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin):
__tablename__ = "boards" __tablename__ = "boards"
name: Mapped[str] = mapped_column(nullable=False) name: Mapped[str] = mapped_column()
lexorank: Mapped[str] = mapped_column(nullable=False) lexorank: Mapped[str] = mapped_column()
project_id: Mapped[int] = mapped_column(ForeignKey("projects.id"), nullable=False) project_id: Mapped[int] = mapped_column(ForeignKey("projects.id"))
project: Mapped["Project"] = relationship(back_populates="boards") project: Mapped["Project"] = relationship(back_populates="boards")
statuses: Mapped[list["Status"]] = relationship(back_populates="board") statuses: Mapped[list["Status"]] = relationship(back_populates="board")

79
models/built_in_module.py Normal file
View File

@ -0,0 +1,79 @@
import enum
from typing import TYPE_CHECKING
from sqlalchemy import Table, Column, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from models.base import BaseModel
if TYPE_CHECKING:
from models import Project
project_built_in_module = Table(
"project_built_in_module",
BaseModel.metadata,
Column("project_id", ForeignKey("projects.id"), primary_key=True),
Column("module_id", ForeignKey("built_in_modules.id"), primary_key=True),
)
built_in_module_dependencies = Table(
"built_in_module_dependencies",
BaseModel.metadata,
Column("module_id", ForeignKey("built_in_modules.id"), primary_key=True),
Column("depends_on_id", ForeignKey("built_in_modules.id"), primary_key=True),
)
class BuiltInModule(BaseModel):
__tablename__ = "built_in_modules"
id: Mapped[int] = mapped_column(primary_key=True)
key: Mapped[str] = mapped_column(unique=True)
label: Mapped[str] = mapped_column()
description: Mapped[str] = mapped_column()
is_deleted: Mapped[bool] = mapped_column(default=False)
depends_on: Mapped[list["BuiltInModule"]] = relationship(
secondary=built_in_module_dependencies,
primaryjoin="BuiltInModule.id == built_in_module_dependencies.c.module_id",
secondaryjoin="BuiltInModule.id == built_in_module_dependencies.c.depends_on_id",
back_populates="depended_on_by",
lazy="immediate",
)
depended_on_by: Mapped[list["BuiltInModule"]] = relationship(
secondary="built_in_module_dependencies",
primaryjoin="BuiltInModule.id == built_in_module_dependencies.c.depends_on_id",
secondaryjoin="BuiltInModule.id == built_in_module_dependencies.c.module_id",
back_populates="depends_on",
lazy="noload",
)
projects: Mapped[list["Project"]] = relationship(
uselist=True,
secondary="project_built_in_module",
back_populates="built_in_modules",
lazy="noload",
)
tabs: Mapped[list["BuiltInModuleTab"]] = relationship(
lazy="immediate", backref="module", cascade="all, delete-orphan"
)
class DeviceType(enum.StrEnum):
MOBILE = "mobile"
DESKTOP = "desktop"
BOTH = "both"
class BuiltInModuleTab(BaseModel):
__tablename__ = "built_in_module_tab"
id: Mapped[int] = mapped_column(primary_key=True)
key: Mapped[str] = mapped_column(unique=True)
label: Mapped[str] = mapped_column()
icon_name: Mapped[str] = mapped_column()
module_id: Mapped[int] = mapped_column(ForeignKey("built_in_modules.id"))
device: Mapped[DeviceType] = mapped_column(default=DeviceType.BOTH)

View File

@ -1,29 +1,58 @@
from typing import TYPE_CHECKING from typing import TYPE_CHECKING, Optional
from sqlalchemy import ForeignKey from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship, backref
from models.base import BaseModel from models.base import BaseModel
from models.mixins import SoftDeleteMixin, CreatedAtMixin, IdMixin from models.mixins import SoftDeleteMixin, CreatedAtMixin, IdMixin
if TYPE_CHECKING: if TYPE_CHECKING:
from models import Status, Board from models import Status, Board, DealStatusHistory, DealGroup, DealTag
from modules.clients.models import Client
class Deal(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin): class Deal(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin):
__tablename__ = "deals" __tablename__ = "deals"
name: Mapped[str] = mapped_column(nullable=False) name: Mapped[str] = mapped_column()
lexorank: Mapped[str] = mapped_column(nullable=False) lexorank: Mapped[str] = mapped_column()
status_id: Mapped[int] = mapped_column( status_id: Mapped[int] = mapped_column(
ForeignKey("statuses.id"), ForeignKey("statuses.id"),
nullable=False,
comment="Текущий статус", comment="Текущий статус",
) )
status: Mapped["Status"] = relationship(lazy="noload") status: Mapped["Status"] = relationship()
board_id: Mapped[int] = mapped_column( board_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("boards.id"), nullable=True, server_default="1" ForeignKey("boards.id"), server_default="1"
)
board: Mapped[Optional["Board"]] = relationship(back_populates="deals")
status_history: Mapped[list["DealStatusHistory"]] = relationship(
back_populates="deal",
cascade="all, delete-orphan",
lazy="noload",
)
group_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("deal_groups.id"), default=None, server_default=None
)
group: Mapped[Optional["DealGroup"]] = relationship(
lazy="noload", back_populates="deals"
)
tags: Mapped[list["DealTag"]] = relationship(
secondary="deals_deal_tags",
back_populates="deals",
lazy="selectin",
primaryjoin="Deal.id == deals_deal_tags.c.deal_id",
secondaryjoin="and_(DealTag.id == deals_deal_tags.c.deal_tag_id, DealTag.is_deleted == False)",
)
# module client
client_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("clients.id", ondelete="CASCADE"),
)
client: Mapped["Client"] = relationship(
backref=backref("deals", cascade="all, delete-orphan"), lazy="immediate"
) )
board: Mapped["Board"] = relationship(back_populates="deals")

17
models/deal_group.py Normal file
View File

@ -0,0 +1,17 @@
from typing import TYPE_CHECKING, Optional
from sqlalchemy.orm import mapped_column, Mapped, relationship
from models.base import BaseModel
from models.mixins import IdMixin
if TYPE_CHECKING:
from models import Deal
class DealGroup(BaseModel, IdMixin):
__tablename__ = "deal_groups"
name: Mapped[Optional[str]] = mapped_column()
lexorank: Mapped[str] = mapped_column()
deals: Mapped[list["Deal"]] = relationship(back_populates="group", lazy="noload")

51
models/deal_tag.py Normal file
View File

@ -0,0 +1,51 @@
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey, Column, Table, Index
from sqlalchemy.orm import mapped_column, Mapped, relationship
from models import BaseModel
from models.mixins import IdMixin, SoftDeleteMixin
if TYPE_CHECKING:
from models import Project, Deal
deals_deal_tags = Table(
"deals_deal_tags",
BaseModel.metadata,
Column("deal_id", ForeignKey("deals.id"), primary_key=True),
Column("deal_tag_id", ForeignKey("deal_tags.id"), primary_key=True),
)
class DealTagColor(BaseModel, IdMixin):
__tablename__ = "deal_tag_colors"
label: Mapped[str] = mapped_column(unique=True)
color: Mapped[str] = mapped_column(unique=True)
background_color: Mapped[str] = mapped_column(unique=True)
is_deleted: Mapped[bool] = mapped_column(default=False)
class DealTag(BaseModel, IdMixin, SoftDeleteMixin):
__tablename__ = "deal_tags"
name: Mapped[str] = mapped_column()
project_id: Mapped[int] = mapped_column(ForeignKey("projects.id"))
project: Mapped["Project"] = relationship(
back_populates="tags",
lazy="noload",
)
deals: Mapped[list["Deal"]] = relationship(
secondary="deals_deal_tags",
lazy="noload",
back_populates="tags",
)
tag_color_id: Mapped[int] = mapped_column(ForeignKey("deal_tag_colors.id"))
tag_color: Mapped[DealTagColor] = relationship(lazy="immediate")
__table_args__ = (
Index("idx_deal_name_project_id", "name", "project_id", "is_deleted"),
)

View File

@ -1,5 +1,6 @@
from datetime import datetime from datetime import datetime, timezone
from sqlalchemy import DateTime, Numeric
from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm import Mapped, mapped_column
@ -8,11 +9,26 @@ class IdMixin:
class SoftDeleteMixin: class SoftDeleteMixin:
is_deleted: Mapped[bool] = mapped_column( is_deleted: Mapped[bool] = mapped_column(default=False)
default=False,
nullable=False,
)
class CreatedAtMixin: class CreatedAtMixin:
created_at: Mapped[datetime] = mapped_column(nullable=False) created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
)
class LastModifiedAtMixin:
last_modified_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
default=lambda: datetime.now(timezone.utc),
)
class PriceMixin:
price: Mapped[float] = mapped_column(Numeric(12, 2), comment="Стоимость")
class CostMixin:
cost: Mapped[float] = mapped_column(Numeric(12, 2), comment="Себестоимость")

View File

@ -6,15 +6,29 @@ from models.base import BaseModel
from models.mixins import SoftDeleteMixin, CreatedAtMixin, IdMixin from models.mixins import SoftDeleteMixin, CreatedAtMixin, IdMixin
if TYPE_CHECKING: if TYPE_CHECKING:
from models import Board from models import Board, BuiltInModule, DealTag
class Project(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin): class Project(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin):
__tablename__ = "projects" __tablename__ = "projects"
name: Mapped[str] = mapped_column(nullable=False) name: Mapped[str] = mapped_column()
boards: Mapped[list["Board"]] = relationship( boards: Mapped[list["Board"]] = relationship(
back_populates="project", back_populates="project",
lazy="noload", lazy="noload",
) )
built_in_modules: Mapped[list["BuiltInModule"]] = relationship(
secondary="project_built_in_module",
back_populates="projects",
lazy="selectin",
order_by="asc(BuiltInModule.id)",
)
tags: Mapped[list["DealTag"]] = relationship(
back_populates="project",
primaryjoin="and_(Project.id == DealTag.project_id, DealTag.is_deleted == False)",
order_by="asc(DealTag.id)",
lazy="selectin",
)

View File

@ -4,17 +4,43 @@ from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from models.base import BaseModel from models.base import BaseModel
from models.mixins import SoftDeleteMixin, IdMixin from models.mixins import SoftDeleteMixin, IdMixin, CreatedAtMixin
if TYPE_CHECKING: if TYPE_CHECKING:
from models import Board from models import Board, Deal
class Status(BaseModel, IdMixin, SoftDeleteMixin): class Status(BaseModel, IdMixin, SoftDeleteMixin):
__tablename__ = "statuses" __tablename__ = "statuses"
name: Mapped[str] = mapped_column(nullable=False) name: Mapped[str] = mapped_column()
lexorank: Mapped[str] = mapped_column(nullable=False) lexorank: Mapped[str] = mapped_column()
color: Mapped[str] = mapped_column()
board_id: Mapped[int] = mapped_column(ForeignKey("boards.id"), nullable=False) board_id: Mapped[int] = mapped_column(ForeignKey("boards.id"))
board: Mapped["Board"] = relationship(back_populates="statuses") board: Mapped["Board"] = relationship(back_populates="statuses")
class DealStatusHistory(BaseModel, IdMixin, CreatedAtMixin):
__tablename__ = "status_history"
deal_id: Mapped[int] = mapped_column(ForeignKey("deals.id"))
deal: Mapped["Deal"] = relationship(back_populates="status_history")
from_status_id: Mapped[int] = mapped_column(
ForeignKey("statuses.id"),
comment="Старый статус",
)
from_status: Mapped[Status] = relationship(
foreign_keys=[from_status_id],
lazy="joined",
)
to_status_id: Mapped[int] = mapped_column(
ForeignKey("statuses.id"),
comment="Новый статус",
)
to_status: Mapped[Status] = relationship(
foreign_keys=[to_status_id],
lazy="joined",
)

0
modules/__init__.py Normal file
View File

View File

View File

@ -0,0 +1 @@
from .client import Client as Client, ClientDetails as ClientDetails

View File

@ -0,0 +1,47 @@
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(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()

View File

@ -0,0 +1 @@
from .client import ClientRepository as ClientRepository

View File

@ -0,0 +1,51 @@
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_with_args(
self, stmt: Select, include_deleted: bool
) -> Select:
if not include_deleted:
stmt = stmt.where(Client.is_deleted.is_(False))
return stmt.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)

View File

@ -0,0 +1,75 @@
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
is_deleted: bool = False
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

View File

@ -0,0 +1 @@
from .client import ClientService as ClientService

View File

@ -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

View File

View File

@ -0,0 +1,2 @@
from .barcode_pdf_generator import BarcodePdfGenerator as BarcodePdfGenerator
from .types import BarcodeData as BarcodeData

View File

@ -0,0 +1,161 @@
from io import BytesIO
from typing import Any, Optional
from reportlab.graphics.barcode import code128
from reportlab.lib.units import mm
from reportlab.platypus import Spacer, PageBreak, Paragraph
from modules.fulfillment_base.barcodes_pdf_gen.types import *
from utils.pdf import PdfMaker, PDFGenerator
class BarcodePdfGenerator(PDFGenerator):
def _get_attr_by_path(
self, value: Any, path: str
) -> Optional[str | int | float | bool]:
keys = path.split(".")
for key in keys:
try:
if isinstance(value, dict):
value = value[key]
else:
value = getattr(value, key)
except (KeyError, AttributeError, TypeError):
return None
return value
def generate(
self, barcodes_data: list[BarcodeData | PdfBarcodeImageGenData]
) -> BytesIO:
pdf_barcodes_gen_data: list[PdfBarcodeGenData | PdfBarcodeImageGenData] = []
for barcode_data in barcodes_data:
if "barcode" not in barcode_data:
pdf_barcodes_gen_data.append(barcode_data)
continue
attributes = {}
for attribute in barcode_data["template"].attributes:
value = self._get_attr_by_path(barcode_data["product"], attribute.key)
if not value or not value.strip():
continue
attributes[attribute.name] = value
barcode_text = "<br/>".join(
[f"{key}: {value}" for key, value in attributes.items()]
)
pdf_barcodes_gen_data.append(
{
"barcode_value": barcode_data["barcode"],
"text": barcode_text,
"num_duplicates": barcode_data["num_duplicates"],
}
)
return self._generate(pdf_barcodes_gen_data)
def _generate(
self, barcodes_data: list[PdfBarcodeGenData | PdfBarcodeImageGenData]
) -> BytesIO:
pdf_maker = PdfMaker((self.page_width, self.page_height))
pdf_files: list[BytesIO] = []
for barcode_data in barcodes_data:
if "barcode_value" in barcode_data:
pdf_files.append(self._generate_for_one_product(barcode_data))
else:
pdf_files.append(self._generate_for_one_product_using_img(barcode_data))
pdf_files.append(self._generate_spacers())
for file in pdf_files[:-1]:
pdf_maker.add_pdfs(file)
return pdf_maker.get_bytes()
def _generate_for_one_product(self, barcode_data: PdfBarcodeGenData) -> BytesIO:
buffer = BytesIO()
doc = self._create_doc(buffer)
# Создаем абзац с новым стилем
paragraph = Paragraph(barcode_data["text"], self.small_style)
# Получаем ширину и высоту абзаца
paragraph_width, paragraph_height = paragraph.wrap(
self.page_width - 2 * mm, self.page_height
)
# Рассчитываем доступное пространство для штрихкода
human_readable_height = 6 * mm # Высота human-readable текста
space_between_text_and_barcode = 4 * mm # Отступ между текстом и штрихкодом
barcode_height = (
self.page_height
- paragraph_height
- human_readable_height
- space_between_text_and_barcode
- 4 * mm
) # Учитываем поля и отступы
# Создаем штрихкод
available_width = self.page_width - 4 * mm # Учитываем поля
# Приблизительное количество элементов в штрихкоде Code 128 для средней длины
num_elements = 11 * len(
barcode_data["barcode_value"]
) # Примерная оценка: 11 элементов на символ
# Рассчитываем ширину штриха
bar_width = available_width / num_elements
barcode = code128.Code128(
barcode_data["barcode_value"],
barWidth=bar_width,
barHeight=barcode_height,
humanReadable=True,
)
# Добавление штрихкодов в список элементов документа
elements = []
for _ in range(barcode_data["num_duplicates"]):
elements.append(paragraph)
elements.append(
Spacer(1, space_between_text_and_barcode)
) # Отступ между текстом и штрихкодом
elements.append(PageBreak())
# Функция для отрисовки штрихкода на canvas
def add_barcode(canvas, doc):
barcode_width = barcode.width
barcode_x = (self.page_width - barcode_width) / 2 # Центрируем штрихкод
# Размещаем штрихкод снизу с учетом отступа
barcode_y = human_readable_height + 2 * mm
barcode.drawOn(canvas, barcode_x, barcode_y)
# Создаем документ
doc.build(
elements, onFirstPage=add_barcode, onLaterPages=add_barcode
) # Убираем последний PageBreak
buffer.seek(0)
return buffer
def _generate_for_one_product_using_img(
self, barcode_data: PdfBarcodeImageGenData
) -> BytesIO:
with open(barcode_data["barcode_image_url"], "rb") as pdf_file:
pdf_bytes = pdf_file.read()
pdf_maker = PdfMaker((self.page_width, self.page_height))
for _ in range(barcode_data["num_duplicates"]):
pdf_maker.add_pdfs(BytesIO(pdf_bytes))
return pdf_maker.get_bytes()
def _generate_spacers(self) -> BytesIO:
buffer = BytesIO()
doc = self._create_doc(buffer)
elements = []
for _ in range(self.number_of_spacing_pages):
elements.append(PageBreak())
doc.build(elements)
buffer.seek(0)
return buffer

View File

@ -0,0 +1,21 @@
from typing import TypedDict
from modules.fulfillment_base.models import BarcodeTemplate, Product
class BarcodeData(TypedDict):
barcode: str
template: BarcodeTemplate
product: Product
num_duplicates: int
class PdfBarcodeGenData(TypedDict):
barcode_value: str
text: str
num_duplicates: int
class PdfBarcodeImageGenData(TypedDict):
barcode_image_url: str
num_duplicates: int

View File

@ -0,0 +1 @@
from .service import *

View File

@ -0,0 +1,7 @@
from enum import IntEnum, unique
@unique
class ServiceType(IntEnum):
DEAL_SERVICE = 0
PRODUCT_SERVICE = 1

View File

@ -0,0 +1,17 @@
from .barcode import (
ProductBarcode as ProductBarcode,
ProductBarcodeImage as ProductBarcodeImage,
)
from .barcode_template import (
BarcodeTemplateAttribute as BarcodeTemplateAttribute,
BarcodeTemplateSize as BarcodeTemplateSize,
BarcodeTemplate as BarcodeTemplate,
)
from .deal_product import (
DealProduct as DealProduct,
DealProductService as DealProductService,
)
from .deal_service import DealService as DealService
from .product import Product as Product
from .service import Service as Service, ServiceCategory as ServiceCategory
from .marketplace import BaseMarketplace as BaseMarketplace, Marketplace as Marketplace

View File

@ -0,0 +1,36 @@
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from models.base import BaseModel
if TYPE_CHECKING:
from modules.fulfillment_base.models import Product
class ProductBarcode(BaseModel):
__tablename__ = "fulfillment_base_product_barcodes"
product_id: Mapped[int] = mapped_column(
ForeignKey("fulfillment_base_products.id"),
primary_key=True,
)
product: Mapped["Product"] = relationship(back_populates="barcodes")
barcode: Mapped[str] = mapped_column(
primary_key=True, index=True, comment="ШК товара"
)
class ProductBarcodeImage(BaseModel):
__tablename__ = "fulfillment_base_product_barcode_images"
product_id: Mapped[int] = mapped_column(
ForeignKey("fulfillment_base_products.id"),
primary_key=True,
comment="ID товара",
)
product: Mapped["Product"] = relationship(back_populates="barcode_image")
filename: Mapped[str] = mapped_column()

View File

@ -0,0 +1,42 @@
from sqlalchemy import ForeignKey, Table, Column
from sqlalchemy.orm import Mapped, mapped_column, relationship
from models.base import BaseModel
from models.mixins import IdMixin, SoftDeleteMixin
barcode_template_attribute_link = Table(
"barcode_template_attribute_links",
BaseModel.metadata,
Column("barcode_template_id", ForeignKey("barcode_templates.id")),
Column("attribute_id", ForeignKey("barcode_template_attributes.id")),
)
class BarcodeTemplateAttribute(BaseModel, IdMixin):
__tablename__ = "barcode_template_attributes"
key: Mapped[str] = mapped_column(index=True, comment="Ключ атрибута")
name: Mapped[str] = mapped_column(index=True, comment="Метка атрибута")
class BarcodeTemplateSize(BaseModel, IdMixin):
__tablename__ = "barcode_template_sizes"
name: Mapped[str] = mapped_column(index=True, comment="Название размера")
width: Mapped[int] = mapped_column(comment="Ширина в мм")
height: Mapped[int] = mapped_column(comment="Высота в мм")
class BarcodeTemplate(BaseModel, IdMixin, SoftDeleteMixin):
__tablename__ = "barcode_templates"
name: Mapped[str] = mapped_column(index=True, comment="Название шаблона")
attributes: Mapped[list["BarcodeTemplateAttribute"]] = relationship(
secondary=barcode_template_attribute_link,
lazy="selectin",
)
is_default: Mapped[bool] = mapped_column(default=False, comment="По умолчанию")
size_id: Mapped[int] = mapped_column(ForeignKey("barcode_template_sizes.id"))
size: Mapped["BarcodeTemplateSize"] = relationship(lazy="joined")

View File

@ -0,0 +1,62 @@
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey, ForeignKeyConstraint
from sqlalchemy.orm import Mapped, mapped_column, relationship
from models.base import BaseModel
from models.mixins import PriceMixin
if TYPE_CHECKING:
from models import Deal, Service, Product
class DealProduct(BaseModel):
__tablename__ = "fulfillment_base_deal_products"
deal_id: Mapped[int] = mapped_column(ForeignKey("deals.id"), primary_key=True)
product_id: Mapped[int] = mapped_column(
ForeignKey("fulfillment_base_products.id"), primary_key=True
)
quantity: Mapped[int] = mapped_column(default=1)
comment: Mapped[str] = mapped_column(comment="Комментарий к товару")
deal: Mapped["Deal"] = relationship(backref="deal_products")
product: Mapped["Product"] = relationship()
product_services: Mapped[list["DealProductService"]] = relationship(
back_populates="deal_product",
primaryjoin="and_(DealProduct.deal_id==DealProductService.deal_id, DealProduct.product_id==DealProductService.product_id)",
cascade="all, delete-orphan",
)
class DealProductService(BaseModel, PriceMixin):
__tablename__ = "fulfillment_base_deal_products_services"
deal_id: Mapped[int] = mapped_column(primary_key=True)
product_id: Mapped[int] = mapped_column(primary_key=True)
service_id: Mapped[int] = mapped_column(
ForeignKey("fulfillment_base_services.id"), primary_key=True
)
is_fixed_price: Mapped[bool] = mapped_column(
default=False, server_default="0", comment="Фиксированная цена"
)
deal_product: Mapped["DealProduct"] = relationship(
back_populates="product_services",
primaryjoin="and_(DealProductService.deal_id==DealProduct.deal_id, DealProductService.product_id==DealProduct.product_id)",
)
service: Mapped["Service"] = relationship()
__table_args__ = (
ForeignKeyConstraint(
["deal_id", "product_id"],
[
"fulfillment_base_deal_products.deal_id",
"fulfillment_base_deal_products.product_id",
],
ondelete="CASCADE",
),
)

View File

@ -0,0 +1,28 @@
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from models.base import BaseModel
from models.mixins import PriceMixin
if TYPE_CHECKING:
from models import Deal
from modules.fulfillment_base.models import Service
class DealService(BaseModel, PriceMixin):
__tablename__ = "fulfillment_base_deal_services"
deal_id: Mapped[int] = mapped_column(ForeignKey("deals.id"), primary_key=True)
deal: Mapped["Deal"] = relationship(backref="deal_services")
service_id: Mapped[int] = mapped_column(
ForeignKey("fulfillment_base_services.id"), primary_key=True
)
service: Mapped["Service"] = relationship()
quantity: Mapped[int] = mapped_column(default=1)
is_fixed_price: Mapped[bool] = mapped_column(
default=False, server_default="0", comment="Фиксированная цена"
)

View File

@ -0,0 +1,32 @@
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey, JSON
from sqlalchemy.orm import Mapped, mapped_column, relationship
from models.base import BaseModel
from models.mixins import IdMixin, SoftDeleteMixin
if TYPE_CHECKING:
from modules.clients.models import Client
class BaseMarketplace(BaseModel, IdMixin):
__tablename__ = "fulfillment_base_base_marketplaces"
name: Mapped[str] = mapped_column()
icon_url: Mapped[str] = mapped_column()
class Marketplace(BaseModel, IdMixin, SoftDeleteMixin):
__tablename__ = "fulfillment_base_marketplaces"
base_marketplace_id: Mapped[str] = mapped_column(
ForeignKey("fulfillment_base_base_marketplaces.id")
)
base_marketplace: Mapped["BaseMarketplace"] = relationship(lazy="joined")
client_id: Mapped[int] = mapped_column(ForeignKey("clients.id"))
client: Mapped["Client"] = relationship()
name: Mapped[str] = mapped_column()
auth_data: Mapped[dict] = mapped_column(type_=JSON)

View File

@ -0,0 +1,65 @@
from typing import Optional
from sqlalchemy import ForeignKey
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,
ProductBarcodeImage,
)
class Product(BaseModel, IdMixin, SoftDeleteMixin):
__tablename__ = "fulfillment_base_products"
name: Mapped[str] = mapped_column()
article: Mapped[str] = mapped_column(index=True)
factory_article: Mapped[str] = mapped_column(
index=True, default="", server_default=""
)
brand: Mapped[str] = mapped_column(default="", comment="Бренд")
color: Mapped[str] = mapped_column(default="", comment="Цвет")
composition: Mapped[str] = mapped_column(default="", comment="Состав")
size: Mapped[str] = mapped_column(default="", comment="Размер")
additional_info: Mapped[str] = mapped_column(
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",
lazy="selectin",
cascade="all, delete-orphan",
)
barcodes: Mapped[list["ProductBarcode"]] = relationship(
back_populates="product",
cascade="all, delete-orphan",
)
barcode_template_id: Mapped[Optional[int]] = mapped_column(
ForeignKey("barcode_templates.id")
)
barcode_template: Mapped["BarcodeTemplate"] = relationship(lazy="joined")
barcode_image: Mapped["ProductBarcodeImage"] = relationship(
back_populates="product",
lazy="joined",
uselist=False,
)
class ProductImage(BaseModel, IdMixin):
__tablename__ = "fulfillment_base_product_images"
product_id: Mapped[int] = mapped_column(ForeignKey("fulfillment_base_products.id"))
product: Mapped["Product"] = relationship(back_populates="images")
image_url: Mapped[str] = mapped_column()

View File

@ -0,0 +1,69 @@
from sqlalchemy import ForeignKey, Table, Column
from sqlalchemy.orm import Mapped, mapped_column, relationship
from models.base import BaseModel
from models.mixins import IdMixin, SoftDeleteMixin, CostMixin, PriceMixin
from modules.fulfillment_base import enums
services_kit_services = Table(
"fulfillment_base_services_kit_services",
BaseModel.metadata,
Column("services_kit_id", ForeignKey("fulfillment_base_services_kits.id")),
Column("service_id", ForeignKey("fulfillment_base_services.id")),
)
class Service(BaseModel, IdMixin, SoftDeleteMixin, PriceMixin, CostMixin):
__tablename__ = "fulfillment_base_services"
name: Mapped[str] = mapped_column(index=True)
price_ranges: Mapped[list["ServicePriceRange"]] = relationship(
back_populates="service",
lazy="selectin",
order_by="asc(ServicePriceRange.from_quantity)",
cascade="all, delete-orphan",
)
category_id: Mapped[int] = mapped_column(
ForeignKey("fulfillment_base_service_categories.id"),
comment="ID категории услуги",
)
category: Mapped["ServiceCategory"] = relationship("ServiceCategory", lazy="joined")
service_type: Mapped[int] = mapped_column(
server_default=f"{enums.service.ServiceType.DEAL_SERVICE}",
comment="Тип услуги",
)
lexorank: Mapped[str] = mapped_column(comment="Ранг услуги")
class ServiceCategory(BaseModel, IdMixin, SoftDeleteMixin):
__tablename__ = "fulfillment_base_service_categories"
name: Mapped[str] = mapped_column()
deal_service_rank: Mapped[str] = mapped_column(comment="Ранг услуги для сделки")
product_service_rank: Mapped[str] = mapped_column(comment="Ранг услуги для товара")
class ServicesKit(BaseModel, IdMixin):
__tablename__ = "fulfillment_base_services_kits"
name: Mapped[str] = mapped_column()
service_type: Mapped[int] = mapped_column(
server_default=f"{enums.ServiceType.DEAL_SERVICE}",
comment="Тип услуги",
)
services: Mapped[list["Service"]] = relationship(
secondary=services_kit_services, lazy="selectin"
)
class ServicePriceRange(BaseModel, IdMixin, PriceMixin):
__tablename__ = "fulfillment_base_service_price_ranges"
service_id: Mapped[int] = mapped_column(ForeignKey("fulfillment_base_services.id"))
service: Mapped[Service] = relationship(back_populates="price_ranges")
from_quantity: Mapped[int] = mapped_column(comment="От количества")
to_quantity: Mapped[int] = mapped_column(comment="До количества")

View File

@ -0,0 +1,9 @@
from .barcode_template import BarcodeTemplateRepository as BarcodeTemplateRepository
from .deal_product import DealProductRepository as DealProductRepository
from .deal_service import DealServiceRepository as DealServiceRepository
from .marketplace import MarketplaceRepository as MarketplaceRepository
from .product import ProductRepository as ProductRepository
from .product_service import ProductServiceRepository as ProductServiceRepository
from .service import ServiceRepository as ServiceRepository
from .service_category import ServiceCategoryRepository as ServiceCategoryRepository
from .services_kit import ServicesKitRepository as ServicesKitRepository

View File

@ -0,0 +1,119 @@
from sqlalchemy import update
from sqlalchemy.orm import joinedload, selectinload
from modules.fulfillment_base.models import (
BarcodeTemplate,
BarcodeTemplateAttribute,
BarcodeTemplateSize,
)
from modules.fulfillment_base.schemas.barcode_template import (
CreateBarcodeTemplateSchema,
UpdateBarcodeTemplateSchema,
)
from repositories.mixins import *
class BarcodeTemplateRepository(
RepCrudMixin[
BarcodeTemplate, CreateBarcodeTemplateSchema, UpdateBarcodeTemplateSchema
],
):
session: AsyncSession
entity_class = BarcodeTemplate
entity_not_found_msg = "Шаблон штрихкода не найден"
def _process_get_all_stmt(self, stmt: Select) -> Select:
return (
stmt.options(
selectinload(BarcodeTemplate.attributes),
joinedload(BarcodeTemplate.size),
)
.where(BarcodeTemplate.is_deleted.is_(False))
.order_by(BarcodeTemplate.id)
)
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
return stmt.options(
selectinload(BarcodeTemplate.attributes),
joinedload(BarcodeTemplate.size),
)
async def _get_size_by_id(self, size_id: int) -> Optional[BarcodeTemplateSize]:
stmt = select(BarcodeTemplateSize).where(BarcodeTemplateSize.id == size_id)
result = await self.session.scalars(stmt)
return result.one_or_none()
async def _get_attrs_by_ids(
self, attrs_ids: list[int]
) -> list[BarcodeTemplateAttribute]:
stmt = select(BarcodeTemplateAttribute).where(
BarcodeTemplateAttribute.id.in_(attrs_ids)
)
result = await self.session.execute(stmt)
return list(result.scalars().all())
async def create(self, data: CreateBarcodeTemplateSchema) -> int:
if data.is_default is not None and data.is_default:
await self._turn_off_defaults()
data_dict = data.model_dump()
data_dict["size"] = await self._get_size_by_id(data.size.id)
data_dict["attributes"] = await self._get_attrs_by_ids(
[a.id for a in data.attributes]
)
obj = BarcodeTemplate(**data_dict)
self.session.add(obj)
await self.session.commit()
await self.session.refresh(obj)
return obj.id
async def _turn_off_defaults(self):
stmt = (
update(BarcodeTemplate)
.where(BarcodeTemplate.is_default.is_(True))
.values({"is_default": False})
)
await self.session.execute(stmt)
async def _set_first_as_default(self, with_commit: bool = False):
stmt = select(BarcodeTemplate).limit(1)
result = await self.session.execute(stmt)
obj = result.scalar()
if not obj:
return
obj.is_default = True
self.session.add(obj)
if with_commit:
await self.session.commit()
await self.session.refresh(obj)
async def update(
self, template: BarcodeTemplate, data: UpdateBarcodeTemplateSchema
) -> BarcodeTemplate:
if data.size is not None:
data.size = await self._get_size_by_id(data.size.id)
if data.attributes is not None:
data.attributes = await self._get_attrs_by_ids(
[a.id for a in data.attributes]
)
if data.is_default is not None:
if data.is_default:
await self._turn_off_defaults()
else:
await self._set_first_as_default()
return await self._apply_update_data_to_model(template, data, True)
async def _before_delete(self, template: BarcodeTemplate):
if template.is_default:
await self._set_first_as_default()
async def get_attributes(self) -> list[BarcodeTemplateAttribute]:
stmt = select(BarcodeTemplateAttribute)
result = await self.session.execute(stmt)
return list(result.scalars().all())
async def get_sizes(self) -> list[BarcodeTemplateSize]:
stmt = select(BarcodeTemplateSize)
result = await self.session.execute(stmt)
return list(result.scalars().all())

View File

@ -0,0 +1,62 @@
from typing import Optional
from sqlalchemy import Select, select
from sqlalchemy.orm import joinedload, selectinload
from modules.fulfillment_base.models import DealProductService, Product
from modules.fulfillment_base.models.deal_product import DealProduct
from modules.fulfillment_base.schemas.deal_product import (
UpdateDealProductSchema,
CreateDealProductSchema,
)
from repositories.base import BaseRepository
from repositories.mixins import RepGetAllMixin, RepUpdateMixin
from utils.exceptions import ObjectNotFoundException
class DealProductRepository(
BaseRepository,
RepGetAllMixin[DealProduct],
RepUpdateMixin[DealProduct, UpdateDealProductSchema],
):
entity_class = DealProduct
def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select:
deal_id = args[0]
return (
stmt.options(
joinedload(DealProduct.product).selectinload(Product.barcodes),
selectinload(DealProduct.product_services).joinedload(
DealProductService.service
),
)
.where(DealProduct.deal_id == deal_id)
.order_by(DealProduct.product_id)
)
async def get_by_id(
self, deal_id: int, product_id: int, raise_if_not_found: Optional[bool] = True
) -> Optional[DealProduct]:
stmt = (
select(DealProduct)
.options(
joinedload(DealProduct.product).selectinload(Product.barcodes),
selectinload(DealProduct.product_services).joinedload(
DealProductService.service
),
)
.where(DealProduct.deal_id == deal_id, DealProduct.product_id == product_id)
)
result = (await self.session.execute(stmt)).scalar_one_or_none()
if result is None and raise_if_not_found:
raise ObjectNotFoundException("Связь сделки с товаром не найдена")
return result
async def create(self, data: CreateDealProductSchema):
deal_product = DealProduct(**data.model_dump())
self.session.add(deal_product)
await self.session.commit()
async def delete(self, obj: DealProduct):
await self.session.delete(obj)
await self.session.commit()

View File

@ -0,0 +1,71 @@
from typing import Optional
from sqlalchemy import Select, select, delete
from sqlalchemy.orm import joinedload
from models import Deal
from modules.fulfillment_base.models import DealService
from modules.fulfillment_base.models.service import ServicesKit
from modules.fulfillment_base.schemas.deal_service import (
UpdateDealServiceSchema,
CreateDealServiceSchema,
)
from repositories.base import BaseRepository
from repositories.mixins import RepGetAllMixin, RepUpdateMixin
from utils.exceptions import ObjectNotFoundException
class DealServiceRepository(
BaseRepository,
RepGetAllMixin[DealService],
RepUpdateMixin[DealService, UpdateDealServiceSchema],
):
entity_class = DealService
def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select:
deal_id = args[0]
return (
stmt.options(
joinedload(DealService.service),
)
.where(DealService.deal_id == deal_id)
.order_by(DealService.service_id)
)
async def get_by_id(
self, deal_id: int, service_id: int, raise_if_not_found: Optional[bool] = True
) -> Optional[DealService]:
stmt = (
select(DealService)
.options(joinedload(DealService.service))
.where(DealService.deal_id == deal_id, DealService.service_id == service_id)
)
result = (await self.session.execute(stmt)).scalar_one_or_none()
if result is None and raise_if_not_found:
raise ObjectNotFoundException("Связь сделки с услугой не найдена")
return result
async def create(self, data: CreateDealServiceSchema):
deal_service = DealService(**data.model_dump())
self.session.add(deal_service)
await self.session.commit()
async def delete(self, obj: DealService):
await self.session.delete(obj)
await self.session.commit()
async def delete_deal_services(self, deal_id: int):
stmt = delete(DealService).where(DealService.deal_id == deal_id)
await self.session.execute(stmt)
await self.session.flush()
async def add_services_kit(self, deal: Deal, services_kit: ServicesKit):
for service in services_kit.services:
deal_service = DealService(
deal_id=deal.id,
service_id=service.id,
price=service.price,
quantity=1,
)
self.session.add(deal_service)
await self.session.commit()

View File

@ -0,0 +1,62 @@
from sqlalchemy.orm import joinedload
from modules.clients.models import Client
from modules.fulfillment_base.models import (
Marketplace,
BaseMarketplace,
)
from modules.fulfillment_base.schemas.marketplace import (
CreateMarketplaceSchema,
UpdateMarketplaceSchema,
)
from repositories.mixins import *
class MarketplaceRepository(
RepCrudMixin[Marketplace, CreateMarketplaceSchema, UpdateMarketplaceSchema],
):
session: AsyncSession
entity_class = Marketplace
entity_not_found_msg = "Маркетплейс не найден"
def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select:
client_id: int = args[0]
return (
stmt.options(
joinedload(Marketplace.base_marketplace),
joinedload(Marketplace.client),
)
.where(
Marketplace.is_deleted.is_(False), Marketplace.client_id == client_id
)
.order_by(Marketplace.id)
)
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
return stmt.options(
joinedload(Marketplace.base_marketplace), joinedload(Marketplace.client)
)
async def get_base_marketplaces(self) -> list[BaseMarketplace]:
stmt = select(BaseMarketplace)
result = await self.session.execute(stmt)
return list(result.scalars().all())
async def _prepare_create(self, data: CreateMarketplaceSchema) -> dict:
dict_data = data.model_dump()
dict_data["base_marketplace_id"] = data.base_marketplace.id
del dict_data["base_marketplace"]
dict_data["client_id"] = data.client.id
del dict_data["client"]
return dict_data
async def update(
self, template: Marketplace, data: UpdateMarketplaceSchema
) -> Marketplace:
if data.base_marketplace:
data.base_marketplace = BaseMarketplace(
**data.base_marketplace.model_dump()
)
if data.client:
data.client = Client(**data.client.model_dump())
return await self._apply_update_data_to_model(template, data, True)

View File

@ -0,0 +1,97 @@
from sqlalchemy import or_, delete
from sqlalchemy.orm import selectinload, joinedload
from modules.fulfillment_base.models import Product, ProductBarcode, BarcodeTemplate
from modules.fulfillment_base.schemas.product import (
CreateProductSchema,
UpdateProductSchema,
)
from repositories.mixins import *
class ProductRepository(
BaseRepository,
RepDeleteMixin[Product],
RepCreateMixin[Product, CreateProductSchema],
RepUpdateMixin[Product, UpdateProductSchema],
RepGetByIdMixin[Product],
):
entity_class = Product
entity_not_found_msg = "Товар не найден"
async def get_all(
self,
page: Optional[int],
items_per_page: Optional[int],
client_id: Optional[int],
search_input: Optional[str],
) -> tuple[list[Product], int]:
stmt = (
select(Product)
.options(selectinload(Product.barcodes))
.where(Product.is_deleted.is_(False))
)
if client_id:
stmt = stmt.where(Product.client_id == client_id)
if search_input:
stmt = stmt.where(
or_(
Product.name.ilike(f"%{search_input}%"),
Product.barcodes.any(
ProductBarcode.barcode.ilike(f"%{search_input}%")
),
Product.article.ilike(f"%{search_input}%"),
Product.factory_article.ilike(f"%{search_input}%"),
)
)
total_items = len((await self.session.execute(stmt)).all())
if page and items_per_page:
stmt = self._apply_pagination(stmt, page, items_per_page)
result = await self.session.execute(stmt)
return list(result.scalars().all()), total_items
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
return stmt.options(
selectinload(Product.barcodes),
joinedload(Product.client),
joinedload(Product.barcode_template).selectinload(
BarcodeTemplate.attributes
),
)
async def _after_create(self, product: Product, data: CreateProductSchema) -> None:
new_barcodes = [
ProductBarcode(product_id=product.id, barcode=barcode)
for barcode in data.barcodes
]
self.session.add_all(new_barcodes)
async def _update_barcodes(self, product: Product, new_barcodes: list[str]):
new_barcodes_set: set[str] = set(new_barcodes)
old_barcodes_set: set[str] = set(obj.barcode for obj in product.barcodes)
barcodes_to_add = new_barcodes_set - old_barcodes_set
barcodes_to_delete = old_barcodes_set - new_barcodes_set
del_stmt = delete(ProductBarcode).where(
ProductBarcode.product_id == product.id,
ProductBarcode.barcode.in_(barcodes_to_delete),
)
await self.session.execute(del_stmt)
new_barcodes = [
ProductBarcode(product_id=product.id, barcode=barcode)
for barcode in barcodes_to_add
]
self.session.add_all(new_barcodes)
await self.session.commit()
await self.session.refresh(product)
async def update(self, product: Product, data: UpdateProductSchema) -> Product:
if data.barcodes is not None:
await self._update_barcodes(product, data.barcodes)
del data.barcodes
return await self._apply_update_data_to_model(product, data, True)

View File

@ -0,0 +1,105 @@
from typing import Optional
from sqlalchemy import select, delete
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import joinedload
from modules.fulfillment_base.models import DealProductService, DealProduct
from modules.fulfillment_base.models.service import ServicesKit
from modules.fulfillment_base.schemas.product_service import *
from repositories.base import BaseRepository
from repositories.mixins import RepUpdateMixin
from utils.exceptions import ObjectNotFoundException
class ProductServiceRepository(
BaseRepository,
RepUpdateMixin[DealProductService, UpdateProductServiceSchema],
):
entity_class = DealProductService
session: AsyncSession
async def get_by_id(
self,
deal_id: int,
product_id: int,
service_id: int,
raise_if_not_found: Optional[bool] = True,
) -> Optional[DealProductService]:
stmt = (
select(DealProductService)
.options(
joinedload(DealProductService.service),
)
.where(
DealProductService.deal_id == deal_id,
DealProductService.product_id == product_id,
DealProductService.service_id == service_id,
)
)
result = (await self.session.execute(stmt)).scalar_one_or_none()
if result is None and raise_if_not_found:
raise ObjectNotFoundException("Связь услуги с товаром не найдена")
return result
async def create(self, data: CreateProductServiceSchema):
deal_product_service = DealProductService(**data.model_dump())
self.session.add(deal_product_service)
await self.session.commit()
async def delete(self, obj: DealProductService):
await self.session.delete(obj)
await self.session.commit()
async def get_product_services(
self, deal_id: int, product_id: int
) -> list[DealProductService]:
stmt = (
select(DealProductService)
.options(
joinedload(DealProductService.service),
)
.where(
DealProductService.deal_id == deal_id,
DealProductService.product_id == product_id,
)
)
return list(await self.session.scalars(stmt))
async def delete_product_services(self, deal_id: int, product_ids: list[int]):
stmt = delete(DealProductService).where(
DealProductService.deal_id == deal_id,
DealProductService.product_id.in_(product_ids),
)
await self.session.execute(stmt)
await self.session.flush()
async def duplicate_services(
self, deal_id: int, product_ids: list[int], services: list[DealProductService]
):
await self.delete_product_services(deal_id, product_ids)
for product_id in product_ids:
for prod_service in services:
product_service = DealProductService(
deal_id=deal_id,
product_id=product_id,
service_id=prod_service.service.id,
price=prod_service.price,
is_fixed_price=prod_service.is_fixed_price,
)
self.session.add(product_service)
await self.session.commit()
async def add_services_kit(
self, deal_product: DealProduct, services_kit: ServicesKit
):
for service in services_kit.services:
deal_product_service = DealProductService(
deal_id=deal_product.deal_id,
product_id=deal_product.product_id,
service_id=service.id,
price=service.price,
)
self.session.add(deal_product_service)
await self.session.commit()

View File

@ -0,0 +1,78 @@
from sqlalchemy import delete
from modules.fulfillment_base.models import Service
from modules.fulfillment_base.models.service import ServicePriceRange
from modules.fulfillment_base.schemas.service import (
CreateServiceSchema,
UpdateServiceSchema,
ServicePriceRangeSchema,
)
from repositories.mixins import *
class ServiceRepository(
BaseRepository,
RepGetAllMixin[Service],
RepDeleteMixin[Service],
RepUpdateMixin[Service, UpdateServiceSchema],
RepGetByIdMixin[Service],
):
entity_class = Service
entity_not_found_msg = "Услуга не найдена"
def _process_get_all_stmt(self, stmt: Select) -> Select:
return stmt.order_by(Service.lexorank)
@staticmethod
def _price_ranges_schemas_to_models(
price_ranges: list[ServicePriceRangeSchema],
) -> list[ServicePriceRange]:
models = []
for range in price_ranges:
models.append(
ServicePriceRange(
from_quantity=range.from_quantity,
to_quantity=range.to_quantity,
price=range.price,
)
)
return models
async def create(self, data: CreateServiceSchema) -> int:
price_ranges = self._price_ranges_schemas_to_models(data.price_ranges)
data_dict = data.model_dump()
data_dict["price_ranges"] = price_ranges
data_dict["category_id"] = data.category.id
del data_dict["category"]
service = Service(**data_dict)
self.session.add(service)
await self.session.commit()
await self.session.refresh(service)
return service.id
async def _delete_price_ranges_by_service_id(self, service_id: int) -> None:
stmt = delete(ServicePriceRange).where(
ServicePriceRange.service_id == service_id
)
await self.session.execute(stmt)
await self.session.commit()
async def update(self, service: Service, data: UpdateServiceSchema) -> Service:
if data.price_ranges is not None:
await self._delete_price_ranges_by_service_id(service.id)
price_ranges = self._price_ranges_schemas_to_models(data.price_ranges)
for price_range in price_ranges:
service.price_ranges.append(price_range)
del data.price_ranges
if data.category is not None:
data.category_id = data.category.id
del data.category
return await self._apply_update_data_to_model(service, data, True)
async def get_by_ids(self, ids: list[int]) -> list[Service]:
stmt = select(Service).where(Service.id.in_(ids))
result = await self.session.execute(stmt)
return result.scalars().all()

View File

@ -0,0 +1,18 @@
from modules.fulfillment_base.models import ServiceCategory
from modules.fulfillment_base.schemas.service_category import (
CreateServiceCategorySchema,
UpdateServiceCategorySchema,
)
from repositories.mixins import *
class ServiceCategoryRepository(
RepCrudMixin[ServiceCategory, CreateServiceCategorySchema, UpdateServiceCategorySchema]
):
entity_class = ServiceCategory
entity_not_found_msg = "Категория услуги не найдена"
async def update(
self, service: ServiceCategory, data: UpdateServiceCategorySchema
) -> ServiceCategory:
return await self._apply_update_data_to_model(service, data, True)

View File

@ -0,0 +1,43 @@
from sqlalchemy.orm import selectinload
from modules.fulfillment_base.models.service import ServicesKit
from modules.fulfillment_base.repositories import ServiceRepository
from modules.fulfillment_base.schemas.services_kit import (
CreateServicesKitSchema,
UpdateServicesKitSchema,
)
from repositories.mixins import *
class ServicesKitRepository(
RepCrudMixin[ServicesKit, CreateServicesKitSchema, UpdateServicesKitSchema],
):
entity_class = ServicesKit
entity_not_found_msg = "Набор услуг не найден"
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
return stmt.options(selectinload(ServicesKit.services))
async def create(self, data: CreateServicesKitSchema) -> int:
if data.services is not None:
service_ids: list[int] = [service.id for service in data.services]
data.services = await ServiceRepository(self.session).get_by_ids(
service_ids
)
kit = ServicesKit(
name=data.name, service_type=data.service_type, services=data.services
)
self.session.add(kit)
await self.session.commit()
await self.session.refresh(kit)
return kit.id
async def update(
self, service_kit: ServicesKit, data: UpdateServicesKitSchema
) -> ServicesKit:
if data.services is not None:
service_ids: list[int] = [service.id for service in data.services]
data.services = await ServiceRepository(self.session).get_by_ids(
service_ids
)
return await self._apply_update_data_to_model(service_kit, data, True)

View File

@ -0,0 +1,82 @@
from typing import Optional
from schemas.base import BaseSchema, BaseResponse
# region Entity
class BarcodeTemplateAttributeSchema(BaseSchema):
id: int
key: str
name: str
class BarcodeTemplateSizeSchema(BaseSchema):
id: int
name: str
width: int
height: int
class CreateBarcodeTemplateSchema(BaseSchema):
name: str
attributes: list[BarcodeTemplateAttributeSchema]
is_default: bool
size: BarcodeTemplateSizeSchema
class BarcodeTemplateSchema(CreateBarcodeTemplateSchema):
id: int
class UpdateBarcodeTemplateSchema(BaseSchema):
name: Optional[str] = None
attributes: Optional[list[BarcodeTemplateAttributeSchema]] = None
is_default: Optional[bool] = None
size: Optional[BarcodeTemplateSizeSchema] = None
# endregion
# region Request
class CreateBarcodeTemplateRequest(BaseSchema):
entity: CreateBarcodeTemplateSchema
class UpdateBarcodeTemplateRequest(BaseSchema):
entity: UpdateBarcodeTemplateSchema
# endregion
# region Response
class GetBarcodeTemplatesResponse(BaseSchema):
items: list[BarcodeTemplateSchema]
class CreateBarcodeTemplateResponse(BaseResponse):
entity: BarcodeTemplateSchema
class UpdateBarcodeTemplateResponse(BaseResponse):
pass
class DeleteBarcodeTemplateResponse(BaseResponse):
pass
class GetBarcodeAttributesResponse(BaseSchema):
items: list[BarcodeTemplateAttributeSchema]
class GetBarcodeTemplateSizesResponse(BaseSchema):
items: list[BarcodeTemplateSizeSchema]
# endregion

View File

@ -0,0 +1,64 @@
from modules.fulfillment_base.schemas.product import ProductSchema
from modules.fulfillment_base.schemas.product_service import ProductServiceSchema
from schemas.base import BaseSchema, BaseResponse
# region Entity
class DealProductSchema(BaseSchema):
deal_id: int
product_id: int
product: ProductSchema
quantity: int
comment: str
product_services: list[ProductServiceSchema]
class CreateDealProductSchema(BaseSchema):
deal_id: int
product_id: int
quantity: int
comment: str
class UpdateDealProductSchema(BaseSchema):
quantity: int
comment: str
# endregion
# region Request
class CreateDealProductRequest(BaseSchema):
entity: CreateDealProductSchema
class UpdateDealProductRequest(BaseSchema):
entity: UpdateDealProductSchema
# endregion
# region Response
class GetDealProductsResponse(BaseSchema):
items: list[DealProductSchema]
class CreateDealProductResponse(BaseResponse):
entity: DealProductSchema
class UpdateDealProductResponse(BaseResponse):
pass
class DeleteDealProductResponse(BaseResponse):
pass
# endregion

View File

@ -0,0 +1,72 @@
from modules.fulfillment_base.schemas.service import ServiceSchema
from schemas.base import BaseSchema, BaseResponse
# region Entity
class DealServiceSchema(BaseSchema):
deal_id: int
service_id: int
service: ServiceSchema
quantity: int
price: float
is_fixed_price: bool
class CreateDealServiceSchema(BaseSchema):
deal_id: int
service_id: int
quantity: int
price: float
class UpdateDealServiceSchema(BaseSchema):
quantity: int
price: float
is_fixed_price: bool
# endregion
# region Request
class CreateDealServiceRequest(BaseSchema):
entity: CreateDealServiceSchema
class UpdateDealServiceRequest(BaseSchema):
entity: UpdateDealServiceSchema
class DealAddKitRequest(BaseSchema):
deal_id: int
kit_id: int
# endregion
# region Response
class GetDealServicesResponse(BaseSchema):
items: list[DealServiceSchema]
class CreateDealServiceResponse(BaseResponse):
entity: DealServiceSchema
class UpdateDealServiceResponse(BaseResponse):
pass
class DeleteDealServiceResponse(BaseResponse):
pass
class DealAddKitResponse(BaseResponse):
pass
# endregion

View File

@ -0,0 +1,77 @@
from typing import Optional
from modules.clients.schemas.client import ClientSchema
from schemas.base import BaseSchema, BaseResponse
# region Entity
class BaseMarketplaceSchema(BaseSchema):
id: int
name: str
icon_url: str
class MarketplaceSchema(BaseSchema):
id: int
base_marketplace_id: int
base_marketplace: BaseMarketplaceSchema
client: ClientSchema
name: str
auth_data: dict
class CreateMarketplaceSchema(BaseSchema):
base_marketplace: BaseMarketplaceSchema
client: ClientSchema
name: str
auth_data: dict
class UpdateMarketplaceSchema(BaseSchema):
base_marketplace: Optional[BaseMarketplaceSchema] = None
client: Optional[ClientSchema] = None
name: Optional[str] = None
auth_data: Optional[dict] = None
# endregion
# region Request
class CreateMarketplaceRequest(BaseSchema):
entity: CreateMarketplaceSchema
class UpdateMarketplaceRequest(BaseSchema):
entity: UpdateMarketplaceSchema
# endregion
# region Response
class GetBaseMarketplacesResponse(BaseSchema):
items: list[BaseMarketplaceSchema]
class GetMarketplacesResponse(BaseSchema):
items: list[MarketplaceSchema]
class CreateMarketplaceResponse(BaseResponse):
entity: MarketplaceSchema
class UpdateMarketplaceResponse(BaseResponse):
pass
class DeleteMarketplaceResponse(BaseResponse):
pass
# endregion

View File

@ -0,0 +1,104 @@
from typing import Optional
from pydantic import field_validator
from modules.fulfillment_base.models import ProductBarcode
from modules.fulfillment_base.schemas.barcode_template import BarcodeTemplateSchema
from schemas.base import BaseSchema, BaseResponse, PaginationInfoSchema, BasePdfResponse
# region Entity
class ProductImageSchema(BaseSchema):
id: int
product_id: int
image_url: str
class CreateProductSchema(BaseSchema):
name: str
article: str
factory_article: str
client_id: int
barcode_template_id: int
brand: Optional[str]
color: Optional[str]
composition: Optional[str]
size: Optional[str]
additional_info: Optional[str]
barcodes: list[str]
class ProductSchema(CreateProductSchema):
id: int
barcode_template: BarcodeTemplateSchema
@field_validator("barcodes", mode="before")
def barcodes_to_list(cls, v: Optional[list[ProductBarcode]]):
if isinstance(v, list):
return [barcode.barcode for barcode in v]
return v
class UpdateProductSchema(BaseSchema):
name: Optional[str] = None
article: Optional[str] = None
factory_article: Optional[str] = None
barcode_template_id: Optional[int] = None
brand: Optional[str] = None
color: Optional[str] = None
composition: Optional[str] = None
size: Optional[str] = None
additional_info: Optional[str] = None
barcodes: Optional[list[str]] = None
images: list[ProductImageSchema] | None = []
# endregion
# region Request
class CreateProductRequest(BaseSchema):
entity: CreateProductSchema
class UpdateProductRequest(BaseSchema):
entity: UpdateProductSchema
class GetProductBarcodePdfRequest(BaseSchema):
quantity: int
product_id: int
barcode: str
# endregion
# region Response
class GetProductsResponse(BaseSchema):
items: list[ProductSchema]
pagination_info: PaginationInfoSchema
class CreateProductResponse(BaseResponse):
entity: ProductSchema
class UpdateProductResponse(BaseResponse):
pass
class DeleteProductResponse(BaseResponse):
pass
class GetProductBarcodePdfResponse(BasePdfResponse):
pass
# endregion

View File

@ -0,0 +1,79 @@
from modules.fulfillment_base.schemas.service import ServiceSchema
from schemas.base import BaseSchema, BaseResponse
# region Entity
class ProductServiceSchema(BaseSchema):
deal_id: int
product_id: int
service_id: int
service: ServiceSchema
price: float
is_fixed_price: bool
class CreateProductServiceSchema(BaseSchema):
deal_id: int
product_id: int
service_id: int
price: float
class UpdateProductServiceSchema(BaseSchema):
price: float
is_fixed_price: bool
# endregion
# region Request
class CreateProductServiceRequest(BaseSchema):
entity: CreateProductServiceSchema
class UpdateProductServiceRequest(BaseSchema):
entity: UpdateProductServiceSchema
class ProductServicesDuplicateRequest(BaseSchema):
deal_id: int
source_deal_product_id: int
target_deal_product_ids: list[int]
class DealProductAddKitRequest(BaseSchema):
deal_id: int
product_id: int
kit_id: int
# endregion
# region Response
class CreateProductServiceResponse(BaseResponse):
entity: ProductServiceSchema
class UpdateProductServiceResponse(BaseResponse):
pass
class DeleteProductServiceResponse(BaseResponse):
pass
class ProductServicesDuplicateResponse(BaseResponse):
pass
class DealProductAddKitResponse(BaseResponse):
pass
# endregion

View File

@ -0,0 +1,77 @@
from typing import Optional
from modules.fulfillment_base.schemas.service_category import ServiceCategorySchema
from schemas.base import BaseSchema, BaseResponse
# region Entity
class ServicePriceRangeSchema(BaseSchema):
id: int | None
from_quantity: int
to_quantity: int
price: float
class CreateServiceSchema(BaseSchema):
name: str
category: ServiceCategorySchema
category_id: Optional[int] = None
price: float
service_type: int
price_ranges: list[ServicePriceRangeSchema]
cost: Optional[float]
lexorank: str
class ServiceSchema(CreateServiceSchema):
id: int
class UpdateServiceSchema(BaseSchema):
name: Optional[str] = None
category: Optional[ServiceCategorySchema] = None
category_id: Optional[int] = None
price: Optional[float] = None
service_type: Optional[int] = None
price_ranges: Optional[list[ServicePriceRangeSchema]] = None
cost: Optional[float] = None
lexorank: Optional[str] = None
# endregion
# region Request
class CreateServiceRequest(BaseSchema):
entity: CreateServiceSchema
class UpdateServiceRequest(BaseSchema):
entity: UpdateServiceSchema
# endregion
# region Response
class GetServicesResponse(BaseSchema):
items: list[ServiceSchema]
class CreateServiceResponse(BaseResponse):
entity: ServiceSchema
class UpdateServiceResponse(BaseResponse):
pass
class DeleteServiceResponse(BaseResponse):
pass
# endregion

View File

@ -0,0 +1,59 @@
from typing import Optional
from schemas.base import BaseSchema, BaseResponse
# region Entity
class CreateServiceCategorySchema(BaseSchema):
name: str
deal_service_rank: str
product_service_rank: str
class ServiceCategorySchema(CreateServiceCategorySchema):
id: int
class UpdateServiceCategorySchema(BaseSchema):
name: Optional[str] = None
deal_service_rank: Optional[str] = None
product_service_rank: Optional[str] = None
# endregion
# region Request
class CreateServiceCategoryRequest(BaseSchema):
entity: CreateServiceCategorySchema
class UpdateServiceCategoryRequest(BaseSchema):
entity: UpdateServiceCategorySchema
# endregion
# region Response
class GetServiceCategoriesResponse(BaseSchema):
items: list[ServiceCategorySchema]
class CreateServiceCategoryResponse(BaseResponse):
entity: ServiceCategorySchema
class UpdateServiceCategoryResponse(BaseResponse):
pass
class DeleteServiceCategoryResponse(BaseResponse):
pass
# endregion

View File

@ -0,0 +1,60 @@
from modules.fulfillment_base.schemas.service import ServiceSchema
from schemas.base import BaseSchema, BaseResponse
# region Entity
class BaseServicesKitSchema(BaseSchema):
name: str
service_type: int
class ServicesKitSchema(BaseServicesKitSchema):
id: int
services: list[ServiceSchema]
class CreateServicesKitSchema(BaseServicesKitSchema):
services: list[ServiceSchema]
class UpdateServicesKitSchema(BaseServicesKitSchema):
services: list[ServiceSchema]
# endregion
# region Request
class CreateServicesKitRequest(BaseSchema):
entity: CreateServicesKitSchema
class UpdateServicesKitRequest(BaseSchema):
entity: UpdateServicesKitSchema
# endregion
# region Response
class GetServicesKitResponse(BaseSchema):
items: list[ServicesKitSchema]
class CreateServicesKitResponse(BaseResponse):
entity: ServicesKitSchema
class UpdateServicesKitResponse(BaseResponse):
pass
class DeleteServicesKitResponse(BaseResponse):
pass
# endregion

View File

@ -0,0 +1,10 @@
from .deal_product import DealProductService as DealProductService
from .deal_service import DealServiceService as DealServiceService
from .product import ProductService as ProductService
from .product_service import ProductServiceService as ProductServiceService
from .service import ServiceModelService as ServiceModelService
from .services_kit import ServicesKitService as ServicesKitService
from .service_category import ServiceCategoryService as ServiceCategoryService
from .barcode_template import BarcodeTemplateService as BarcodeTemplateService
from .barcode_printer_service import BarcodePrinterService as BarcodePrinterService
from .marketplace import MarketplaceService as MarketplaceService

View File

@ -0,0 +1,41 @@
import base64
from io import BytesIO
from sqlalchemy.ext.asyncio import AsyncSession
from modules.fulfillment_base.barcodes_pdf_gen import BarcodePdfGenerator, BarcodeData
from modules.fulfillment_base.models import Product
from modules.fulfillment_base.repositories import ProductRepository
from modules.fulfillment_base.schemas.product import GetProductBarcodePdfRequest
class BarcodePrinterService:
session: AsyncSession
def __init__(self, session: AsyncSession):
self.session = session
async def generate_pdf(
self, request: GetProductBarcodePdfRequest
) -> tuple[str, BytesIO]:
product: Product = await ProductRepository(self.session).get_by_id(
request.product_id
)
barcode_data: BarcodeData = {
"barcode": request.barcode,
"template": product.barcode_template,
"product": product,
"num_duplicates": request.quantity,
}
filename = f"{product.id}_barcode.pdf"
size = product.barcode_template.size
generator = BarcodePdfGenerator(size.width, size.height)
return filename, generator.generate([barcode_data])
async def generate_base64(
self, request: GetProductBarcodePdfRequest
) -> tuple[str, str]:
filename, pdf_buffer = await self.generate_pdf(request)
base64_string = base64.b64encode(pdf_buffer.read()).decode("utf-8")
return filename, base64_string

View File

@ -0,0 +1,36 @@
from modules.fulfillment_base.models import BarcodeTemplate
from modules.fulfillment_base.repositories import BarcodeTemplateRepository
from modules.fulfillment_base.schemas.barcode_template import *
from services.mixins import *
class BarcodeTemplateService(
ServiceCrudMixin[
BarcodeTemplate,
BarcodeTemplateSchema,
CreateBarcodeTemplateRequest,
UpdateBarcodeTemplateRequest,
]
):
schema_class = BarcodeTemplateSchema
entity_deleted_msg = "Шаблон штрихкода успешно удален"
entity_updated_msg = "Шаблон штрихкода успешно обновлен"
entity_created_msg = "Шаблон штрихкода успешно создан"
def __init__(self, session: AsyncSession):
self.repository = BarcodeTemplateRepository(session)
async def get_attributes(self) -> GetBarcodeAttributesResponse:
attributes = await self.repository.get_attributes()
return GetBarcodeAttributesResponse(
items=[
BarcodeTemplateAttributeSchema.model_validate(attr)
for attr in attributes
]
)
async def get_sizes(self) -> GetBarcodeTemplateSizesResponse:
sizes = await self.repository.get_sizes()
return GetBarcodeTemplateSizesResponse(
items=[BarcodeTemplateSizeSchema.model_validate(size) for size in sizes]
)

View File

@ -0,0 +1,37 @@
from sqlalchemy.ext.asyncio import AsyncSession
from modules.fulfillment_base.models import DealProduct
from modules.fulfillment_base.repositories import DealProductRepository
from modules.fulfillment_base.schemas.deal_product import *
from services.mixins import ServiceGetAllMixin
class DealProductService(ServiceGetAllMixin[DealProduct, DealProductSchema]):
schema_class = DealProductSchema
def __init__(self, session: AsyncSession):
self.repository = DealProductRepository(session)
async def create(
self, request: CreateDealProductRequest
) -> CreateDealProductResponse:
await self.repository.create(request.entity)
deal_product = await self.repository.get_by_id(
request.entity.deal_id, request.entity.product_id
)
return CreateDealProductResponse(
entity=DealProductSchema.model_validate(deal_product),
message="Товар добавлен в сделку",
)
async def update(
self, deal_id: int, product_id: int, data: UpdateDealProductRequest
) -> UpdateDealProductResponse:
entity = await self.repository.get_by_id(deal_id, product_id)
await self.repository.update(entity, data.entity)
return UpdateDealProductResponse(message="Товар сделки обновлен")
async def delete(self, deal_id: int, product_id: int) -> DeleteDealProductResponse:
entity = await self.repository.get_by_id(deal_id, product_id)
await self.repository.delete(entity)
return DeleteDealProductResponse(message="Товар удален из сделки")

View File

@ -0,0 +1,53 @@
from sqlalchemy.ext.asyncio import AsyncSession
from modules.fulfillment_base.models import DealService
from modules.fulfillment_base.repositories import (
DealServiceRepository,
ServicesKitRepository,
)
from modules.fulfillment_base.schemas.deal_service import *
from repositories import DealRepository
from services.mixins import ServiceGetAllMixin
class DealServiceService(ServiceGetAllMixin[DealService, DealServiceSchema]):
schema_class = DealServiceSchema
def __init__(self, session: AsyncSession):
self.repository = DealServiceRepository(session)
async def create(
self, request: CreateDealServiceRequest
) -> CreateDealServiceResponse:
await self.repository.create(request.entity)
deal_service = await self.repository.get_by_id(
request.entity.deal_id, request.entity.service_id
)
return CreateDealServiceResponse(
entity=DealServiceSchema.model_validate(deal_service),
message="Услуга добавлена в сделку",
)
async def update(
self, deal_id: int, service_id: int, data: UpdateDealServiceRequest
) -> UpdateDealServiceResponse:
entity = await self.repository.get_by_id(deal_id, service_id)
await self.repository.update(entity, data.entity)
return UpdateDealServiceResponse(message="Услуга сделки обновлена")
async def delete(self, deal_id: int, service_id: int) -> DeleteDealServiceResponse:
entity = await self.repository.get_by_id(deal_id, service_id)
await self.repository.delete(entity)
return DeleteDealServiceResponse(message="Услуга удалена из сделки")
async def add_services_kit(self, request: DealAddKitRequest) -> DealAddKitResponse:
services_kit_repo = ServicesKitRepository(self.repository.session)
services_kit = await services_kit_repo.get_by_id(request.kit_id)
deal_repo = DealRepository(self.repository.session)
deal = await deal_repo.get_by_id(request.deal_id)
await self.repository.delete_deal_services(request.deal_id)
await self.repository.add_services_kit(deal, services_kit)
return DealAddKitResponse(message="Комплект добавлен в сделку")

View File

@ -0,0 +1,27 @@
from modules.fulfillment_base.models import Marketplace
from modules.fulfillment_base.repositories import MarketplaceRepository
from modules.fulfillment_base.schemas.marketplace import *
from services.mixins import *
class MarketplaceService(
ServiceCrudMixin[
Marketplace,
MarketplaceSchema,
CreateMarketplaceRequest,
UpdateMarketplaceRequest,
]
):
schema_class = MarketplaceSchema
entity_deleted_msg = "Маркетплейс успешно удален"
entity_updated_msg = "Маркетплейс успешно обновлен"
entity_created_msg = "Маркетплейс успешно создан"
def __init__(self, session: AsyncSession):
self.repository = MarketplaceRepository(session)
async def get_base_marketplaces(self) -> GetBaseMarketplacesResponse:
mps = await self.repository.get_base_marketplaces()
return GetBaseMarketplacesResponse(
items=[BaseMarketplaceSchema.model_validate(mp) for mp in mps]
)

View File

@ -0,0 +1,50 @@
import math
from modules.fulfillment_base.models import Product
from modules.fulfillment_base.repositories import ProductRepository
from modules.fulfillment_base.schemas.product import (
CreateProductRequest,
ProductSchema,
UpdateProductRequest, GetProductsResponse,
)
from schemas.base import PaginationSchema, PaginationInfoSchema
from services.mixins import *
class ProductService(
ServiceCreateMixin[Product, CreateProductRequest, ProductSchema],
ServiceUpdateMixin[Product, UpdateProductRequest],
ServiceDeleteMixin[Product],
):
schema_class = ProductSchema
entity_deleted_msg = "Товар успешно удален"
entity_updated_msg = "Товар успешно обновлен"
entity_created_msg = "Товар успешно создан"
def __init__(self, session: AsyncSession):
self.repository = ProductRepository(session)
async def get_all(
self,
pagination: PaginationSchema,
*filters,
) -> GetProductsResponse:
products, total_items = await self.repository.get_all(
pagination.page,
pagination.items_per_page,
*filters,
)
total_pages = 1
if pagination.items_per_page:
total_pages = math.ceil(total_items / pagination.items_per_page)
return GetProductsResponse(
items=[ProductSchema.model_validate(product) for product in products],
pagination_info=PaginationInfoSchema(
total_pages=total_pages, total_items=total_items
),
)
async def is_soft_delete(self, product: ProductSchema) -> bool:
return True

View File

@ -0,0 +1,81 @@
from sqlalchemy.ext.asyncio import AsyncSession
from modules.fulfillment_base.models import DealProductService
from modules.fulfillment_base.repositories import (
ProductServiceRepository,
ServicesKitRepository,
DealProductRepository,
)
from modules.fulfillment_base.schemas.product_service import *
class ProductServiceService:
schema_class = ProductServiceSchema
def __init__(self, session: AsyncSession):
self.repository = ProductServiceRepository(session)
async def create(
self, request: CreateProductServiceRequest
) -> CreateProductServiceResponse:
await self.repository.create(request.entity)
deal_product = await self.repository.get_by_id(
request.entity.deal_id,
request.entity.product_id,
request.entity.service_id,
)
return CreateProductServiceResponse(
entity=ProductServiceSchema.model_validate(deal_product),
message="Услуга добавлена к товару",
)
async def update(
self,
deal_id: int,
product_id: int,
service_id: int,
data: UpdateProductServiceRequest,
) -> UpdateProductServiceResponse:
entity = await self.repository.get_by_id(deal_id, product_id, service_id)
await self.repository.update(entity, data.entity)
return UpdateProductServiceResponse(message="Услуга обновлена")
async def delete(
self, deal_id: int, product_id: int, service_id: int
) -> DeleteProductServiceResponse:
entity = await self.repository.get_by_id(deal_id, product_id, service_id)
await self.repository.delete(entity)
return DeleteProductServiceResponse(message="Товар удален из сделки")
async def duplicate_product_services(
self, request: ProductServicesDuplicateRequest
) -> ProductServicesDuplicateResponse:
services_to_copy: list[
DealProductService
] = await self.repository.get_product_services(
request.deal_id, request.source_deal_product_id
)
await self.repository.duplicate_services(
request.deal_id, request.target_deal_product_ids, services_to_copy
)
return ProductServicesDuplicateResponse(message="Услуги продублированы")
async def add_services_kit(
self, request: DealProductAddKitRequest
) -> DealProductAddKitResponse:
services_kit_repo = ServicesKitRepository(self.repository.session)
services_kit = await services_kit_repo.get_by_id(request.kit_id)
deal_product_repo = DealProductRepository(self.repository.session)
deal_product = await deal_product_repo.get_by_id(
request.deal_id, request.product_id
)
await self.repository.delete_product_services(
request.deal_id, [request.product_id]
)
await self.repository.add_services_kit(deal_product, services_kit)
return DealProductAddKitResponse(message="Комплект добавлен в товар")

View File

@ -0,0 +1,26 @@
from modules.fulfillment_base.models import Service
from modules.fulfillment_base.repositories import ServiceRepository
from modules.fulfillment_base.schemas.service import (
ServiceSchema,
CreateServiceRequest,
UpdateServiceRequest,
)
from services.mixins import *
class ServiceModelService(
ServiceGetAllMixin[Service, ServiceSchema],
ServiceCreateMixin[Service, CreateServiceRequest, ServiceSchema],
ServiceUpdateMixin[Service, UpdateServiceRequest],
ServiceDeleteMixin[Service],
):
schema_class = ServiceSchema
entity_deleted_msg = "Услуга успешно удалена"
entity_updated_msg = "Услуга успешно обновлена"
entity_created_msg = "Услуга успешно создана"
def __init__(self, session: AsyncSession):
self.repository = ServiceRepository(session)
async def is_soft_delete(self, service: ServiceSchema) -> bool:
return True

View File

@ -0,0 +1,28 @@
from modules.fulfillment_base.models import ServiceCategory
from modules.fulfillment_base.repositories import ServiceCategoryRepository
from modules.fulfillment_base.schemas.service_category import (
ServiceCategorySchema,
CreateServiceCategoryRequest,
UpdateServiceCategoryRequest,
)
from services.mixins import *
class ServiceCategoryService(
ServiceGetAllMixin[ServiceCategory, ServiceCategorySchema],
ServiceCreateMixin[
ServiceCategory, CreateServiceCategoryRequest, ServiceCategorySchema
],
ServiceUpdateMixin[ServiceCategory, UpdateServiceCategoryRequest],
ServiceDeleteMixin[ServiceCategory],
):
schema_class = ServiceCategorySchema
entity_deleted_msg = "Категория услуг успешно удалена"
entity_updated_msg = "Категория услуг успешно обновлена"
entity_created_msg = "Категория услуг успешно создана"
def __init__(self, session: AsyncSession):
self.repository = ServiceCategoryRepository(session)
async def is_soft_delete(self, service: ServiceCategorySchema) -> bool:
return True

View File

@ -0,0 +1,23 @@
from modules.fulfillment_base.models.service import ServicesKit
from modules.fulfillment_base.repositories import ServicesKitRepository
from modules.fulfillment_base.schemas.services_kit import (
ServicesKitSchema,
CreateServicesKitRequest,
UpdateServicesKitRequest,
)
from services.mixins import *
class ServicesKitService(
ServiceCrudMixin[ServicesKit, ServicesKitSchema, CreateServicesKitRequest, UpdateServicesKitRequest]
):
schema_class = ServicesKitSchema
entity_deleted_msg = "Набор услуг успешно удален"
entity_updated_msg = "Набор услуг успешно обновлен"
entity_created_msg = "Набор услуг успешно создан"
def __init__(self, session: AsyncSession):
self.repository = ServicesKitRepository(session)
async def is_soft_delete(self, service: ServicesKitSchema) -> bool:
return False

View File

@ -17,6 +17,14 @@ dependencies = [
"uvicorn[standard]>=0.35.0", "uvicorn[standard]>=0.35.0",
"fastapi-endpoints @ git+https://github.com/vladNed/fastapi-endpoints.git@main", "fastapi-endpoints @ git+https://github.com/vladNed/fastapi-endpoints.git@main",
"uvicorn-worker>=0.3.0", "uvicorn-worker>=0.3.0",
"aioboto3>=15.4.0",
"pymupdf>=1.26.5",
"pdfrw>=0.4",
"fpdf>=1.7.2",
"reportlab>=4.4.4",
"pathlib>=1.0.1",
"starlette>=0.47.2",
"lexorank>=1.0.1",
] ]
[dependency-groups] [dependency-groups]

View File

@ -1,4 +1,7 @@
from .board import BoardRepository as BoardRepository from .board import BoardRepository as BoardRepository
from .built_in_module import BuiltInModuleRepository as BuiltInModuleRepository
from .deal import DealRepository as DealRepository from .deal import DealRepository as DealRepository
from .deal_group import DealGroupRepository as DealGroupRepository
from .deal_tag import DealTagRepository as DealTagRepository
from .project import ProjectRepository as ProjectRepository from .project import ProjectRepository as ProjectRepository
from .status import StatusRepository as StatusRepository from .status import StatusRepository as StatusRepository

View File

@ -1,6 +1,15 @@
from sqlalchemy import Select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
class BaseRepository: class BaseRepository:
session: AsyncSession
def __init__(self, session: AsyncSession): def __init__(self, session: AsyncSession):
self.session = session self.session = session
@staticmethod
def _apply_pagination(query: Select, page: int, items_per_page: int) -> Select:
offset = (page - 1) * items_per_page
query = query.offset(offset).limit(items_per_page)
return query

View File

@ -1,30 +1,20 @@
from typing import Optional from sqlalchemy.orm import selectinload
from sqlalchemy import select
from models import Board from models import Board
from repositories.base import BaseRepository from repositories.mixins import *
from schemas.board import UpdateBoardSchema from schemas.board import UpdateBoardSchema, CreateBoardSchema
class BoardRepository(BaseRepository): class BoardRepository(RepCrudMixin[Board, CreateBoardSchema, UpdateBoardSchema]):
async def get_all(self, project_id: int) -> list[Board]: entity_class = Board
stmt = select(Board).where( entity_not_found_msg = "Доска не найдена"
Board.is_deleted.is_(False), Board.project_id == project_id
)
result = await self.session.execute(stmt)
return list(result.scalars().all())
async def get_by_id(self, board_id: int) -> Optional[Board]: def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select:
stmt = select(Board).where(Board.id == board_id, Board.is_deleted.is_(False)) project_id = args[0]
result = await self.session.execute(stmt) return stmt.where(Board.project_id == project_id).order_by(Board.lexorank)
return result.scalar_one_or_none()
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
return stmt.options(selectinload(Board.deals))
async def update(self, board: Board, data: UpdateBoardSchema) -> Board: async def update(self, board: Board, data: UpdateBoardSchema) -> Board:
board.lexorank = data.lexorank if data.lexorank else board.lexorank return await self._apply_update_data_to_model(board, data, True)
board.name = data.name if data.name else board.name
self.session.add(board)
await self.session.commit()
await self.session.refresh(board)
return board

View File

@ -0,0 +1,18 @@
from models import Board, BuiltInModule
from repositories.mixins import *
class BuiltInModuleRepository(
BaseRepository,
RepGetAllMixin[BuiltInModule],
):
entity_class = BuiltInModule
def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select:
project_id = args[0]
return stmt.where(Board.project_id == project_id).order_by(Board.lexorank)
async def get_by_ids(self, ids: list[int]) -> list[BuiltInModule]:
stmt = select(BuiltInModule).where(BuiltInModule.id.in_(ids))
built_in_modules = await self.session.scalars(stmt)
return built_in_modules.all()

View File

@ -1,27 +1,172 @@
from typing import Optional from sqlalchemy import func
from sqlalchemy.orm import joinedload, selectinload
from sqlalchemy import select from models import Deal, Board, DealStatusHistory
from modules.fulfillment_base.models import (
from models import Deal DealService,
from repositories.base import BaseRepository Service,
from schemas.deal import UpdateDealSchema DealProductService,
DealProduct,
)
from repositories.mixins import *
from schemas.base import SortDir
from schemas.deal import UpdateDealSchema, CreateDealSchema
from utils.sorting import apply_sorting
class DealRepository(BaseRepository): class DealRepository(
async def get_all(self, board_id: int) -> list[Deal]: BaseRepository,
stmt = select(Deal).where(Deal.is_deleted.is_(False), Deal.board_id == board_id) RepDeleteMixin[Deal],
RepCreateMixin[Deal, CreateDealSchema],
RepUpdateMixin[Deal, UpdateDealSchema],
RepGetByIdMixin[Deal],
):
entity_class = Deal
entity_not_found_msg = "Сделка не найдена"
def _get_price_subquery(self):
deal_services_subquery = (
select(
DealService.deal_id,
func.sum(DealService.quantity * DealService.price).label("total_price"),
)
.join(Service)
.group_by(DealService.deal_id)
)
product_services_subquery = select(
select(
DealProductService.deal_id,
func.sum(DealProduct.quantity * DealProductService.price).label(
"total_price"
),
)
.join(DealProduct)
.group_by(DealProductService.deal_id)
.subquery()
)
union_subqueries = deal_services_subquery.union_all(
product_services_subquery
).subquery()
final_subquery = (
select(
union_subqueries.c.deal_id,
func.sum(union_subqueries.c.total_price).label("total_price"),
)
.group_by(union_subqueries.c.deal_id)
.subquery()
)
return final_subquery
def _get_products_quantity_subquery(self):
return (
select(
DealProduct.deal_id,
func.sum(DealProduct.quantity).label("products_quantity"),
)
.group_by(DealProduct.deal_id)
.subquery()
)
async def get_all(
self,
page: Optional[int],
items_per_page: Optional[int],
field: Optional[str],
direction: Optional[SortDir],
project_id: Optional[int],
board_id: Optional[int],
status_id: Optional[int],
id: Optional[int],
name: Optional[str],
) -> tuple[list[tuple[Deal, int, int]], int]:
price_subquery = self._get_price_subquery()
products_quantity_subquery = self._get_products_quantity_subquery()
stmt = (
select(
Deal,
func.coalesce(price_subquery.c.total_price, 0),
func.coalesce(products_quantity_subquery.c.products_quantity, 0),
)
.outerjoin(
price_subquery,
Deal.id == price_subquery.c.deal_id,
)
.outerjoin(
products_quantity_subquery,
Deal.id == products_quantity_subquery.c.deal_id,
)
.options(
joinedload(Deal.status),
joinedload(Deal.board),
selectinload(Deal.group),
selectinload(Deal.tags),
)
.where(Deal.is_deleted.is_(False))
)
if id:
stmt = stmt.where(Deal.id == id)
if project_id:
stmt = stmt.join(Board).where(Board.project_id == project_id)
if board_id:
stmt = stmt.where(Deal.board_id == board_id)
if status_id:
stmt = stmt.where(Deal.status_id == status_id)
if name:
stmt = stmt.where(Deal.name.ilike(f"%{name}%"))
total_items = len((await self.session.execute(stmt)).all())
if field and direction is not None:
stmt = apply_sorting(stmt, Deal, field, direction)
else:
stmt = stmt.order_by(Deal.lexorank)
if page and items_per_page:
stmt = self._apply_pagination(stmt, page, items_per_page)
rows: list[tuple[Deal, int, int]] = (await self.session.execute(stmt)).all()
return rows, total_items
async def get_by_group_id(self, group_id: int) -> list[Deal]:
stmt = (
select(Deal)
.where(Deal.group_id == group_id, Deal.is_deleted.is_(False))
.options(joinedload(Deal.status), joinedload(Deal.board))
)
result = await self.session.execute(stmt) result = await self.session.execute(stmt)
return list(result.scalars().all()) return list(result.scalars().all())
async def get_by_id(self, deal_id: int) -> Optional[Deal]: async def get_by_ids(self, deal_ids: list[int]) -> list[Deal]:
stmt = select(Deal).where(Deal.id == deal_id, Deal.is_deleted.is_(False)) stmt = (
select(Deal)
.where(Deal.id.in_(deal_ids), Deal.is_deleted.is_(False))
.options(joinedload(Deal.status), joinedload(Deal.board))
)
result = await self.session.execute(stmt) result = await self.session.execute(stmt)
return result.scalar_one_or_none() return list(result.scalars().all())
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
return stmt.options(joinedload(Deal.status), joinedload(Deal.board))
async def update_status(self, deal: Deal, status_id: int):
if deal.status_id == status_id:
return
deal.status_history.append(
DealStatusHistory(
from_status_id=deal.status_id,
to_status_id=status_id,
)
)
deal.status_id = status_id
async def update(self, deal: Deal, data: UpdateDealSchema) -> Deal: async def update(self, deal: Deal, data: UpdateDealSchema) -> Deal:
deal.lexorank = data.lexorank if data.lexorank else deal.lexorank fields = ["lexorank", "name", "board_id"]
deal.name = data.name if data.name else deal.name deal = await self._apply_update_data_to_model(deal, data, False, fields)
deal.status_id = data.status_id if data.status_id else deal.status_id
if data.status_id:
await self.update_status(deal, data.status_id)
self.session.add(deal) self.session.add(deal)
await self.session.commit() await self.session.commit()

View File

@ -0,0 +1,54 @@
from models import DealGroup, Deal
from repositories import DealRepository
from repositories.mixins import *
from schemas.deal_group import UpdateDealGroupSchema
class DealGroupRepository(
BaseRepository,
RepGetByIdMixin[DealGroup],
RepUpdateMixin[DealGroup, UpdateDealGroupSchema],
):
entity_class = DealGroup
async def create(self, deals: list[Deal], lexorank: str) -> DealGroup:
group = DealGroup(deals=deals, lexorank=lexorank)
self.session.add(group)
await self.session.flush()
await self.session.refresh(group)
group.name = "Группа ID: " + str(group.id)
await self.session.commit()
return group
async def update(self, entity: DealGroup, data: UpdateDealGroupSchema) -> DealGroup:
if data.status_id:
deal_repo = DealRepository(self.session)
deals = await deal_repo.get_by_group_id(entity.id)
for deal in deals:
await deal_repo.update_status(deal, data.status_id)
del data.status_id
return await self._apply_update_data_to_model(entity, data, True)
async def update_group_deals(
self, group_id: int, old_deals: list[Deal], new_deals: list[Deal]
):
old_set = set(old_deals)
new_set = set(new_deals)
deals_to_remove = old_set - new_set
deals_to_add = new_set - old_set
for deal in deals_to_remove:
deal.group_id = None
for deal in deals_to_add:
deal.group_id = group_id
self.session.add_all([*deals_to_remove, *deals_to_add])
await self.session.commit()
async def delete(self, group_id: int) -> None:
deal_repo = DealRepository(self.session)
deals = await deal_repo.get_by_group_id(group_id)
for deal in deals:
deal.is_deleted = True
self.session.add(deal)
await self.session.commit()

62
repositories/deal_tag.py Normal file
View File

@ -0,0 +1,62 @@
from models import DealTag, DealTagColor, Deal
from models.project import Project
from repositories.mixins import *
from schemas.deal_tag import *
class DealTagRepository(
RepCrudMixin[DealTag, CreateDealTagSchema, UpdateDealTagSchema]
):
session: AsyncSession
entity_class = DealTag
entity_not_found_msg = "Тег не найден"
def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select:
project_id = args[0]
return stmt.where(
DealTag.project_id == project_id, DealTag.is_deleted.is_(False)
).order_by(DealTag.id)
async def _get_tag_color(self, tag_id: int) -> DealTagColor:
stmt = select(DealTagColor).where(DealTagColor.id == tag_id)
result = await self.session.execute(stmt)
tag = result.one_or_none()
if not tag:
raise ObjectNotFoundException("Цвет тега не найден")
return tag[0]
async def update(self, deal_tag: DealTag, data: UpdateDealTagSchema) -> Project:
if data.tag_color is not None:
data.tag_color = await self._get_tag_color(data.tag_color.id)
return await self._apply_update_data_to_model(deal_tag, data, True)
async def switch_tag_in_deal(self, tag: DealTag, deal: Deal):
if tag in deal.tags:
deal.tags.remove(tag)
else:
deal.tags.append(tag)
await self.session.commit()
async def switch_tag_in_deals(self, tag: DealTag, deals: list[Deal]):
for deal in deals:
if tag in deal.tags:
deal.tags.remove(tag)
else:
deal.tags.append(tag)
await self.session.commit()
async def get_tag_colors(self) -> list[DealTagColor]:
stmt = select(DealTagColor)
result = await self.session.execute(stmt)
return list(result.scalars().all())
async def sync_deals_tags(self, deals: list[Deal]):
tags_set: set[DealTag] = set()
for deal in deals:
for tag in deal.tags:
tags_set.add(tag)
tags: list[DealTag] = list(tags_set)
for deal in deals:
deal.tags = tags

140
repositories/mixins.py Normal file
View File

@ -0,0 +1,140 @@
from typing import Type, Optional, TypeVar, Generic
from sqlalchemy import select, Select
from sqlalchemy.ext.asyncio import AsyncSession
from repositories.base import BaseRepository
from schemas.base import BaseSchema
from utils.exceptions import ObjectNotFoundException
EntityType = TypeVar("EntityType")
CreateSchemaType = TypeVar("CreateSchemaType", bound=BaseSchema)
UpdateSchemaType = TypeVar("UpdateSchemaType", bound=BaseSchema)
class RepBaseMixin(Generic[EntityType]):
session: AsyncSession
class RepDeleteMixin(Generic[EntityType], RepBaseMixin[EntityType]):
async def _before_delete(self, obj: EntityType) -> None:
pass
async def delete(self, obj: EntityType, is_soft: bool) -> None:
await self._before_delete(obj)
if not is_soft:
await self.session.delete(obj)
await self.session.commit()
return
if not hasattr(obj, "is_deleted"):
raise AttributeError(
f"{obj.__class__.__name__} does not support soft delete (missing is_deleted field)"
)
obj.is_deleted = True
self.session.add(obj)
await self.session.commit()
class RepCreateMixin(Generic[EntityType, CreateSchemaType], RepBaseMixin[EntityType]):
entity_class: Type[EntityType]
async def _prepare_create(self, data: CreateSchemaType) -> dict:
return data.model_dump()
async def _after_create(self, obj: EntityType, data: CreateSchemaType) -> None:
pass
async def create(self, data: CreateSchemaType) -> int:
prepared_data = await self._prepare_create(data)
obj = self.entity_class(**prepared_data)
self.session.add(obj)
await self.session.flush()
await self.session.refresh(obj)
await self._after_create(obj, data)
await self.session.commit()
await self.session.refresh(obj)
return obj.id
class RepUpdateMixin(Generic[EntityType, UpdateSchemaType], RepBaseMixin[EntityType]):
async def _apply_update_data_to_model(
self,
model: EntityType,
data: UpdateSchemaType,
with_commit: Optional[bool] = False,
fields: Optional[list[str]] = None,
) -> EntityType:
if fields is None:
fields = data.model_dump().keys()
for field in fields:
value = getattr(data, field)
if value is not None:
setattr(model, field, value)
if with_commit:
self.session.add(model)
await self.session.commit()
await self.session.refresh(model)
return model
async def update(self, entity: EntityType, data: UpdateSchemaType) -> EntityType:
return await self._apply_update_data_to_model(entity, data, True)
class RepGetByIdMixin(Generic[EntityType], RepBaseMixin[EntityType]):
entity_class: Type[EntityType]
entity_not_found_msg = "Entity not found"
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
return stmt
async def get_by_id(
self, item_id: int, raise_if_not_found: Optional[bool] = True
) -> Optional[EntityType]:
stmt = select(self.entity_class).where(self.entity_class.id == item_id)
if hasattr(self, "is_deleted"):
stmt = stmt.where(self.entity_class.is_deleted.is_(False))
stmt = self._process_get_by_id_stmt(stmt)
result = (await self.session.execute(stmt)).scalar_one_or_none()
if result is None and raise_if_not_found:
raise ObjectNotFoundException(self.entity_not_found_msg)
return result
class RepGetAllMixin(Generic[EntityType], RepBaseMixin[EntityType]):
entity_class: Type[EntityType]
def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select:
return stmt
def _process_get_all_stmt(self, stmt: Select) -> Select:
return stmt
async def get_all(self, *args) -> list[EntityType]:
stmt = select(self.entity_class)
if hasattr(self, "is_deleted"):
stmt = stmt.where(self.entity_class.is_deleted.is_(False))
if args:
stmt = self._process_get_all_stmt_with_args(stmt, *args)
else:
stmt = self._process_get_all_stmt(stmt)
result = await self.session.execute(stmt)
return list(result.scalars().all())
class RepCrudMixin(
Generic[EntityType, CreateSchemaType, UpdateSchemaType],
BaseRepository,
RepGetAllMixin[EntityType],
RepCreateMixin[EntityType, CreateSchemaType],
RepUpdateMixin[EntityType, UpdateSchemaType],
RepGetByIdMixin[EntityType],
RepDeleteMixin[EntityType],
):
pass

View File

@ -1,11 +1,36 @@
from sqlalchemy import select from sqlalchemy.orm import selectinload
from models import DealTag
from models.project import Project from models.project import Project
from repositories.base import BaseRepository from repositories.built_in_module import BuiltInModuleRepository
from repositories.mixins import *
from schemas.project import CreateProjectSchema, UpdateProjectSchema
class ProjectRepository(BaseRepository): class ProjectRepository(
async def get_all(self) -> list[Project]: RepCrudMixin[Project, CreateProjectSchema, UpdateProjectSchema]
stmt = select(Project).where(Project.is_deleted.is_(False)) ):
result = await self.session.execute(stmt) entity_class = Project
return list(result.scalars().all()) entity_not_found_msg = "Проект не найден"
def _apply_options(self, stmt: Select) -> Select:
return stmt.options(
selectinload(Project.boards),
selectinload(Project.tags).joinedload(DealTag.tag_color),
)
def _process_get_all_stmt(self, stmt: Select) -> Select:
return self._apply_options(stmt).order_by(Project.id)
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
return self._apply_options(stmt)
async def update(self, project: Project, data: UpdateProjectSchema) -> Project:
if data.built_in_modules is not None:
built_in_modules = data.built_in_modules
module_ids = [module.id for module in built_in_modules]
data.built_in_modules = await BuiltInModuleRepository(
self.session
).get_by_ids(module_ids)
return await self._apply_update_data_to_model(project, data, True)

View File

@ -1,32 +1,40 @@
from typing import Optional from sqlalchemy import func
from sqlalchemy import select from models import Status, Deal, DealStatusHistory
from repositories.mixins import *
from models import Status from schemas.status import UpdateStatusSchema, CreateStatusSchema
from repositories.base import BaseRepository
from schemas.status import UpdateStatusSchema
class StatusRepository(BaseRepository): class StatusRepository(RepCrudMixin[Status, CreateStatusSchema, UpdateStatusSchema]):
entity_class = Status
entity_not_found_msg = "Статус не найден"
def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select:
board_id = args[0]
return stmt.where(Status.board_id == board_id).order_by(Status.lexorank)
async def get_all(self, board_id: int) -> list[Status]: async def get_all(self, board_id: int) -> list[Status]:
stmt = select(Status).where( stmt = (
Status.is_deleted.is_(False), Status.board_id == board_id select(Status)
.where(Status.is_deleted.is_(False), Status.board_id == board_id)
.order_by(Status.lexorank)
) )
result = await self.session.execute(stmt) result = await self.session.execute(stmt)
return list(result.scalars().all()) return list(result.scalars().all())
async def get_by_id(self, status_id: int) -> Optional[Status]: async def get_deals_count(self, status_id: int) -> int:
stmt = select(Status).where( stmt = select(func.count(Deal.id)).where(Deal.status_id == status_id)
Status.id == status_id, Status.is_deleted.is_(False)
)
result = await self.session.execute(stmt) result = await self.session.execute(stmt)
return result.scalar_one_or_none() return result.scalar()
async def update(self, status: Status, data: UpdateStatusSchema) -> Status: async def update(self, status: Status, data: UpdateStatusSchema) -> Status:
status.lexorank = data.lexorank if data.lexorank else status.lexorank return await self._apply_update_data_to_model(status, data, True)
status.name = data.name if data.name else status.name
self.session.add(status) async def get_status_history(self, deal_id: int) -> list[DealStatusHistory]:
await self.session.commit() stmt = (
await self.session.refresh(status) select(DealStatusHistory)
return status .where(DealStatusHistory.deal_id == deal_id)
.order_by(DealStatusHistory.created_at)
)
result = await self.session.execute(stmt)
return list(result.scalars().all())

View File

@ -1,34 +0,0 @@
from fastapi import APIRouter, Path
from backend.dependecies import SessionDependency
from schemas.board import GetBoardsResponse, UpdateBoardRequest, UpdateBoardResponse
from services import BoardService
board_router = APIRouter(
tags=["board"],
)
@board_router.get(
"/{projectId}",
response_model=GetBoardsResponse,
operation_id="get_boards",
)
async def get_boards(
session: SessionDependency,
project_id: int = Path(alias="projectId"),
):
return await BoardService(session).get_boards(project_id)
@board_router.patch(
"/{boardId}",
response_model=UpdateBoardResponse,
operation_id="update_board",
)
async def update_board(
session: SessionDependency,
request: UpdateBoardRequest,
board_id: int = Path(alias="boardId"),
):
return await BoardService(session).update_board(board_id, request)

0
routers/crm/__init__.py Normal file
View File

View File

56
routers/crm/v1/board.py Normal file
View File

@ -0,0 +1,56 @@
from fastapi import APIRouter, Path
from backend.dependecies import SessionDependency
from schemas.board import *
from services import BoardService
router = APIRouter(tags=["board"])
@router.get(
"/{projectId}",
response_model=GetBoardsResponse,
operation_id="get_boards",
)
async def get_boards(
session: SessionDependency,
project_id: int = Path(alias="projectId"),
):
return await BoardService(session).get_all(project_id)
@router.post(
"/",
response_model=CreateBoardResponse,
operation_id="create_board",
)
async def create_board(
session: SessionDependency,
request: CreateBoardRequest,
):
return await BoardService(session).create(request)
@router.patch(
"/{pk}",
response_model=UpdateBoardResponse,
operation_id="update_board",
)
async def update_board(
session: SessionDependency,
request: UpdateBoardRequest,
pk: int = Path(),
):
return await BoardService(session).update(pk, request)
@router.delete(
"/{pk}",
response_model=DeleteBoardResponse,
operation_id="delete_board",
)
async def delete_board(
session: SessionDependency,
pk: int = Path(),
):
return await BoardService(session).delete(pk)

74
routers/crm/v1/deal.py Normal file
View File

@ -0,0 +1,74 @@
from fastapi import APIRouter, Path, Query
from backend.dependecies import (
SessionDependency,
PaginationDependency,
SortingDependency,
)
from schemas.deal import *
from services import DealService
router = APIRouter(tags=["deal"])
@router.get(
"/",
response_model=GetDealsResponse,
operation_id="get_deals",
)
async def get_deals(
session: SessionDependency,
pagination: PaginationDependency,
sorting: SortingDependency,
project_id: Optional[int] = Query(alias="projectId", default=None),
board_id: Optional[int] = Query(alias="boardId", default=None),
status_id: Optional[int] = Query(alias="statusId", default=None),
id: Optional[int] = Query(default=None),
name: Optional[str] = Query(default=None),
):
return await DealService(session).get_all(
pagination,
sorting,
project_id,
board_id,
status_id,
id,
name,
)
@router.post(
"/",
response_model=CreateDealResponse,
operation_id="create_deal",
)
async def create_deal(
session: SessionDependency,
request: CreateDealRequest,
):
return await DealService(session).create(request)
@router.patch(
"/{pk}",
response_model=UpdateDealResponse,
operation_id="update_deal",
)
async def update_deal(
session: SessionDependency,
request: UpdateDealRequest,
pk: int = Path(),
):
return await DealService(session).update(pk, request)
@router.delete(
"/{pk}",
response_model=DeleteDealResponse,
operation_id="delete_deal",
)
async def delete_deal(
session: SessionDependency,
pk: int = Path(),
):
return await DealService(session).delete(pk)

View File

@ -0,0 +1,59 @@
from pathlib import Path
from fastapi import APIRouter
from backend.dependecies import SessionDependency
from schemas.deal_group import *
from services import DealGroupService
router = APIRouter(tags=["deal-group"])
@router.patch(
"/{pk}",
response_model=UpdateDealGroupResponse,
operation_id="update_deal_group",
)
async def update_group(
request: UpdateDealGroupRequest,
session: SessionDependency,
pk: int = Path(),
):
return await DealGroupService(session).update(pk, request)
@router.post(
"/",
response_model=CreateDealGroupResponse,
operation_id="create_deal_group",
)
async def create_group(
request: CreateDealGroupRequest,
session: SessionDependency,
):
return await DealGroupService(session).create(request)
@router.delete(
"/{pk}",
response_model=DeleteDealGroupResponse,
operation_id="delete_deal_group",
)
async def delete_group(
session: SessionDependency,
pk: int = Path(),
):
return await DealGroupService(session).delete(pk)
@router.post(
"/{pk}/deals",
response_model=UpdateDealsInGroupResponse,
operation_id="update_deals_in_group",
)
async def update_deals_in_group(
request: UpdateDealsInGroupRequest,
session: SessionDependency,
pk: int = Path(),
):
return await DealGroupService(session).update_deals_in_group(pk, request)

View File

@ -0,0 +1,81 @@
from pathlib import Path
from fastapi import APIRouter
from backend.dependecies import SessionDependency
from schemas.deal_tag import *
from services import DealTagService
router = APIRouter(tags=["deal-tag"])
@router.get(
"/{projectId}",
response_model=GetDealTagsResponse,
operation_id="get_deal_tags",
)
async def get_deal_tags(
session: SessionDependency,
projectId: int = Path(alias="projectId"),
):
return await DealTagService(session).get_all(projectId)
@router.post(
"/",
operation_id="create_deal_tag",
response_model=CreateDealTagResponse,
)
async def create_deal_tag(
request: CreateDealTagRequest,
session: SessionDependency,
):
return await DealTagService(session).create(request)
@router.patch(
"/{pk}",
operation_id="update_deal_tag",
response_model=UpdateDealTagResponse,
)
async def update_deal_tag(
request: UpdateDealTagRequest,
session: SessionDependency,
pk: int = Path(),
):
return await DealTagService(session).update(pk, request)
@router.delete(
"/{pk}",
response_model=DeleteDealTagResponse,
operation_id="delete_deal_tag",
)
async def delete_deal_tag(
session: SessionDependency,
pk: int = Path(),
):
return await DealTagService(session).delete(pk)
@router.post(
"/switch",
response_model=SwitchDealTagResponse,
operation_id="switch_deal_tag",
)
async def switch_deal_tag(
session: SessionDependency,
request: SwitchDealTagRequest,
):
return await DealTagService(session).switch_tag(request)
@router.post(
"/colors",
response_model=GetTagColorsResponse,
operation_id="get_deal_tag_colors",
)
async def get_deal_tag_colors(
session: SessionDependency,
):
return await DealTagService(session).get_tag_colors()

18
routers/crm/v1/module.py Normal file
View File

@ -0,0 +1,18 @@
from fastapi import APIRouter
from backend.dependecies import SessionDependency
from schemas.module import GetAllBuiltInModulesResponse
from services.built_in_module import BuiltInModuleService
router = APIRouter(tags=["modules"])
@router.get(
"/built-in/",
response_model=GetAllBuiltInModulesResponse,
operation_id="get_built_in_modules",
)
async def get_built_in_modules(
session: SessionDependency,
):
return await BuiltInModuleService(session).get_all()

View File

View File

@ -0,0 +1,56 @@
from fastapi import APIRouter, Path, Query
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,
include_deleted: bool = Query(alias="includeDeleted", default=False),
):
return await ClientService(session).get_all(include_deleted)
@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)

View File

@ -0,0 +1,77 @@
from fastapi import APIRouter, Path
from backend.dependecies import SessionDependency
from modules.fulfillment_base.schemas.barcode_template import *
from modules.fulfillment_base.services import BarcodeTemplateService
router = APIRouter(tags=["barcode_template"])
@router.get(
"/",
response_model=GetBarcodeTemplatesResponse,
operation_id="get_barcode_templates",
)
async def get_barcode_templates(
session: SessionDependency,
):
return await BarcodeTemplateService(session).get_all()
@router.post(
"/",
response_model=CreateBarcodeTemplateResponse,
operation_id="create_barcode_template",
)
async def create_barcode_template(
session: SessionDependency,
request: CreateBarcodeTemplateRequest,
):
return await BarcodeTemplateService(session).create(request)
@router.patch(
"/{pk}",
response_model=UpdateBarcodeTemplateResponse,
operation_id="update_barcode_template",
)
async def update_barcode_template(
session: SessionDependency,
request: UpdateBarcodeTemplateRequest,
pk: int = Path(),
):
return await BarcodeTemplateService(session).update(pk, request)
@router.delete(
"/{pk}",
response_model=DeleteBarcodeTemplateResponse,
operation_id="delete_barcode_template",
)
async def delete_barcode_template(
session: SessionDependency,
pk: int = Path(),
):
return await BarcodeTemplateService(session).delete(pk)
@router.get(
"/attributes",
response_model=GetBarcodeAttributesResponse,
operation_id="get_barcode_template_attributes",
)
async def get_barcode_template_attributes(
session: SessionDependency,
):
return await BarcodeTemplateService(session).get_attributes()
@router.get(
"/sizes",
response_model=GetBarcodeTemplateSizesResponse,
operation_id="get_barcode_template_sizes",
)
async def get_barcode_template_sizes(
session: SessionDependency,
):
return await BarcodeTemplateService(session).get_sizes()

View File

@ -0,0 +1,137 @@
from fastapi import APIRouter, Path
from backend.dependecies import SessionDependency
from modules.fulfillment_base.schemas.deal_product import *
from modules.fulfillment_base.schemas.product_service import *
from modules.fulfillment_base.services import DealProductService, ProductServiceService
router = APIRouter(tags=["deal-product"])
# region DealProduct
@router.get(
"/{dealId}",
response_model=GetDealProductsResponse,
operation_id="get_deal_products",
)
async def get_deal_products(
session: SessionDependency,
deal_id: int = Path(alias="dealId"),
):
return await DealProductService(session).get_all(deal_id)
@router.post(
"/",
response_model=CreateDealProductResponse,
operation_id="create_deal_product",
)
async def create_deal_product(
session: SessionDependency,
request: CreateDealProductRequest,
):
return await DealProductService(session).create(request)
@router.patch(
"/{dealId}/product/{productId}",
response_model=UpdateDealProductResponse,
operation_id="update_deal_product",
)
async def update_deal_product(
session: SessionDependency,
request: UpdateDealProductRequest,
deal_id: int = Path(alias="dealId"),
product_id: int = Path(alias="productId"),
):
return await DealProductService(session).update(deal_id, product_id, request)
@router.delete(
"/{dealId}/product/{productId}",
response_model=DeleteDealProductResponse,
operation_id="delete_deal_product",
)
async def delete_deal_product(
session: SessionDependency,
deal_id: int = Path(alias="dealId"),
product_id: int = Path(alias="productId"),
):
return await DealProductService(session).delete(deal_id, product_id)
# endregion
# region DealProductService
@router.post(
"/service",
response_model=CreateProductServiceResponse,
operation_id="create_deal_product_service",
)
async def create_deal_product_service(
session: SessionDependency,
request: CreateProductServiceRequest,
):
return await ProductServiceService(session).create(request)
@router.patch(
"/{dealId}/product/{productId}/service/{serviceId}",
response_model=UpdateProductServiceResponse,
operation_id="update_deal_product_service",
)
async def update_deal_product_service(
session: SessionDependency,
request: UpdateProductServiceRequest,
deal_id: int = Path(alias="dealId"),
product_id: int = Path(alias="productId"),
service_id: int = Path(alias="serviceId"),
):
return await ProductServiceService(session).update(
deal_id, product_id, service_id, request
)
@router.delete(
"/{dealId}/product/{productId}/service/{serviceId}",
response_model=DeleteProductServiceResponse,
operation_id="delete_deal_product_service",
)
async def delete_deal_product_service(
session: SessionDependency,
deal_id: int = Path(alias="dealId"),
product_id: int = Path(alias="productId"),
service_id: int = Path(alias="serviceId"),
):
return await ProductServiceService(session).delete(deal_id, product_id, service_id)
@router.post(
"/services/duplicate",
response_model=ProductServicesDuplicateResponse,
operation_id="duplicate_product_services",
)
async def copy_product_services(
session: SessionDependency,
request: ProductServicesDuplicateRequest,
):
return await ProductServiceService(session).duplicate_product_services(request)
@router.post(
"/add-services-kit",
response_model=DealProductAddKitResponse,
operation_id="add_kit_to_deal_product",
)
async def add_kit_to_deal_product(
session: SessionDependency,
request: DealProductAddKitRequest,
):
return await ProductServiceService(session).add_services_kit(request)
# endregion

View File

@ -0,0 +1,70 @@
from fastapi import APIRouter, Path
from backend.dependecies import SessionDependency
from modules.fulfillment_base.schemas.deal_service import *
from modules.fulfillment_base.services import DealServiceService
router = APIRouter(tags=["deal-service"])
@router.get(
"/{dealId}",
response_model=GetDealServicesResponse,
operation_id="get_deal_services",
)
async def get_deal_services(
session: SessionDependency,
deal_id: int = Path(alias="dealId"),
):
return await DealServiceService(session).get_all(deal_id)
@router.post(
"/",
response_model=CreateDealServiceResponse,
operation_id="create_deal_service",
)
async def create_deal_service(
session: SessionDependency,
request: CreateDealServiceRequest,
):
return await DealServiceService(session).create(request)
@router.patch(
"/{dealId}/service/{serviceId}",
response_model=UpdateDealServiceResponse,
operation_id="update_deal_service",
)
async def update_deal_service(
session: SessionDependency,
request: UpdateDealServiceRequest,
deal_id: int = Path(alias="dealId"),
service_id: int = Path(alias="serviceId"),
):
return await DealServiceService(session).update(deal_id, service_id, request)
@router.delete(
"/{dealId}/service/{serviceId}",
response_model=DeleteDealServiceResponse,
operation_id="delete_deal_service",
)
async def delete_deal_service(
session: SessionDependency,
deal_id: int = Path(alias="dealId"),
service_id: int = Path(alias="serviceId"),
):
return await DealServiceService(session).delete(deal_id, service_id)
@router.post(
"/add-services-kit",
response_model=DealAddKitResponse,
operation_id="add_kit_to_deal",
)
async def add_kit_to_deal(
session: SessionDependency,
request: DealAddKitRequest,
):
return await DealServiceService(session).add_services_kit(request)

View File

@ -0,0 +1,66 @@
from fastapi import APIRouter, Path
from backend.dependecies import SessionDependency
from modules.fulfillment_base.schemas.marketplace import *
from modules.fulfillment_base.services import MarketplaceService
router = APIRouter(tags=["marketplace"])
@router.get(
"/base",
response_model=GetBaseMarketplacesResponse,
operation_id="get_base_marketplaces",
)
async def get_base_marketplaces(
session: SessionDependency,
):
return await MarketplaceService(session).get_base_marketplaces()
@router.get(
"/{clientId}",
response_model=GetMarketplacesResponse,
operation_id="get_marketplaces",
)
async def get_marketplaces(
session: SessionDependency, client_id: int = Path(alias="clientId")
):
return await MarketplaceService(session).get_all(client_id)
@router.post(
"/",
response_model=CreateMarketplaceResponse,
operation_id="create_marketplace",
)
async def create_product(
session: SessionDependency,
request: CreateMarketplaceRequest,
):
return await MarketplaceService(session).create(request)
@router.patch(
"/{pk}",
response_model=UpdateMarketplaceResponse,
operation_id="update_marketplace",
)
async def update_marketplace(
session: SessionDependency,
request: UpdateMarketplaceRequest,
pk: int = Path(),
):
return await MarketplaceService(session).update(pk, request)
@router.delete(
"/{pk}",
response_model=DeleteMarketplaceResponse,
operation_id="delete_marketplace",
)
async def delete_marketplace(
session: SessionDependency,
pk: int = Path(),
):
return await MarketplaceService(session).delete(pk)

View File

@ -0,0 +1,73 @@
from fastapi import APIRouter, Path, Query
from backend.dependecies import SessionDependency, PaginationDependency
from modules.fulfillment_base.schemas.product import *
from modules.fulfillment_base.services import ProductService, BarcodePrinterService
router = APIRouter(tags=["product"])
@router.get(
"/",
response_model=GetProductsResponse,
operation_id="get_products",
)
async def get_products(
session: SessionDependency,
pagination: PaginationDependency,
client_id: Optional[int] = Query(alias="clientId", default=None),
search_input: Optional[str] = Query(alias="searchInput", default=None),
):
return await ProductService(session).get_all(pagination, client_id, search_input)
@router.post(
"/",
response_model=CreateProductResponse,
operation_id="create_product",
)
async def create_product(
session: SessionDependency,
request: CreateProductRequest,
):
return await ProductService(session).create(request)
@router.patch(
"/{pk}",
response_model=UpdateProductResponse,
operation_id="update_product",
)
async def update_product(
session: SessionDependency,
request: UpdateProductRequest,
pk: int = Path(),
):
return await ProductService(session).update(pk, request)
@router.delete(
"/{pk}",
response_model=DeleteProductResponse,
operation_id="delete_product",
)
async def delete_product(
session: SessionDependency,
pk: int = Path(),
):
return await ProductService(session).delete(pk)
@router.post(
"/barcode/get-pdf",
operation_id="get_product_barcode_pdf",
response_model=GetProductBarcodePdfResponse,
)
async def get_product_barcode_pdf(
request: GetProductBarcodePdfRequest, session: SessionDependency
):
service = BarcodePrinterService(session)
filename, base64_string = await service.generate_base64(request)
return GetProductBarcodePdfResponse(
base64_string=base64_string, filename=filename, mime_type="application/pdf"
)

View File

@ -0,0 +1,55 @@
from fastapi import APIRouter, Path
from backend.dependecies import SessionDependency
from modules.fulfillment_base.schemas.service import *
from modules.fulfillment_base.services import ServiceModelService
router = APIRouter(tags=["service"])
@router.get(
"/",
response_model=GetServicesResponse,
operation_id="get_services",
)
async def get_services(
session: SessionDependency,
):
return await ServiceModelService(session).get_all()
@router.post(
"/",
response_model=CreateServiceResponse,
operation_id="create_service",
)
async def create_service(
session: SessionDependency,
request: CreateServiceRequest,
):
return await ServiceModelService(session).create(request)
@router.patch(
"/{pk}",
response_model=UpdateServiceResponse,
operation_id="update_service",
)
async def update_service(
session: SessionDependency,
request: UpdateServiceRequest,
pk: int = Path(),
):
return await ServiceModelService(session).update(pk, request)
@router.delete(
"/{pk}",
response_model=DeleteServiceResponse,
operation_id="delete_service",
)
async def delete_service(
session: SessionDependency,
pk: int = Path(),
):
return await ServiceModelService(session).delete(pk)

View File

@ -0,0 +1,55 @@
from fastapi import APIRouter, Path
from backend.dependecies import SessionDependency
from modules.fulfillment_base.schemas.service_category import *
from modules.fulfillment_base.services import ServiceCategoryService
router = APIRouter(tags=["service-category"])
@router.get(
"/",
response_model=GetServiceCategoriesResponse,
operation_id="get_service_categories",
)
async def get_services_categories(
session: SessionDependency,
):
return await ServiceCategoryService(session).get_all()
@router.post(
"/",
response_model=CreateServiceCategoryResponse,
operation_id="create_service_category",
)
async def create_service_category(
session: SessionDependency,
request: CreateServiceCategoryRequest,
):
return await ServiceCategoryService(session).create(request)
@router.patch(
"/{pk}",
response_model=UpdateServiceCategoryResponse,
operation_id="update_service_category",
)
async def update_service_category(
session: SessionDependency,
request: UpdateServiceCategoryRequest,
pk: int = Path(),
):
return await ServiceCategoryService(session).update(pk, request)
@router.delete(
"/{pk}",
response_model=DeleteServiceCategoryResponse,
operation_id="delete_service_category",
)
async def delete_service_category(
session: SessionDependency,
pk: int = Path(),
):
return await ServiceCategoryService(session).delete(pk)

View File

@ -0,0 +1,55 @@
from fastapi import APIRouter, Path
from backend.dependecies import SessionDependency
from modules.fulfillment_base.schemas.services_kit import *
from modules.fulfillment_base.services import ServicesKitService
router = APIRouter(tags=["services-kit"])
@router.get(
"/",
response_model=GetServicesKitResponse,
operation_id="get_services_kits",
)
async def get_services_kits(
session: SessionDependency,
):
return await ServicesKitService(session).get_all()
@router.post(
"/",
response_model=CreateServicesKitResponse,
operation_id="create_services_kit",
)
async def create_services_kit(
session: SessionDependency,
request: CreateServicesKitRequest,
):
return await ServicesKitService(session).create(request)
@router.patch(
"/{pk}",
response_model=UpdateServicesKitResponse,
operation_id="update_services_kit",
)
async def update_services_kit(
session: SessionDependency,
request: UpdateServicesKitRequest,
pk: int = Path(),
):
return await ServicesKitService(session).update(pk, request)
@router.delete(
"/{pk}",
response_model=DeleteServicesKitResponse,
operation_id="delete_services_kit",
)
async def delete_services_kit(
session: SessionDependency,
pk: int = Path(),
):
return await ServicesKitService(session).delete(pk)

55
routers/crm/v1/project.py Normal file
View File

@ -0,0 +1,55 @@
from fastapi import APIRouter, Path
from backend.dependecies import SessionDependency
from schemas.project import *
from services import ProjectService
router = APIRouter(tags=["project"])
@router.get(
"/",
response_model=GetProjectsResponse,
operation_id="get_projects",
)
async def get_projects(
session: SessionDependency,
):
return await ProjectService(session).get_all()
@router.post(
"/",
response_model=CreateProjectResponse,
operation_id="create_project",
)
async def create_project(
session: SessionDependency,
request: CreateProjectRequest,
):
return await ProjectService(session).create(request)
@router.patch(
"/{pk}",
response_model=UpdateProjectResponse,
operation_id="update_project",
)
async def update_project(
session: SessionDependency,
request: UpdateProjectRequest,
pk: int = Path(),
):
return await ProjectService(session).update(pk, request)
@router.delete(
"/{pk}",
response_model=DeleteProjectResponse,
operation_id="delete_project",
)
async def delete_project(
session: SessionDependency,
pk: int = Path(),
):
return await ProjectService(session).delete(pk)

68
routers/crm/v1/status.py Normal file
View File

@ -0,0 +1,68 @@
from fastapi import APIRouter, Path
from backend.dependecies import SessionDependency
from schemas.status import *
from services import StatusService
router = APIRouter(tags=["status"])
@router.get(
"/{boardId}",
response_model=GetStatusesResponse,
operation_id="get_statuses",
)
async def get_statuses(
session: SessionDependency,
board_id: int = Path(alias="boardId"),
):
return await StatusService(session).get_all(board_id)
@router.post(
"/",
response_model=CreateStatusResponse,
operation_id="create_status",
)
async def create_status(
session: SessionDependency,
request: CreateStatusRequest,
):
return await StatusService(session).create(request)
@router.patch(
"/{pk}",
response_model=UpdateStatusResponse,
operation_id="update_status",
)
async def update_status(
session: SessionDependency,
request: UpdateStatusRequest,
pk: int = Path(),
):
return await StatusService(session).update(pk, request)
@router.delete(
"/{pk}",
response_model=DeleteStatusResponse,
operation_id="delete_status",
)
async def delete_status(
session: SessionDependency,
pk: int = Path(),
):
return await StatusService(session).delete(pk)
@router.get(
"/history/{dealId}",
response_model=GetStatusHistoryResponse,
operation_id="get_status_history",
)
async def get_status_history(
session: SessionDependency,
deal_id: int = Path(alias="dealId"),
):
return await StatusService(session).get_status_history(deal_id)

View File

@ -1,34 +0,0 @@
from fastapi import APIRouter, Path
from backend.dependecies import SessionDependency
from schemas.deal import GetDealsResponse, UpdateDealResponse, UpdateDealRequest
from services import DealService
deal_router = APIRouter(
tags=["deal"],
)
@deal_router.get(
"/{boardId}",
response_model=GetDealsResponse,
operation_id="get_deals",
)
async def get_deals(
session: SessionDependency,
board_id: int = Path(alias="boardId"),
):
return await DealService(session).get_deals(board_id)
@deal_router.patch(
"/{dealId}",
response_model=UpdateDealResponse,
operation_id="update_deal",
)
async def update_deal(
session: SessionDependency,
request: UpdateDealRequest,
deal_id: int = Path(alias="dealId"),
):
return await DealService(session).update_deal(deal_id, request)

Some files were not shown because too many files have changed in this diff Show More