Compare commits
55 Commits
812e30a2f8
...
34ac2a0a69
| Author | SHA1 | Date | |
|---|---|---|---|
| 34ac2a0a69 | |||
| 79a1dff720 | |||
| 44f00b1057 | |||
| ffee658349 | |||
| d7c7d1775f | |||
| 6b1b4109c6 | |||
| 35869e2ea5 | |||
| d8eba188c9 | |||
| 636821e74a | |||
| fbb0c72bce | |||
| bd4f4138be | |||
| 4c871e1e1b | |||
| 6b0f8a1aa5 | |||
| 7d6155ff6c | |||
| 986712d5b7 | |||
| 66b50fb951 | |||
| 9c9b3f4706 | |||
| c2594f9d55 | |||
| fbadddeada | |||
| 8cf589c54e | |||
| 22b8428035 | |||
| 6b3d124adf | |||
| 44f315b4a0 | |||
| 1df57c69c1 | |||
| 7eeb24f8ff | |||
| 8794241541 | |||
| 1a9dbd857a | |||
| 98d3026e0d | |||
| 276626d6f7 | |||
| be8052848c | |||
| d73748deab | |||
| 67634836dc | |||
| 7a76da4058 | |||
| 7990e7d460 | |||
| c1d3ac98f0 | |||
| e5be35be35 | |||
| c632fb8037 | |||
| fbab70d6c1 | |||
| 404a58735d | |||
| de5ffed7de | |||
| b9ae3bc18a | |||
| 57c3ada2fa | |||
| 93141da22c | |||
| 5fbd6d6185 | |||
| 4c7a997be6 | |||
| b776ad6758 | |||
| b4b29d448b | |||
| dd1f4145ae | |||
| c862544ae0 | |||
| c5e4dea52c | |||
| 5e20da8356 | |||
| 71c0901909 | |||
| 3b1b6f0523 | |||
| 2fed828768 | |||
| 734099165b |
BIN
assets/fonts/Arial Nova Cond.ttf
Normal file
BIN
assets/fonts/Arial Nova Cond.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/DejaVuSans.ttf
Normal file
BIN
assets/fonts/DejaVuSans.ttf
Normal file
Binary file not shown.
@ -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
18
main.py
@ -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")
|
||||||
|
|||||||
@ -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()
|
||||||
|
|||||||
@ -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
79
models/built_in_module.py
Normal 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)
|
||||||
@ -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
17
models/deal_group.py
Normal 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
51
models/deal_tag.py
Normal 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"),
|
||||||
|
)
|
||||||
@ -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="Себестоимость")
|
||||||
|
|||||||
@ -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",
|
||||||
|
)
|
||||||
|
|||||||
@ -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
0
modules/__init__.py
Normal file
0
modules/clients/__init__.py
Normal file
0
modules/clients/__init__.py
Normal file
1
modules/clients/models/__init__.py
Normal file
1
modules/clients/models/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .client import Client as Client, ClientDetails as ClientDetails
|
||||||
47
modules/clients/models/client.py
Normal file
47
modules/clients/models/client.py
Normal 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()
|
||||||
1
modules/clients/repositories/__init__.py
Normal file
1
modules/clients/repositories/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .client import ClientRepository as ClientRepository
|
||||||
51
modules/clients/repositories/client.py
Normal file
51
modules/clients/repositories/client.py
Normal 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)
|
||||||
75
modules/clients/schemas/client.py
Normal file
75
modules/clients/schemas/client.py
Normal 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
|
||||||
1
modules/clients/services/__init__.py
Normal file
1
modules/clients/services/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .client import ClientService as ClientService
|
||||||
26
modules/clients/services/client.py
Normal file
26
modules/clients/services/client.py
Normal 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
|
||||||
0
modules/fulfillment_base/__init__.py
Normal file
0
modules/fulfillment_base/__init__.py
Normal file
2
modules/fulfillment_base/barcodes_pdf_gen/__init__.py
Normal file
2
modules/fulfillment_base/barcodes_pdf_gen/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from .barcode_pdf_generator import BarcodePdfGenerator as BarcodePdfGenerator
|
||||||
|
from .types import BarcodeData as BarcodeData
|
||||||
@ -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
|
||||||
21
modules/fulfillment_base/barcodes_pdf_gen/types.py
Normal file
21
modules/fulfillment_base/barcodes_pdf_gen/types.py
Normal 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
|
||||||
1
modules/fulfillment_base/enums/__init__.py
Normal file
1
modules/fulfillment_base/enums/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from .service import *
|
||||||
7
modules/fulfillment_base/enums/service.py
Normal file
7
modules/fulfillment_base/enums/service.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from enum import IntEnum, unique
|
||||||
|
|
||||||
|
|
||||||
|
@unique
|
||||||
|
class ServiceType(IntEnum):
|
||||||
|
DEAL_SERVICE = 0
|
||||||
|
PRODUCT_SERVICE = 1
|
||||||
17
modules/fulfillment_base/models/__init__.py
Normal file
17
modules/fulfillment_base/models/__init__.py
Normal 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
|
||||||
36
modules/fulfillment_base/models/barcode.py
Normal file
36
modules/fulfillment_base/models/barcode.py
Normal 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()
|
||||||
42
modules/fulfillment_base/models/barcode_template.py
Normal file
42
modules/fulfillment_base/models/barcode_template.py
Normal 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")
|
||||||
62
modules/fulfillment_base/models/deal_product.py
Normal file
62
modules/fulfillment_base/models/deal_product.py
Normal 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",
|
||||||
|
),
|
||||||
|
)
|
||||||
28
modules/fulfillment_base/models/deal_service.py
Normal file
28
modules/fulfillment_base/models/deal_service.py
Normal 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="Фиксированная цена"
|
||||||
|
)
|
||||||
32
modules/fulfillment_base/models/marketplace.py
Normal file
32
modules/fulfillment_base/models/marketplace.py
Normal 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)
|
||||||
65
modules/fulfillment_base/models/product.py
Normal file
65
modules/fulfillment_base/models/product.py
Normal 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()
|
||||||
69
modules/fulfillment_base/models/service.py
Normal file
69
modules/fulfillment_base/models/service.py
Normal 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="До количества")
|
||||||
9
modules/fulfillment_base/repositories/__init__.py
Normal file
9
modules/fulfillment_base/repositories/__init__.py
Normal 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
|
||||||
119
modules/fulfillment_base/repositories/barcode_template.py
Normal file
119
modules/fulfillment_base/repositories/barcode_template.py
Normal 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())
|
||||||
62
modules/fulfillment_base/repositories/deal_product.py
Normal file
62
modules/fulfillment_base/repositories/deal_product.py
Normal 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()
|
||||||
71
modules/fulfillment_base/repositories/deal_service.py
Normal file
71
modules/fulfillment_base/repositories/deal_service.py
Normal 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()
|
||||||
62
modules/fulfillment_base/repositories/marketplace.py
Normal file
62
modules/fulfillment_base/repositories/marketplace.py
Normal 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)
|
||||||
97
modules/fulfillment_base/repositories/product.py
Normal file
97
modules/fulfillment_base/repositories/product.py
Normal 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)
|
||||||
105
modules/fulfillment_base/repositories/product_service.py
Normal file
105
modules/fulfillment_base/repositories/product_service.py
Normal 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()
|
||||||
78
modules/fulfillment_base/repositories/service.py
Normal file
78
modules/fulfillment_base/repositories/service.py
Normal 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()
|
||||||
18
modules/fulfillment_base/repositories/service_category.py
Normal file
18
modules/fulfillment_base/repositories/service_category.py
Normal 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)
|
||||||
43
modules/fulfillment_base/repositories/services_kit.py
Normal file
43
modules/fulfillment_base/repositories/services_kit.py
Normal 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)
|
||||||
82
modules/fulfillment_base/schemas/barcode_template.py
Normal file
82
modules/fulfillment_base/schemas/barcode_template.py
Normal 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
|
||||||
64
modules/fulfillment_base/schemas/deal_product.py
Normal file
64
modules/fulfillment_base/schemas/deal_product.py
Normal 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
|
||||||
72
modules/fulfillment_base/schemas/deal_service.py
Normal file
72
modules/fulfillment_base/schemas/deal_service.py
Normal 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
|
||||||
77
modules/fulfillment_base/schemas/marketplace.py
Normal file
77
modules/fulfillment_base/schemas/marketplace.py
Normal 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
|
||||||
104
modules/fulfillment_base/schemas/product.py
Normal file
104
modules/fulfillment_base/schemas/product.py
Normal 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
|
||||||
79
modules/fulfillment_base/schemas/product_service.py
Normal file
79
modules/fulfillment_base/schemas/product_service.py
Normal 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
|
||||||
77
modules/fulfillment_base/schemas/service.py
Normal file
77
modules/fulfillment_base/schemas/service.py
Normal 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
|
||||||
59
modules/fulfillment_base/schemas/service_category.py
Normal file
59
modules/fulfillment_base/schemas/service_category.py
Normal 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
|
||||||
60
modules/fulfillment_base/schemas/services_kit.py
Normal file
60
modules/fulfillment_base/schemas/services_kit.py
Normal 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
|
||||||
10
modules/fulfillment_base/services/__init__.py
Normal file
10
modules/fulfillment_base/services/__init__.py
Normal 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
|
||||||
41
modules/fulfillment_base/services/barcode_printer_service.py
Normal file
41
modules/fulfillment_base/services/barcode_printer_service.py
Normal 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
|
||||||
36
modules/fulfillment_base/services/barcode_template.py
Normal file
36
modules/fulfillment_base/services/barcode_template.py
Normal 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]
|
||||||
|
)
|
||||||
37
modules/fulfillment_base/services/deal_product.py
Normal file
37
modules/fulfillment_base/services/deal_product.py
Normal 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="Товар удален из сделки")
|
||||||
53
modules/fulfillment_base/services/deal_service.py
Normal file
53
modules/fulfillment_base/services/deal_service.py
Normal 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="Комплект добавлен в сделку")
|
||||||
27
modules/fulfillment_base/services/marketplace.py
Normal file
27
modules/fulfillment_base/services/marketplace.py
Normal 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]
|
||||||
|
)
|
||||||
50
modules/fulfillment_base/services/product.py
Normal file
50
modules/fulfillment_base/services/product.py
Normal 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
|
||||||
81
modules/fulfillment_base/services/product_service.py
Normal file
81
modules/fulfillment_base/services/product_service.py
Normal 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="Комплект добавлен в товар")
|
||||||
26
modules/fulfillment_base/services/service.py
Normal file
26
modules/fulfillment_base/services/service.py
Normal 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
|
||||||
28
modules/fulfillment_base/services/service_category.py
Normal file
28
modules/fulfillment_base/services/service_category.py
Normal 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
|
||||||
23
modules/fulfillment_base/services/services_kit.py
Normal file
23
modules/fulfillment_base/services/services_kit.py
Normal 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
|
||||||
@ -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]
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
|
||||||
|
|||||||
18
repositories/built_in_module.py
Normal file
18
repositories/built_in_module.py
Normal 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()
|
||||||
@ -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()
|
||||||
|
|||||||
54
repositories/deal_group.py
Normal file
54
repositories/deal_group.py
Normal 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
62
repositories/deal_tag.py
Normal 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
140
repositories/mixins.py
Normal 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
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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())
|
||||||
|
|||||||
@ -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
0
routers/crm/__init__.py
Normal file
0
routers/crm/v1/__init__.py
Normal file
0
routers/crm/v1/__init__.py
Normal file
56
routers/crm/v1/board.py
Normal file
56
routers/crm/v1/board.py
Normal 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
74
routers/crm/v1/deal.py
Normal 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)
|
||||||
59
routers/crm/v1/deal_group.py
Normal file
59
routers/crm/v1/deal_group.py
Normal 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)
|
||||||
81
routers/crm/v1/deal_tag.py
Normal file
81
routers/crm/v1/deal_tag.py
Normal 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
18
routers/crm/v1/module.py
Normal 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()
|
||||||
0
routers/crm/v1/modules/__init__.py
Normal file
0
routers/crm/v1/modules/__init__.py
Normal file
0
routers/crm/v1/modules/clients/__init__.py
Normal file
0
routers/crm/v1/modules/clients/__init__.py
Normal file
56
routers/crm/v1/modules/clients/client.py
Normal file
56
routers/crm/v1/modules/clients/client.py
Normal 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)
|
||||||
0
routers/crm/v1/modules/fulfillment_base/__init__.py
Normal file
0
routers/crm/v1/modules/fulfillment_base/__init__.py
Normal file
77
routers/crm/v1/modules/fulfillment_base/barcode_template.py
Normal file
77
routers/crm/v1/modules/fulfillment_base/barcode_template.py
Normal 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()
|
||||||
137
routers/crm/v1/modules/fulfillment_base/deal_product.py
Normal file
137
routers/crm/v1/modules/fulfillment_base/deal_product.py
Normal 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
|
||||||
70
routers/crm/v1/modules/fulfillment_base/deal_service.py
Normal file
70
routers/crm/v1/modules/fulfillment_base/deal_service.py
Normal 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)
|
||||||
66
routers/crm/v1/modules/fulfillment_base/marketplace.py
Normal file
66
routers/crm/v1/modules/fulfillment_base/marketplace.py
Normal 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)
|
||||||
73
routers/crm/v1/modules/fulfillment_base/product.py
Normal file
73
routers/crm/v1/modules/fulfillment_base/product.py
Normal 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"
|
||||||
|
)
|
||||||
55
routers/crm/v1/modules/fulfillment_base/service.py
Normal file
55
routers/crm/v1/modules/fulfillment_base/service.py
Normal 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)
|
||||||
55
routers/crm/v1/modules/fulfillment_base/service_category.py
Normal file
55
routers/crm/v1/modules/fulfillment_base/service_category.py
Normal 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)
|
||||||
55
routers/crm/v1/modules/fulfillment_base/services_kit.py
Normal file
55
routers/crm/v1/modules/fulfillment_base/services_kit.py
Normal 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
55
routers/crm/v1/project.py
Normal 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
68
routers/crm/v1/status.py
Normal 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)
|
||||||
@ -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
Reference in New Issue
Block a user