Compare commits
24 Commits
44f00b1057
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| eab801e41b | |||
| 7defcbdbd4 | |||
| ee18f16250 | |||
| c266814c96 | |||
| a7bda3d9f6 | |||
| be878717e5 | |||
| 2700538945 | |||
| 80a74ac8e6 | |||
| ef657c4939 | |||
| 36b3e056dc | |||
| 307e6573e3 | |||
| 82fcd6e8cb | |||
| 0e8c9077c9 | |||
| 9b109a7270 | |||
| c1196497d4 | |||
| 759a8d6478 | |||
| a579ae4145 | |||
| fcaa7fe177 | |||
| 281600c72d | |||
| 62aeebf079 | |||
| 83f3b55f49 | |||
| 90c0bae8f1 | |||
| 34ac2a0a69 | |||
| 79a1dff720 |
11
.env.example
11
.env.example
@ -5,3 +5,14 @@ PG_DATABASE=
|
||||
PG_HOST=
|
||||
|
||||
SECRET_KEY=
|
||||
|
||||
S3_URL=
|
||||
S3_ACCESS_KEY=
|
||||
S3_SECRET_ACCESS_KEY=
|
||||
S3_REGION=
|
||||
S3_BUCKET=
|
||||
|
||||
REDIS_PASSWORD=
|
||||
REDIS_HOST=
|
||||
REDIS_DB=
|
||||
REDIS_URL=
|
||||
|
||||
@ -12,3 +12,11 @@ PG_DATABASE = os.environ.get("PG_DATABASE")
|
||||
PG_HOST = os.environ.get("PG_HOST")
|
||||
|
||||
SECRET_KEY = os.environ.get("SECRET_KEY")
|
||||
|
||||
S3_URL = os.environ.get("S3_URL")
|
||||
S3_ACCESS_KEY = os.environ.get("S3_ACCESS_KEY")
|
||||
S3_SECRET_ACCESS_KEY = os.environ.get("S3_SECRET_ACCESS_KEY")
|
||||
S3_REGION = os.environ.get("S3_REGION")
|
||||
S3_BUCKET = os.environ.get("S3_BUCKET")
|
||||
|
||||
RABBITMQ_URL = os.environ.get("RABBITMQ_URL")
|
||||
|
||||
4
core/__init__.py
Normal file
4
core/__init__.py
Normal file
@ -0,0 +1,4 @@
|
||||
from .lifespan import lifespan as lifespan
|
||||
from .app_settings import settings as settings
|
||||
from .middlewares import register_middlewares as register_middlewares
|
||||
from .exceptions import register_exception_handlers as register_exception_handlers
|
||||
11
core/app_settings.py
Normal file
11
core/app_settings.py
Normal file
@ -0,0 +1,11 @@
|
||||
from fastapi.responses import ORJSONResponse
|
||||
|
||||
|
||||
class Settings:
|
||||
ROOT_PATH = "/api"
|
||||
DEFAULT_RESPONSE_CLASS = ORJSONResponse
|
||||
ORIGINS = ["http://localhost:3000"]
|
||||
STATIC_DIR = "static"
|
||||
|
||||
|
||||
settings = Settings()
|
||||
17
core/exceptions.py
Normal file
17
core/exceptions.py
Normal file
@ -0,0 +1,17 @@
|
||||
from fastapi import Request
|
||||
from fastapi.applications import AppType
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
from utils.exceptions import ObjectNotFoundException, ForbiddenException
|
||||
|
||||
|
||||
def register_exception_handlers(app: AppType):
|
||||
@app.exception_handler(ObjectNotFoundException)
|
||||
async def not_found_exception_handler(
|
||||
request: Request, exc: ObjectNotFoundException
|
||||
):
|
||||
return JSONResponse(status_code=404, content={"detail": exc.name})
|
||||
|
||||
@app.exception_handler(ForbiddenException)
|
||||
async def forbidden_exception_handler(request: Request, exc: ForbiddenException):
|
||||
return JSONResponse(status_code=403, content={"detail": exc.name})
|
||||
16
core/lifespan.py
Normal file
16
core/lifespan.py
Normal file
@ -0,0 +1,16 @@
|
||||
import contextlib
|
||||
|
||||
from fastapi.applications import AppType
|
||||
|
||||
from task_management import broker
|
||||
|
||||
|
||||
@contextlib.asynccontextmanager
|
||||
async def lifespan(app: AppType):
|
||||
if not broker.is_worker_process:
|
||||
await broker.startup()
|
||||
|
||||
yield
|
||||
|
||||
if not broker.is_worker_process:
|
||||
await broker.shutdown()
|
||||
19
core/middlewares.py
Normal file
19
core/middlewares.py
Normal file
@ -0,0 +1,19 @@
|
||||
from fastapi.applications import AppType
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
|
||||
from .app_settings import settings
|
||||
|
||||
|
||||
def register_middlewares(app: AppType):
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.ORIGINS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
app.add_middleware(
|
||||
GZipMiddleware,
|
||||
minimum_size=1_000,
|
||||
)
|
||||
0
external/__init__.py
vendored
Normal file
0
external/__init__.py
vendored
Normal file
1
external/s3_uploader/__init__.py
vendored
Normal file
1
external/s3_uploader/__init__.py
vendored
Normal file
@ -0,0 +1 @@
|
||||
from .uploader import S3Uploader as S3Uploader
|
||||
81
external/s3_uploader/uploader.py
vendored
Normal file
81
external/s3_uploader/uploader.py
vendored
Normal file
@ -0,0 +1,81 @@
|
||||
import uuid
|
||||
from typing import Optional
|
||||
|
||||
from aioboto3 import Session
|
||||
from fastapi import UploadFile
|
||||
|
||||
from backend import config
|
||||
from logger import logger_builder
|
||||
|
||||
|
||||
class S3Uploader:
|
||||
session: Session
|
||||
|
||||
def __init__(self):
|
||||
self.session = Session()
|
||||
|
||||
def _get_client(self) -> int:
|
||||
return self.session.client(
|
||||
"s3",
|
||||
endpoint_url=config.S3_URL,
|
||||
aws_access_key_id=config.S3_ACCESS_KEY,
|
||||
aws_secret_access_key=config.S3_SECRET_ACCESS_KEY,
|
||||
region_name=config.S3_REGION,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
async def _generate_s3_key() -> str:
|
||||
return uuid.uuid4().hex
|
||||
|
||||
@staticmethod
|
||||
def get_file_path_from_name(filename: str) -> str:
|
||||
return f"{config.S3_URL}/{config.S3_BUCKET}/{filename}"
|
||||
|
||||
async def upload_from_bytes(
|
||||
self,
|
||||
image_bytes: bytes,
|
||||
content_type: str,
|
||||
extension: str,
|
||||
unique_s3_key: Optional[str] = None,
|
||||
) -> str:
|
||||
logger = logger_builder.get_logger()
|
||||
|
||||
if unique_s3_key is None:
|
||||
unique_s3_key = await self._generate_s3_key()
|
||||
|
||||
filename = unique_s3_key + "." + extension
|
||||
file_url = self.get_file_path_from_name(filename)
|
||||
|
||||
try:
|
||||
async with self._get_client() as s3_client:
|
||||
await s3_client.put_object(
|
||||
Bucket=config.S3_BUCKET,
|
||||
Key=filename,
|
||||
Body=image_bytes,
|
||||
ContentType=content_type,
|
||||
)
|
||||
|
||||
logger.info(f"Successfully uploaded {filename} to S3")
|
||||
return file_url
|
||||
except Exception as e:
|
||||
logger.error(f"Error uploading image bytes: {e}")
|
||||
raise
|
||||
|
||||
async def upload_from_upload_file_obj(self, upload_file: UploadFile) -> str:
|
||||
file_bytes = upload_file.file.read()
|
||||
extension = upload_file.filename.split(".")[-1]
|
||||
file_url = await self.upload_from_bytes(
|
||||
file_bytes, upload_file.content_type, extension
|
||||
)
|
||||
return file_url
|
||||
|
||||
async def delete_image(self, s3_key: str):
|
||||
logger = logger_builder.get_logger()
|
||||
|
||||
try:
|
||||
async with self._get_client() as s3_client:
|
||||
await s3_client.delete_object(Bucket=config.S3_BUCKET, Key=s3_key)
|
||||
|
||||
logger.info(f"Successfully deleted {s3_key} from S3")
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting image from S3: {e}")
|
||||
@ -10,7 +10,7 @@ from logger.constants import (
|
||||
from logger.formatter import JsonFormatter
|
||||
from logger.gunzip_rotating_file_handler import GunZipRotatingFileHandler
|
||||
from logger.filters import LevelFilter, RequestIdFilter
|
||||
from core.singleton import Singleton
|
||||
from utils.singleton import Singleton
|
||||
|
||||
|
||||
class LoggerBuilder(metaclass=Singleton):
|
||||
|
||||
48
main.py
48
main.py
@ -1,40 +1,28 @@
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.middleware.gzip import GZipMiddleware
|
||||
from fastapi.responses import ORJSONResponse
|
||||
import taskiq_fastapi
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from starlette.responses import JSONResponse
|
||||
|
||||
import routers
|
||||
from core import lifespan, settings, register_middlewares, register_exception_handlers
|
||||
from task_management import broker
|
||||
from utils.auto_include_routers import auto_include_routers
|
||||
from utils.exceptions import ObjectNotFoundException
|
||||
|
||||
origins = ["http://localhost:3000"]
|
||||
|
||||
app = FastAPI(
|
||||
def create_app() -> FastAPI:
|
||||
app = FastAPI(
|
||||
separate_input_output_schemas=True,
|
||||
default_response_class=ORJSONResponse,
|
||||
root_path="/api",
|
||||
)
|
||||
default_response_class=settings.DEFAULT_RESPONSE_CLASS,
|
||||
root_path=settings.ROOT_PATH,
|
||||
# lifespan=lifespan,
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=origins,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
app.add_middleware(
|
||||
GZipMiddleware,
|
||||
minimum_size=1_000,
|
||||
)
|
||||
register_middlewares(app)
|
||||
register_exception_handlers(app)
|
||||
auto_include_routers(app, routers, full_path=True)
|
||||
app.mount("/static", StaticFiles(directory=settings.STATIC_DIR), name="static")
|
||||
|
||||
taskiq_fastapi.init(broker, "main:app")
|
||||
return app
|
||||
|
||||
|
||||
@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 = create_app()
|
||||
|
||||
@ -1,13 +1,20 @@
|
||||
from sqlalchemy.orm import configure_mappers
|
||||
|
||||
from modules.fulfillment_base.models import * # noqa: F401
|
||||
from .attr_select import (
|
||||
AttributeOption as AttributeOption,
|
||||
AttributeSelect as AttributeSelect,
|
||||
)
|
||||
from .attribute import (
|
||||
AttributeType as AttributeType,
|
||||
Attribute as Attribute,
|
||||
AttributeValue as AttributeValue,
|
||||
AttributeLabel as AttributeLabel,
|
||||
module_attribute as module_attribute,
|
||||
)
|
||||
from .auth import User as User
|
||||
from .base import BaseModel as BaseModel
|
||||
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_group import DealGroup as DealGroup
|
||||
from .deal_tag import (
|
||||
@ -15,6 +22,12 @@ from .deal_tag import (
|
||||
DealTagColor as DealTagColor,
|
||||
deals_deal_tags as deals_deal_tags,
|
||||
)
|
||||
from .module import ( # noqa: F401
|
||||
Module as Module,
|
||||
ModuleTab as ModuleTab,
|
||||
project_module as project_module,
|
||||
module_dependencies as module_dependencies,
|
||||
)
|
||||
from .project import Project as Project
|
||||
from .status import Status as Status, DealStatusHistory as DealStatusHistory
|
||||
|
||||
|
||||
43
models/attr_select.py
Normal file
43
models/attr_select.py
Normal file
@ -0,0 +1,43 @@
|
||||
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 IdMixin, SoftDeleteMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from models import Attribute
|
||||
|
||||
|
||||
class AttributeSelect(BaseModel, IdMixin, SoftDeleteMixin):
|
||||
__tablename__ = "attribute_selects"
|
||||
|
||||
name: Mapped[str] = mapped_column()
|
||||
is_built_in: Mapped[bool] = mapped_column(
|
||||
default=False,
|
||||
comment="Если встроенный select, то запрещено редактировать пользователю",
|
||||
)
|
||||
|
||||
options: Mapped[list["AttributeOption"]] = relationship(
|
||||
back_populates="select",
|
||||
lazy="noload",
|
||||
)
|
||||
|
||||
attributes: Mapped[list["Attribute"]] = relationship(
|
||||
back_populates="select",
|
||||
lazy="noload",
|
||||
)
|
||||
|
||||
|
||||
class AttributeOption(BaseModel, IdMixin, SoftDeleteMixin):
|
||||
__tablename__ = "attribute_options"
|
||||
|
||||
name: Mapped[str] = mapped_column()
|
||||
lexorank: Mapped[str] = mapped_column(comment="Ранг опции")
|
||||
|
||||
select_id: Mapped[int] = mapped_column(ForeignKey("attribute_selects.id"))
|
||||
select: Mapped[AttributeSelect] = relationship(
|
||||
back_populates="options",
|
||||
lazy="noload",
|
||||
)
|
||||
139
models/attribute.py
Normal file
139
models/attribute.py
Normal file
@ -0,0 +1,139 @@
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from sqlalchemy import ForeignKey, Table, Column
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from models.base import BaseModel
|
||||
from models.mixins import IdMixin, SoftDeleteMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from models import Module, Deal, AttributeSelect, AttributeOption
|
||||
|
||||
module_attribute = Table(
|
||||
"module_attribute",
|
||||
BaseModel.metadata,
|
||||
Column("module_id", ForeignKey("modules.id"), primary_key=True),
|
||||
Column("attribute_id", ForeignKey("attributes.id"), primary_key=True),
|
||||
)
|
||||
|
||||
|
||||
class AttributeType(BaseModel, IdMixin, SoftDeleteMixin):
|
||||
__tablename__ = "attribute_types"
|
||||
|
||||
type: Mapped[str] = mapped_column(unique=True)
|
||||
name: Mapped[str] = mapped_column(unique=True)
|
||||
|
||||
attributes: Mapped["Attribute"] = relationship(
|
||||
back_populates="type",
|
||||
lazy="noload",
|
||||
)
|
||||
|
||||
|
||||
class Attribute(BaseModel, IdMixin, SoftDeleteMixin):
|
||||
__tablename__ = "attributes"
|
||||
|
||||
label: Mapped[str] = mapped_column()
|
||||
is_applicable_to_group: Mapped[bool] = mapped_column(
|
||||
default=False,
|
||||
comment="Применять ли изменения атрибута карточки ко всем карточкам в группе",
|
||||
)
|
||||
is_nullable: Mapped[bool] = mapped_column(default=False)
|
||||
|
||||
default_value: Mapped[Optional[dict[str, any]]] = mapped_column(JSONB)
|
||||
default_option_id: Mapped[Optional[int]] = mapped_column(
|
||||
ForeignKey("attribute_options.id")
|
||||
)
|
||||
default_option: Mapped[Optional["AttributeOption"]] = relationship(
|
||||
backref="attributes", lazy="joined"
|
||||
)
|
||||
|
||||
description: Mapped[str] = mapped_column(default="")
|
||||
|
||||
is_built_in: Mapped[bool] = mapped_column(default=False)
|
||||
|
||||
type_id: Mapped[int] = mapped_column(ForeignKey("attribute_types.id"))
|
||||
type: Mapped[AttributeType] = relationship(
|
||||
back_populates="attributes",
|
||||
lazy="joined",
|
||||
)
|
||||
|
||||
select_id: Mapped[Optional[int]] = mapped_column(ForeignKey("attribute_selects.id"))
|
||||
select: Mapped[Optional["AttributeSelect"]] = relationship(
|
||||
back_populates="attributes",
|
||||
lazy="joined",
|
||||
)
|
||||
|
||||
modules: Mapped[list["Module"]] = relationship(
|
||||
secondary=module_attribute,
|
||||
back_populates="attributes",
|
||||
lazy="noload",
|
||||
)
|
||||
|
||||
values: Mapped[list["AttributeValue"]] = relationship(
|
||||
uselist=True,
|
||||
back_populates="attribute",
|
||||
lazy="noload",
|
||||
)
|
||||
|
||||
|
||||
class AttributeValue(BaseModel):
|
||||
__tablename__ = "attribute_values"
|
||||
|
||||
id: Mapped[int] = mapped_column(
|
||||
primary_key=True,
|
||||
autoincrement=True,
|
||||
)
|
||||
|
||||
value: Mapped[Optional[dict[str, any]]] = mapped_column(JSONB)
|
||||
|
||||
deal_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("deals.id"),
|
||||
primary_key=True,
|
||||
)
|
||||
deal: Mapped["Deal"] = relationship(
|
||||
back_populates="attributes_values",
|
||||
lazy="noload",
|
||||
)
|
||||
|
||||
module_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("modules.id"),
|
||||
primary_key=True,
|
||||
)
|
||||
module: Mapped["Module"] = relationship(
|
||||
back_populates="attribute_values",
|
||||
lazy="noload",
|
||||
)
|
||||
|
||||
attribute_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("attributes.id"),
|
||||
primary_key=True,
|
||||
)
|
||||
attribute: Mapped[Attribute] = relationship(
|
||||
back_populates="values",
|
||||
lazy="noload",
|
||||
)
|
||||
|
||||
|
||||
class AttributeLabel(BaseModel):
|
||||
__tablename__ = "attribute_labels"
|
||||
|
||||
label: Mapped[str] = mapped_column()
|
||||
|
||||
module_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("modules.id"),
|
||||
primary_key=True,
|
||||
)
|
||||
module: Mapped["Module"] = relationship(
|
||||
backref="attribute_labels",
|
||||
lazy="noload",
|
||||
)
|
||||
|
||||
attribute_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("attributes.id"),
|
||||
primary_key=True,
|
||||
)
|
||||
attribute: Mapped[Attribute] = relationship(
|
||||
backref="attribute_labels",
|
||||
lazy="noload",
|
||||
)
|
||||
6
models/auth.py
Normal file
6
models/auth.py
Normal file
@ -0,0 +1,6 @@
|
||||
from models.base import BaseModel
|
||||
from models.mixins import IdMixin, SoftDeleteMixin
|
||||
|
||||
|
||||
class User(BaseModel, IdMixin, SoftDeleteMixin):
|
||||
__tablename__ = "users"
|
||||
@ -13,10 +13,10 @@ if TYPE_CHECKING:
|
||||
class Board(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin):
|
||||
__tablename__ = "boards"
|
||||
|
||||
name: Mapped[str] = mapped_column(nullable=False)
|
||||
lexorank: Mapped[str] = mapped_column(nullable=False)
|
||||
name: Mapped[str] = mapped_column()
|
||||
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")
|
||||
|
||||
statuses: Mapped[list["Status"]] = relationship(back_populates="board")
|
||||
|
||||
@ -1,79 +0,0 @@
|
||||
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)
|
||||
@ -7,7 +7,14 @@ from models.base import BaseModel
|
||||
from models.mixins import SoftDeleteMixin, CreatedAtMixin, IdMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from models import Status, Board, DealStatusHistory, DealGroup, DealTag
|
||||
from models import (
|
||||
Status,
|
||||
Board,
|
||||
DealStatusHistory,
|
||||
DealGroup,
|
||||
DealTag,
|
||||
AttributeValue,
|
||||
)
|
||||
from modules.clients.models import Client
|
||||
|
||||
|
||||
@ -49,6 +56,10 @@ class Deal(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin):
|
||||
secondaryjoin="and_(DealTag.id == deals_deal_tags.c.deal_tag_id, DealTag.is_deleted == False)",
|
||||
)
|
||||
|
||||
attributes_values: Mapped[list["AttributeValue"]] = relationship(
|
||||
back_populates="deal",
|
||||
)
|
||||
|
||||
# module client
|
||||
client_id: Mapped[Optional[int]] = mapped_column(
|
||||
ForeignKey("clients.id", ondelete="CASCADE"),
|
||||
|
||||
89
models/module.py
Normal file
89
models/module.py
Normal file
@ -0,0 +1,89 @@
|
||||
import enum
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from sqlalchemy import Table, Column, ForeignKey
|
||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
|
||||
|
||||
from models import AttributeValue, Attribute
|
||||
from models.base import BaseModel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from models import Project
|
||||
|
||||
project_module = Table(
|
||||
"project_module",
|
||||
BaseModel.metadata,
|
||||
Column("project_id", ForeignKey("projects.id"), primary_key=True),
|
||||
Column("module_id", ForeignKey("modules.id"), primary_key=True),
|
||||
)
|
||||
|
||||
|
||||
module_dependencies = Table(
|
||||
"module_dependencies",
|
||||
BaseModel.metadata,
|
||||
Column("module_id", ForeignKey("modules.id"), primary_key=True),
|
||||
Column("depends_on_id", ForeignKey("modules.id"), primary_key=True),
|
||||
)
|
||||
|
||||
|
||||
class Module(BaseModel):
|
||||
__tablename__ = "modules"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
key: Mapped[str] = mapped_column(unique=True)
|
||||
label: Mapped[str] = mapped_column()
|
||||
description: Mapped[Optional[str]] = mapped_column()
|
||||
is_deleted: Mapped[bool] = mapped_column(default=False)
|
||||
is_built_in: Mapped[bool] = mapped_column(default=False, server_default="0")
|
||||
|
||||
depends_on: Mapped[list["Module"]] = relationship(
|
||||
secondary=module_dependencies,
|
||||
primaryjoin="Module.id == module_dependencies.c.module_id",
|
||||
secondaryjoin="Module.id == module_dependencies.c.depends_on_id",
|
||||
back_populates="depended_on_by",
|
||||
lazy="immediate",
|
||||
)
|
||||
|
||||
depended_on_by: Mapped[list["Module"]] = relationship(
|
||||
secondary="module_dependencies",
|
||||
primaryjoin="Module.id == module_dependencies.c.depends_on_id",
|
||||
secondaryjoin="Module.id == module_dependencies.c.module_id",
|
||||
back_populates="depends_on",
|
||||
lazy="noload",
|
||||
)
|
||||
|
||||
projects: Mapped[list["Project"]] = relationship(
|
||||
uselist=True,
|
||||
secondary="project_module",
|
||||
back_populates="modules",
|
||||
lazy="noload",
|
||||
)
|
||||
|
||||
tabs: Mapped[list["ModuleTab"]] = relationship(
|
||||
lazy="immediate", backref="module", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
attributes: Mapped[list["Attribute"]] = relationship(
|
||||
secondary="module_attribute", back_populates="modules", lazy="noload"
|
||||
)
|
||||
|
||||
attribute_values: Mapped[list["AttributeValue"]] = relationship(
|
||||
lazy="noload", back_populates="module"
|
||||
)
|
||||
|
||||
|
||||
class DeviceType(enum.StrEnum):
|
||||
MOBILE = "mobile"
|
||||
DESKTOP = "desktop"
|
||||
BOTH = "both"
|
||||
|
||||
|
||||
class ModuleTab(BaseModel):
|
||||
__tablename__ = "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[Optional[str]] = mapped_column()
|
||||
module_id: Mapped[int] = mapped_column(ForeignKey("modules.id"))
|
||||
device: Mapped[DeviceType] = mapped_column(default=DeviceType.BOTH)
|
||||
@ -6,7 +6,7 @@ from models.base import BaseModel
|
||||
from models.mixins import SoftDeleteMixin, CreatedAtMixin, IdMixin
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from models import Board, BuiltInModule, DealTag
|
||||
from models import Board, Module, DealTag
|
||||
|
||||
|
||||
class Project(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin):
|
||||
@ -19,11 +19,11 @@ class Project(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin):
|
||||
lazy="noload",
|
||||
)
|
||||
|
||||
built_in_modules: Mapped[list["BuiltInModule"]] = relationship(
|
||||
secondary="project_built_in_module",
|
||||
modules: Mapped[list["Module"]] = relationship(
|
||||
secondary="project_module",
|
||||
back_populates="projects",
|
||||
lazy="selectin",
|
||||
order_by="asc(BuiltInModule.id)",
|
||||
order_by="asc(Module.id)",
|
||||
)
|
||||
|
||||
tags: Mapped[list["DealTag"]] = relationship(
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from io import BytesIO
|
||||
from typing import Any, Optional
|
||||
|
||||
import aiohttp
|
||||
from reportlab.graphics.barcode import code128
|
||||
from reportlab.lib.units import mm
|
||||
from reportlab.platypus import Spacer, PageBreak, Paragraph
|
||||
@ -24,7 +25,7 @@ class BarcodePdfGenerator(PDFGenerator):
|
||||
return None
|
||||
return value
|
||||
|
||||
def generate(
|
||||
async def generate(
|
||||
self, barcodes_data: list[BarcodeData | PdfBarcodeImageGenData]
|
||||
) -> BytesIO:
|
||||
pdf_barcodes_gen_data: list[PdfBarcodeGenData | PdfBarcodeImageGenData] = []
|
||||
@ -52,9 +53,9 @@ class BarcodePdfGenerator(PDFGenerator):
|
||||
}
|
||||
)
|
||||
|
||||
return self._generate(pdf_barcodes_gen_data)
|
||||
return await self._generate(pdf_barcodes_gen_data)
|
||||
|
||||
def _generate(
|
||||
async def _generate(
|
||||
self, barcodes_data: list[PdfBarcodeGenData | PdfBarcodeImageGenData]
|
||||
) -> BytesIO:
|
||||
pdf_maker = PdfMaker((self.page_width, self.page_height))
|
||||
@ -63,9 +64,10 @@ class BarcodePdfGenerator(PDFGenerator):
|
||||
|
||||
for barcode_data in barcodes_data:
|
||||
if "barcode_value" in barcode_data:
|
||||
pdf_files.append(self._generate_for_one_product(barcode_data))
|
||||
result = self._generate_for_one_product(barcode_data)
|
||||
else:
|
||||
pdf_files.append(self._generate_for_one_product_using_img(barcode_data))
|
||||
result = await self._generate_for_one_product_using_img(barcode_data)
|
||||
pdf_files.append(result)
|
||||
pdf_files.append(self._generate_spacers())
|
||||
|
||||
for file in pdf_files[:-1]:
|
||||
@ -138,11 +140,18 @@ class BarcodePdfGenerator(PDFGenerator):
|
||||
buffer.seek(0)
|
||||
return buffer
|
||||
|
||||
def _generate_for_one_product_using_img(
|
||||
async 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_url = barcode_data["barcode_image_url"]
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(pdf_url) as response:
|
||||
if response.status != 200:
|
||||
raise ValueError(
|
||||
f"Failed to download PDF from {pdf_url} (status {response.status})"
|
||||
)
|
||||
pdf_bytes = await response.read()
|
||||
|
||||
pdf_maker = PdfMaker((self.page_width, self.page_height))
|
||||
for _ in range(barcode_data["num_duplicates"]):
|
||||
|
||||
@ -33,4 +33,4 @@ class ProductBarcodeImage(BaseModel):
|
||||
)
|
||||
product: Mapped["Product"] = relationship(back_populates="barcode_image")
|
||||
|
||||
filename: Mapped[str] = mapped_column()
|
||||
image_url: Mapped[str] = mapped_column()
|
||||
|
||||
@ -52,7 +52,6 @@ class Product(BaseModel, IdMixin, SoftDeleteMixin):
|
||||
barcode_image: Mapped["ProductBarcodeImage"] = relationship(
|
||||
back_populates="product",
|
||||
lazy="joined",
|
||||
uselist=False,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ class DealProductRepository(
|
||||
return (
|
||||
stmt.options(
|
||||
joinedload(DealProduct.product).selectinload(Product.barcodes),
|
||||
joinedload(DealProduct.product).joinedload(Product.client),
|
||||
selectinload(DealProduct.product_services).joinedload(
|
||||
DealProductService.service
|
||||
),
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
from sqlalchemy import or_, delete
|
||||
from sqlalchemy.orm import selectinload, joinedload
|
||||
|
||||
from modules.fulfillment_base.models import Product, ProductBarcode, BarcodeTemplate
|
||||
from modules.fulfillment_base.models import Product, ProductBarcode, BarcodeTemplate, ProductBarcodeImage
|
||||
from modules.fulfillment_base.models.product import ProductImage
|
||||
from modules.fulfillment_base.schemas.product import (
|
||||
CreateProductSchema,
|
||||
UpdateProductSchema,
|
||||
@ -16,6 +17,7 @@ class ProductRepository(
|
||||
RepUpdateMixin[Product, UpdateProductSchema],
|
||||
RepGetByIdMixin[Product],
|
||||
):
|
||||
session: AsyncSession
|
||||
entity_class = Product
|
||||
entity_not_found_msg = "Товар не найден"
|
||||
|
||||
@ -95,3 +97,36 @@ class ProductRepository(
|
||||
await self._update_barcodes(product, data.barcodes)
|
||||
del data.barcodes
|
||||
return await self._apply_update_data_to_model(product, data, True)
|
||||
|
||||
async def delete_images(self, product_images: list[ProductImage], with_commit: bool = False):
|
||||
for img in product_images:
|
||||
await self.session.delete(img)
|
||||
if with_commit:
|
||||
await self.session.commit()
|
||||
else:
|
||||
await self.session.flush()
|
||||
|
||||
async def delete_barcode_image(self, barcode_image: ProductBarcodeImage, with_commit: bool = False):
|
||||
await self.session.delete(barcode_image)
|
||||
if with_commit:
|
||||
await self.session.commit()
|
||||
else:
|
||||
await self.session.flush()
|
||||
|
||||
async def create_image(self, product_id: int, image_url: str) -> ProductImage:
|
||||
product_image = ProductImage(
|
||||
product_id=product_id,
|
||||
image_url=image_url,
|
||||
)
|
||||
self.session.add(product_image)
|
||||
await self.session.commit()
|
||||
return product_image
|
||||
|
||||
async def create_barcode_image(self, product_id: int, image_url: str) -> ProductBarcodeImage:
|
||||
product_barcode_image = ProductBarcodeImage(
|
||||
product_id=product_id,
|
||||
image_url=image_url,
|
||||
)
|
||||
self.session.add(product_barcode_image)
|
||||
await self.session.commit()
|
||||
return product_barcode_image
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import field_validator
|
||||
from pydantic import field_validator, model_validator
|
||||
|
||||
from modules.fulfillment_base.models import ProductBarcode
|
||||
from modules.fulfillment_base.schemas.barcode_template import BarcodeTemplateSchema
|
||||
@ -16,6 +16,11 @@ class ProductImageSchema(BaseSchema):
|
||||
image_url: str
|
||||
|
||||
|
||||
class ProductBarcodeImageSchema(BaseSchema):
|
||||
product_id: int
|
||||
image_url: str
|
||||
|
||||
|
||||
class CreateProductSchema(BaseSchema):
|
||||
name: str
|
||||
article: str
|
||||
@ -27,10 +32,27 @@ class CreateProductSchema(BaseSchema):
|
||||
composition: Optional[str]
|
||||
size: Optional[str]
|
||||
additional_info: Optional[str]
|
||||
barcodes: list[str]
|
||||
barcodes: list[str] = []
|
||||
|
||||
|
||||
class ProductSchema(CreateProductSchema):
|
||||
class BaseProductSchema(CreateProductSchema):
|
||||
image_url: Optional[str] = None
|
||||
images: Optional[list[ProductImageSchema]] = []
|
||||
barcode_image_url: Optional[str] = None
|
||||
barcode_image: Optional[ProductBarcodeImageSchema] = None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def images_list_to_image_url(cls, values):
|
||||
if values.images:
|
||||
latest_image = values.images[-1]
|
||||
values.image_url = latest_image.image_url
|
||||
|
||||
if values.barcode_image:
|
||||
values.barcode_image_url = values.barcode_image.image_url
|
||||
return values
|
||||
|
||||
|
||||
class ProductSchema(BaseProductSchema):
|
||||
id: int
|
||||
barcode_template: BarcodeTemplateSchema
|
||||
|
||||
@ -75,6 +97,10 @@ class GetProductBarcodePdfRequest(BaseSchema):
|
||||
barcode: str
|
||||
|
||||
|
||||
class GetDealBarcodesPdfRequest(BaseSchema):
|
||||
deal_id: int
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region Response
|
||||
@ -93,6 +119,10 @@ class UpdateProductResponse(BaseResponse):
|
||||
pass
|
||||
|
||||
|
||||
class ProductUploadImageResponse(BaseResponse):
|
||||
image_url: Optional[str] = None
|
||||
|
||||
|
||||
class DeleteProductResponse(BaseResponse):
|
||||
pass
|
||||
|
||||
@ -101,4 +131,16 @@ class GetProductBarcodePdfResponse(BasePdfResponse):
|
||||
pass
|
||||
|
||||
|
||||
class GetDealBarcodesPdfResponse(BasePdfResponse):
|
||||
pass
|
||||
|
||||
|
||||
class BarcodeUploadImageResponse(BaseResponse):
|
||||
image_url: Optional[str] = None
|
||||
|
||||
|
||||
class DeleteBarcodeImageResponse(BaseResponse):
|
||||
pass
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
@ -4,9 +4,16 @@ 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
|
||||
from modules.fulfillment_base.barcodes_pdf_gen.types import PdfBarcodeImageGenData
|
||||
from modules.fulfillment_base.models import Product, DealProduct
|
||||
from modules.fulfillment_base.repositories import (
|
||||
ProductRepository,
|
||||
DealProductRepository,
|
||||
)
|
||||
from modules.fulfillment_base.schemas.product import (
|
||||
GetProductBarcodePdfRequest,
|
||||
GetDealBarcodesPdfRequest,
|
||||
)
|
||||
|
||||
|
||||
class BarcodePrinterService:
|
||||
@ -15,27 +22,75 @@ class BarcodePrinterService:
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
|
||||
async def generate_pdf(
|
||||
async def generate_product_pdf(
|
||||
self, request: GetProductBarcodePdfRequest
|
||||
) -> tuple[str, BytesIO]:
|
||||
product: Product = await ProductRepository(self.session).get_by_id(
|
||||
request.product_id
|
||||
)
|
||||
if product.barcode_image:
|
||||
barcode_data: PdfBarcodeImageGenData = {
|
||||
"barcode_image_url": product.barcode_image.image_url,
|
||||
"num_duplicates": request.quantity,
|
||||
}
|
||||
else:
|
||||
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])
|
||||
return filename, await generator.generate([barcode_data])
|
||||
|
||||
async def generate_base64(
|
||||
async def generate_product_base64(
|
||||
self, request: GetProductBarcodePdfRequest
|
||||
) -> tuple[str, str]:
|
||||
filename, pdf_buffer = await self.generate_pdf(request)
|
||||
filename, pdf_buffer = await self.generate_product_pdf(request)
|
||||
base64_string = base64.b64encode(pdf_buffer.read()).decode("utf-8")
|
||||
return filename, base64_string
|
||||
|
||||
async def generate_deal_pdf(
|
||||
self, request: GetDealBarcodesPdfRequest
|
||||
) -> tuple[str, BytesIO]:
|
||||
deal_product_repo = DealProductRepository(self.session)
|
||||
deal_products: list[DealProduct] = await deal_product_repo.get_all(
|
||||
request.deal_id
|
||||
)
|
||||
|
||||
if len(deal_products) == 0:
|
||||
return "no_content.pdf", BytesIO()
|
||||
|
||||
barcodes_data: list[BarcodeData | PdfBarcodeImageGenData] = []
|
||||
|
||||
for deal_product in deal_products:
|
||||
if deal_product.product.barcode_image:
|
||||
barcode_data: PdfBarcodeImageGenData = {
|
||||
"barcode_image_url": deal_product.product.barcode_image.image_url,
|
||||
"num_duplicates": deal_product.quantity,
|
||||
}
|
||||
barcodes_data.append(barcode_data)
|
||||
elif len(deal_product.product.barcodes) > 0:
|
||||
barcode_data: BarcodeData = {
|
||||
"barcode": deal_product.product.barcodes[0].barcode,
|
||||
"template": deal_product.product.barcode_template,
|
||||
"product": deal_product.product,
|
||||
"num_duplicates": deal_product.quantity,
|
||||
}
|
||||
barcodes_data.append(barcode_data)
|
||||
|
||||
size = deal_products[0].product.barcode_template.size
|
||||
generator = BarcodePdfGenerator(size.width, size.height)
|
||||
filename = "deal_barcodes.pdf"
|
||||
return filename, await generator.generate(barcodes_data)
|
||||
|
||||
async def generate_deal_base64(
|
||||
self, request: GetDealBarcodesPdfRequest
|
||||
) -> tuple[str, str]:
|
||||
filename, pdf_buffer = await self.generate_deal_pdf(request)
|
||||
base64_string = base64.b64encode(pdf_buffer.read()).decode("utf-8")
|
||||
return filename, base64_string
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import math
|
||||
|
||||
from fastapi import UploadFile
|
||||
|
||||
from external.s3_uploader import S3Uploader
|
||||
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 modules.fulfillment_base.schemas.product import *
|
||||
from schemas.base import PaginationSchema, PaginationInfoSchema
|
||||
from services.mixins import *
|
||||
|
||||
@ -46,5 +45,54 @@ class ProductService(
|
||||
),
|
||||
)
|
||||
|
||||
async def is_soft_delete(self, product: ProductSchema) -> bool:
|
||||
return True
|
||||
async def upload_image(
|
||||
self, product_id: int, upload_file: UploadFile
|
||||
) -> ProductUploadImageResponse:
|
||||
product: Product = await self.repository.get_by_id(product_id)
|
||||
s3_uploader = S3Uploader()
|
||||
|
||||
if len(product.images) > 0:
|
||||
for image in product.images:
|
||||
s3_key = image.image_url.split("/")[-1]
|
||||
await s3_uploader.delete_image(s3_key)
|
||||
await self.repository.delete_images(product.images)
|
||||
|
||||
image_url = await s3_uploader.upload_from_upload_file_obj(upload_file)
|
||||
await self.repository.create_image(product_id, image_url)
|
||||
return ProductUploadImageResponse(
|
||||
message="Изображение успешно загружено", image_url=image_url
|
||||
)
|
||||
|
||||
async def upload_barcode_image(
|
||||
self, product_id: int, upload_file: UploadFile
|
||||
) -> BarcodeUploadImageResponse:
|
||||
product: Product = await self.repository.get_by_id(product_id)
|
||||
s3_uploader = S3Uploader()
|
||||
|
||||
if product.barcode_image:
|
||||
s3_key = product.barcode_image.image_url.split("/")[-1]
|
||||
await s3_uploader.delete_image(s3_key)
|
||||
await self.repository.delete_barcode_image(product.barcode_image)
|
||||
|
||||
image_url = await s3_uploader.upload_from_upload_file_obj(upload_file)
|
||||
await self.repository.create_barcode_image(product_id, image_url)
|
||||
return BarcodeUploadImageResponse(
|
||||
message="Изображение штрихкода успешно загружено", image_url=image_url
|
||||
)
|
||||
|
||||
async def delete_barcode_image(self, product_id: int) -> DeleteBarcodeImageResponse:
|
||||
product: Product = await self.repository.get_by_id(product_id)
|
||||
|
||||
if not product.barcode_image:
|
||||
return DeleteBarcodeImageResponse(
|
||||
message="У товара нет изображения штрихкода"
|
||||
)
|
||||
|
||||
s3_uploader = S3Uploader()
|
||||
s3_key = product.barcode_image.image_url.split("/")[-1]
|
||||
await s3_uploader.delete_image(s3_key)
|
||||
await self.repository.delete_barcode_image(product.barcode_image, True)
|
||||
|
||||
return DeleteBarcodeImageResponse(
|
||||
message="Изображение штрихкода успешно удалено"
|
||||
)
|
||||
|
||||
@ -17,6 +17,19 @@ dependencies = [
|
||||
"uvicorn[standard]>=0.35.0",
|
||||
"fastapi-endpoints @ git+https://github.com/vladNed/fastapi-endpoints.git@main",
|
||||
"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",
|
||||
"python-multipart>=0.0.20",
|
||||
"lexorank-py==0.1.1",
|
||||
"taskiq>=0.11.19",
|
||||
"taskiq-aio-pika>=0.4.4",
|
||||
"taskiq-fastapi>=0.3.5",
|
||||
"taskiq-postgresql>=0.4.0",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
from .attr_select import AttrSelectRepository as AttrSelectRepository
|
||||
from .attribute import AttributeRepository as AttributeRepository
|
||||
from .board import BoardRepository as BoardRepository
|
||||
from .built_in_module import BuiltInModuleRepository as BuiltInModuleRepository
|
||||
from .deal import DealRepository as DealRepository
|
||||
from .deal_group import DealGroupRepository as DealGroupRepository
|
||||
from .deal_tag import DealTagRepository as DealTagRepository
|
||||
from .module import ModuleRepository as ModuleRepository
|
||||
from .project import ProjectRepository as ProjectRepository
|
||||
from .status import StatusRepository as StatusRepository
|
||||
from .attr_option import AttrOptionRepository as AttrOptionRepository
|
||||
|
||||
21
repositories/attr_option.py
Normal file
21
repositories/attr_option.py
Normal file
@ -0,0 +1,21 @@
|
||||
from sqlalchemy import Select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models import AttributeOption
|
||||
from repositories.mixins import RepCrudMixin
|
||||
from schemas.attr_option import CreateAttrOptionSchema, UpdateAttrOptionSchema
|
||||
|
||||
|
||||
class AttrOptionRepository(
|
||||
RepCrudMixin[AttributeOption, CreateAttrOptionSchema, UpdateAttrOptionSchema],
|
||||
):
|
||||
session: AsyncSession
|
||||
entity_class = AttributeOption
|
||||
entity_not_found_msg = "Опция не найдена"
|
||||
|
||||
def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select:
|
||||
select_id = args[0]
|
||||
return stmt.where(
|
||||
AttributeOption.select_id == select_id,
|
||||
AttributeOption.is_deleted.is_(False),
|
||||
).order_by(AttributeOption.id)
|
||||
16
repositories/attr_select.py
Normal file
16
repositories/attr_select.py
Normal file
@ -0,0 +1,16 @@
|
||||
from sqlalchemy import Select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models import AttributeSelect
|
||||
from repositories.mixins import RepCrudMixin
|
||||
from schemas.attr_select import UpdateAttrSelectSchema, CreateAttrSelectSchema
|
||||
|
||||
|
||||
class AttrSelectRepository(
|
||||
RepCrudMixin[AttributeSelect, CreateAttrSelectSchema, UpdateAttrSelectSchema],
|
||||
):
|
||||
session: AsyncSession
|
||||
entity_class = AttributeSelect
|
||||
|
||||
def _process_get_all_stmt(self, stmt: Select) -> Select:
|
||||
return stmt.where(AttributeSelect.is_deleted.is_(False))
|
||||
212
repositories/attribute.py
Normal file
212
repositories/attribute.py
Normal file
@ -0,0 +1,212 @@
|
||||
from collections import defaultdict
|
||||
|
||||
from sqlalchemy import and_
|
||||
from sqlalchemy.orm import joinedload
|
||||
|
||||
from models import (
|
||||
Attribute,
|
||||
AttributeLabel,
|
||||
AttributeType,
|
||||
AttributeValue,
|
||||
module_attribute,
|
||||
Module,
|
||||
Project,
|
||||
)
|
||||
from repositories.mixins import *
|
||||
from schemas.attribute import (
|
||||
CreateAttributeSchema,
|
||||
UpdateAttributeSchema,
|
||||
UpdateDealModuleAttributeSchema,
|
||||
)
|
||||
from utils.exceptions import ForbiddenException
|
||||
|
||||
|
||||
class AttributeRepository(
|
||||
RepCrudMixin[Attribute, CreateAttributeSchema, UpdateAttributeSchema]
|
||||
):
|
||||
session: AsyncSession
|
||||
entity_class = Attribute
|
||||
|
||||
def _process_get_all_stmt(self, stmt: Select) -> Select:
|
||||
return (
|
||||
stmt.options(joinedload(Attribute.type))
|
||||
.where(Attribute.is_deleted.is_(False))
|
||||
.order_by(Attribute.is_built_in.desc(), Attribute.id)
|
||||
)
|
||||
|
||||
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
|
||||
return stmt.options(joinedload(Attribute.type))
|
||||
|
||||
async def update(self, attr: Attribute, data: UpdateAttributeSchema) -> Attribute:
|
||||
return await self._apply_update_data_to_model(
|
||||
attr, data, with_commit=True, set_if_value_is_not_none=False
|
||||
)
|
||||
|
||||
async def _before_delete(self, attribute: Attribute) -> None:
|
||||
if attribute.is_built_in:
|
||||
raise ForbiddenException("Нельзя менять встроенный атрибут")
|
||||
|
||||
async def _get_all_attributes_for_deal(
|
||||
self, project_id
|
||||
) -> list[tuple[Attribute, int]]:
|
||||
stmt = (
|
||||
select(Attribute, Module.id)
|
||||
.join(Attribute.modules)
|
||||
.join(Module.projects)
|
||||
.where(
|
||||
Module.is_deleted.is_(False),
|
||||
Project.is_deleted.is_(False),
|
||||
Project.id == project_id,
|
||||
)
|
||||
.distinct(Attribute.id, Module.id)
|
||||
)
|
||||
result = await self.session.execute(stmt)
|
||||
return list(result.all())
|
||||
|
||||
async def create_attributes_for_new_deal(
|
||||
self, deal_id: int, project_id: int
|
||||
) -> None:
|
||||
attributes = await self._get_all_attributes_for_deal(project_id)
|
||||
for attribute, module_id in attributes:
|
||||
def_val = (
|
||||
attribute.default_option_id
|
||||
if attribute.default_option_id is not None
|
||||
else attribute.default_value
|
||||
)
|
||||
if def_val is None:
|
||||
continue
|
||||
value = AttributeValue(
|
||||
attribute_id=attribute.id,
|
||||
deal_id=deal_id,
|
||||
module_id=module_id,
|
||||
value=def_val,
|
||||
)
|
||||
self.session.add(value)
|
||||
|
||||
async def _get_attribute_module_label(
|
||||
self, module_id: int, attribute_id: int
|
||||
) -> Optional[AttributeLabel]:
|
||||
stmt = select(AttributeLabel).where(
|
||||
AttributeLabel.attribute_id == attribute_id,
|
||||
AttributeLabel.module_id == module_id,
|
||||
)
|
||||
result = await self.session.execute(stmt)
|
||||
row = result.one_or_none()
|
||||
return row[0] if row else None
|
||||
|
||||
async def create_or_update_attribute_label(
|
||||
self, module_id: int, attribute_id: int, label: str
|
||||
):
|
||||
attribute_label = await self._get_attribute_module_label(
|
||||
module_id, attribute_id
|
||||
)
|
||||
if attribute_label:
|
||||
attribute_label.label = label
|
||||
else:
|
||||
attribute_label = AttributeLabel(
|
||||
module_id=module_id,
|
||||
attribute_id=attribute_id,
|
||||
label=label,
|
||||
)
|
||||
|
||||
self.session.add(attribute_label)
|
||||
await self.session.commit()
|
||||
|
||||
async def get_attribute_types(self) -> list[AttributeType]:
|
||||
stmt = select(AttributeType).where(AttributeType.is_deleted.is_(False))
|
||||
result = await self.session.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def get_deal_module_attributes(
|
||||
self, deal_id: int, module_id: int
|
||||
) -> list[tuple[Attribute, AttributeValue, AttributeLabel]]:
|
||||
stmt = (
|
||||
select(
|
||||
Attribute,
|
||||
AttributeValue,
|
||||
AttributeLabel,
|
||||
)
|
||||
.join(
|
||||
module_attribute,
|
||||
and_(
|
||||
module_attribute.c.attribute_id == Attribute.id,
|
||||
module_attribute.c.module_id == module_id,
|
||||
),
|
||||
)
|
||||
.outerjoin(
|
||||
AttributeValue,
|
||||
and_(
|
||||
AttributeValue.attribute_id == Attribute.id,
|
||||
AttributeValue.module_id == module_id,
|
||||
AttributeValue.deal_id == deal_id,
|
||||
),
|
||||
)
|
||||
.outerjoin(
|
||||
AttributeLabel,
|
||||
and_(
|
||||
AttributeLabel.attribute_id == Attribute.id,
|
||||
AttributeLabel.module_id == module_id,
|
||||
),
|
||||
)
|
||||
.where(Attribute.is_deleted.is_(False))
|
||||
.options(joinedload(Attribute.select))
|
||||
)
|
||||
result = await self.session.execute(stmt)
|
||||
return list(result.all())
|
||||
|
||||
async def _get_deals_attribute_values(
|
||||
self, deal_ids: list[int], module_id: int
|
||||
) -> list[AttributeValue]:
|
||||
stmt = (
|
||||
select(AttributeValue)
|
||||
.join(Attribute, AttributeValue.attribute_id == Attribute.id)
|
||||
.where(
|
||||
AttributeValue.deal_id.in_(deal_ids),
|
||||
AttributeValue.module_id == module_id,
|
||||
Attribute.is_deleted.is_(False),
|
||||
)
|
||||
)
|
||||
result = await self.session.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def update_or_create_deals_attribute_values(
|
||||
self,
|
||||
main_deal_id: int,
|
||||
group_deal_ids: list[int],
|
||||
module_id: int,
|
||||
attributes: list[UpdateDealModuleAttributeSchema],
|
||||
):
|
||||
old_deal_attribute_values: list[
|
||||
AttributeValue
|
||||
] = await self._get_deals_attribute_values(group_deal_ids, module_id)
|
||||
|
||||
dict_old_attrs: dict[int, dict[int, AttributeValue]] = defaultdict(dict)
|
||||
|
||||
for deal_attribute in old_deal_attribute_values:
|
||||
dict_old_attrs[deal_attribute.deal_id][deal_attribute.attribute_id] = (
|
||||
deal_attribute
|
||||
)
|
||||
|
||||
for attribute in attributes:
|
||||
if attribute.is_applicable_to_group:
|
||||
deal_ids_to_apply = group_deal_ids
|
||||
else:
|
||||
deal_ids_to_apply = [main_deal_id]
|
||||
|
||||
for deal_id in deal_ids_to_apply:
|
||||
if attribute.attribute_id in dict_old_attrs[deal_id]:
|
||||
attribute_value = dict_old_attrs[deal_id][attribute.attribute_id]
|
||||
attribute_value.value = attribute.value
|
||||
else:
|
||||
if attribute.value is None:
|
||||
continue
|
||||
|
||||
attribute_value = AttributeValue(
|
||||
attribute_id=attribute.attribute_id,
|
||||
deal_id=deal_id,
|
||||
module_id=module_id,
|
||||
value=attribute.value,
|
||||
)
|
||||
self.session.add(attribute_value)
|
||||
|
||||
await self.session.commit()
|
||||
@ -1,18 +0,0 @@
|
||||
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()
|
||||
@ -8,6 +8,7 @@ from modules.fulfillment_base.models import (
|
||||
DealProductService,
|
||||
DealProduct,
|
||||
)
|
||||
from repositories import AttributeRepository
|
||||
from repositories.mixins import *
|
||||
from schemas.base import SortDir
|
||||
from schemas.deal import UpdateDealSchema, CreateDealSchema
|
||||
@ -146,6 +147,15 @@ class DealRepository(
|
||||
result = await self.session.execute(stmt)
|
||||
return list(result.scalars().all())
|
||||
|
||||
async def _prepare_create(self, data: CreateDealSchema) -> dict:
|
||||
dumped = data.model_dump()
|
||||
del dumped["project_id"]
|
||||
return dumped
|
||||
|
||||
async def _after_create(self, obj: Deal, data: CreateDealSchema) -> None:
|
||||
attr_repo = AttributeRepository(self.session)
|
||||
await attr_repo.create_attributes_for_new_deal(obj.id, data.project_id)
|
||||
|
||||
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
|
||||
return stmt.options(joinedload(Deal.status), joinedload(Deal.board))
|
||||
|
||||
|
||||
@ -65,13 +65,14 @@ class RepUpdateMixin(Generic[EntityType, UpdateSchemaType], RepBaseMixin[EntityT
|
||||
data: UpdateSchemaType,
|
||||
with_commit: Optional[bool] = False,
|
||||
fields: Optional[list[str]] = None,
|
||||
set_if_value_is_not_none: Optional[bool] = True,
|
||||
) -> EntityType:
|
||||
if fields is None:
|
||||
fields = data.model_dump().keys()
|
||||
|
||||
for field in fields:
|
||||
value = getattr(data, field)
|
||||
if value is not None:
|
||||
if not set_if_value_is_not_none or value is not None:
|
||||
setattr(model, field, value)
|
||||
|
||||
if with_commit:
|
||||
|
||||
101
repositories/module.py
Normal file
101
repositories/module.py
Normal file
@ -0,0 +1,101 @@
|
||||
import uuid
|
||||
|
||||
from sqlalchemy import and_, or_
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from models import Module, Attribute, AttributeLabel, module_attribute, ModuleTab
|
||||
from models.module import DeviceType
|
||||
from repositories.mixins import *
|
||||
from schemas.module import UpdateModuleCommonInfoSchema, CreateModuleSchema
|
||||
|
||||
|
||||
class ModuleRepository(
|
||||
RepCrudMixin[Module, CreateModuleSchema, UpdateModuleCommonInfoSchema]
|
||||
):
|
||||
entity_class = Module
|
||||
|
||||
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
|
||||
return stmt.options(selectinload(Module.attributes).joinedload(Attribute.type))
|
||||
|
||||
async def get_by_ids(self, ids: list[int]) -> list[Module]:
|
||||
stmt = select(Module).where(Module.id.in_(ids))
|
||||
modules = await self.session.scalars(stmt)
|
||||
return modules.all()
|
||||
|
||||
@staticmethod
|
||||
def _get_stmt_modules_with_tuples() -> Select:
|
||||
return (
|
||||
select(Module, Attribute, AttributeLabel)
|
||||
.join(
|
||||
module_attribute,
|
||||
Module.id == module_attribute.c.module_id,
|
||||
isouter=True,
|
||||
)
|
||||
.join(
|
||||
Attribute, module_attribute.c.attribute_id == Attribute.id, isouter=True
|
||||
)
|
||||
.join(
|
||||
AttributeLabel,
|
||||
and_(
|
||||
Module.id == AttributeLabel.module_id,
|
||||
Attribute.id == AttributeLabel.attribute_id,
|
||||
),
|
||||
isouter=True,
|
||||
)
|
||||
.where(
|
||||
Module.is_deleted.is_(False),
|
||||
or_(Attribute.id.is_(None), Attribute.is_deleted.is_(False)),
|
||||
)
|
||||
.order_by(Module.id, Attribute.id)
|
||||
)
|
||||
|
||||
async def get_with_attributes_as_tuples(
|
||||
self,
|
||||
) -> list[tuple[Module, Attribute, AttributeLabel]]:
|
||||
stmt = self._get_stmt_modules_with_tuples()
|
||||
return (await self.session.execute(stmt)).unique().all()
|
||||
|
||||
async def get_with_attributes_as_tuple_by_id(
|
||||
self, pk: int
|
||||
) -> list[tuple[Module, Attribute, AttributeLabel]]:
|
||||
stmt = self._get_stmt_modules_with_tuples()
|
||||
stmt = stmt.where(Module.id == pk)
|
||||
return (await self.session.execute(stmt)).unique().all()
|
||||
|
||||
async def _prepare_create(self, data: CreateSchemaType) -> dict:
|
||||
dump = data.model_dump()
|
||||
dump["key"] = str(uuid.uuid4())
|
||||
return dump
|
||||
|
||||
async def _after_create(self, module: Module, _) -> None:
|
||||
tab = ModuleTab(
|
||||
key=module.key,
|
||||
label=module.label,
|
||||
icon_name=None,
|
||||
module_id=module.id,
|
||||
device=DeviceType.BOTH,
|
||||
)
|
||||
self.session.add(tab)
|
||||
|
||||
async def get_module_tabs_by_module_id(self, module_id: int) -> list[ModuleTab]:
|
||||
stmt = select(ModuleTab).where(ModuleTab.module_id == module_id)
|
||||
result = await self.session.scalars(stmt)
|
||||
return list(result.all())
|
||||
|
||||
async def update(
|
||||
self, module: Module, data: UpdateModuleCommonInfoSchema
|
||||
) -> Module:
|
||||
tabs = await self.get_module_tabs_by_module_id(module.id)
|
||||
for tab in tabs:
|
||||
tab.label = data.label
|
||||
self.session.add(tab)
|
||||
|
||||
return await self._apply_update_data_to_model(module, data, True)
|
||||
|
||||
async def add_attribute_to_module(self, module: Module, attribute: Attribute):
|
||||
module.attributes.append(attribute)
|
||||
await self.session.commit()
|
||||
|
||||
async def delete_attribute_from_module(self, module: Module, attribute: Attribute):
|
||||
module.attributes.remove(attribute)
|
||||
await self.session.commit()
|
||||
@ -2,7 +2,7 @@ from sqlalchemy.orm import selectinload
|
||||
|
||||
from models import DealTag
|
||||
from models.project import Project
|
||||
from repositories.built_in_module import BuiltInModuleRepository
|
||||
from repositories.module import ModuleRepository
|
||||
from repositories.mixins import *
|
||||
from schemas.project import CreateProjectSchema, UpdateProjectSchema
|
||||
|
||||
@ -26,10 +26,10 @@ class ProjectRepository(
|
||||
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(
|
||||
if data.modules is not None:
|
||||
modules = data.modules
|
||||
module_ids = [module.id for module in modules]
|
||||
data.modules = await ModuleRepository(
|
||||
self.session
|
||||
).get_by_ids(module_ids)
|
||||
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
PyMuPDF
|
||||
pdfrw
|
||||
fpdf
|
||||
reportlab
|
||||
fastapi
|
||||
SQLAlchemy
|
||||
pathlib
|
||||
python-dotenv
|
||||
starlette
|
||||
pydantic
|
||||
alembic
|
||||
56
routers/crm/v1/attr_option.py
Normal file
56
routers/crm/v1/attr_option.py
Normal file
@ -0,0 +1,56 @@
|
||||
from fastapi import APIRouter, Path
|
||||
|
||||
from backend.dependecies import SessionDependency
|
||||
from schemas.attr_option import *
|
||||
from services import AttrOptionService
|
||||
|
||||
router = APIRouter(tags=["attr_option"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{selectId}",
|
||||
response_model=GetAllAttrSelectOptionsResponse,
|
||||
operation_id="get_attr_options",
|
||||
)
|
||||
async def get_attr_options(
|
||||
session: SessionDependency,
|
||||
select_id: int = Path(alias="selectId"),
|
||||
):
|
||||
return await AttrOptionService(session).get_all(select_id)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/",
|
||||
response_model=CreateAttrOptionResponse,
|
||||
operation_id="create_attr_option",
|
||||
)
|
||||
async def create_attr_select(
|
||||
session: SessionDependency,
|
||||
request: CreateAttrOptionRequest,
|
||||
):
|
||||
return await AttrOptionService(session).create(request)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/{pk}",
|
||||
response_model=UpdateAttrOptionResponse,
|
||||
operation_id="update_attr_option",
|
||||
)
|
||||
async def update_attr_option(
|
||||
session: SessionDependency,
|
||||
request: UpdateAttrOptionRequest,
|
||||
pk: int = Path(),
|
||||
):
|
||||
return await AttrOptionService(session).update(pk, request)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{pk}",
|
||||
response_model=DeleteAttrOptionResponse,
|
||||
operation_id="delete_attr_option",
|
||||
)
|
||||
async def delete_attr_option(
|
||||
session: SessionDependency,
|
||||
pk: int = Path(),
|
||||
):
|
||||
return await AttrOptionService(session).delete(pk)
|
||||
55
routers/crm/v1/attr_select.py
Normal file
55
routers/crm/v1/attr_select.py
Normal file
@ -0,0 +1,55 @@
|
||||
from fastapi import APIRouter, Path
|
||||
|
||||
from backend.dependecies import SessionDependency
|
||||
from schemas.attr_select import *
|
||||
from services import AttrSelectService
|
||||
|
||||
router = APIRouter(tags=["attr_select"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=GetAllAttrSelectsResponse,
|
||||
operation_id="get_attr_selects",
|
||||
)
|
||||
async def get_attr_selects(
|
||||
session: SessionDependency,
|
||||
):
|
||||
return await AttrSelectService(session).get_all()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/",
|
||||
response_model=CreateAttrSelectResponse,
|
||||
operation_id="create_attr_select",
|
||||
)
|
||||
async def create_attr_select(
|
||||
session: SessionDependency,
|
||||
request: CreateAttrSelectRequest,
|
||||
):
|
||||
return await AttrSelectService(session).create(request)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/{pk}",
|
||||
response_model=UpdateAttrSelectResponse,
|
||||
operation_id="update_attr_select",
|
||||
)
|
||||
async def update_attr_select(
|
||||
session: SessionDependency,
|
||||
request: UpdateAttrSelectRequest,
|
||||
pk: int = Path(),
|
||||
):
|
||||
return await AttrSelectService(session).update(pk, request)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{pk}",
|
||||
response_model=DeleteAttrSelectResponse,
|
||||
operation_id="delete_attr_select",
|
||||
)
|
||||
async def delete_attr_select(
|
||||
session: SessionDependency,
|
||||
pk: int = Path(),
|
||||
):
|
||||
return await AttrSelectService(session).delete(pk)
|
||||
109
routers/crm/v1/attribute.py
Normal file
109
routers/crm/v1/attribute.py
Normal file
@ -0,0 +1,109 @@
|
||||
from fastapi import APIRouter, Path
|
||||
|
||||
from backend.dependecies import SessionDependency
|
||||
from schemas.attribute import *
|
||||
from services import AttributeService
|
||||
|
||||
router = APIRouter(tags=["attribute"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"/",
|
||||
response_model=GetAllAttributesResponse,
|
||||
operation_id="get_attributes",
|
||||
)
|
||||
async def get_attributes(
|
||||
session: SessionDependency,
|
||||
):
|
||||
return await AttributeService(session).get_all()
|
||||
|
||||
|
||||
@router.post(
|
||||
"/",
|
||||
response_model=CreateAttributeResponse,
|
||||
operation_id="create_attribute",
|
||||
)
|
||||
async def create_attribute(
|
||||
session: SessionDependency,
|
||||
request: CreateAttributeRequest,
|
||||
):
|
||||
return await AttributeService(session).create(request)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/{pk}",
|
||||
response_model=UpdateAttributeResponse,
|
||||
operation_id="update_attribute",
|
||||
)
|
||||
async def update_attribute(
|
||||
session: SessionDependency,
|
||||
request: UpdateAttributeRequest,
|
||||
pk: int = Path(),
|
||||
):
|
||||
return await AttributeService(session).update(pk, request)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{pk}",
|
||||
response_model=DeleteAttributeResponse,
|
||||
operation_id="delete_attribute",
|
||||
)
|
||||
async def delete_attribute(
|
||||
session: SessionDependency,
|
||||
pk: int = Path(),
|
||||
):
|
||||
return await AttributeService(session).delete(pk)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/label",
|
||||
response_model=UpdateAttributeLabelResponse,
|
||||
operation_id="update_attribute_label",
|
||||
)
|
||||
async def update_attribute_label(
|
||||
session: SessionDependency,
|
||||
request: UpdateAttributeLabelRequest,
|
||||
):
|
||||
return await AttributeService(session).update_attribute_label(request)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/type",
|
||||
response_model=GetAllAttributeTypesResponse,
|
||||
operation_id="get_attribute_types",
|
||||
)
|
||||
async def get_attribute_types(
|
||||
session: SessionDependency,
|
||||
):
|
||||
return await AttributeService(session).get_attribute_types()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/deal/{dealId}/module/{moduleId}",
|
||||
response_model=GetDealModuleAttributesResponse,
|
||||
operation_id="get_deal_module_attributes",
|
||||
)
|
||||
async def get_deal_module_attributes(
|
||||
session: SessionDependency,
|
||||
deal_id: int = Path(alias="dealId"),
|
||||
module_id: int = Path(alias="moduleId"),
|
||||
):
|
||||
return await AttributeService(session).get_deal_module_attributes(
|
||||
deal_id, module_id
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/deal/{dealId}/module/{moduleId}",
|
||||
response_model=UpdateDealModuleAttributesResponse,
|
||||
operation_id="update_deal_module_attributes",
|
||||
)
|
||||
async def update_deal_module_attributes(
|
||||
session: SessionDependency,
|
||||
request: UpdateDealModuleAttributesRequest,
|
||||
deal_id: int = Path(alias="dealId"),
|
||||
module_id: int = Path(alias="moduleId"),
|
||||
):
|
||||
return await AttributeService(session).update_deal_module_attributes(
|
||||
deal_id, module_id, request
|
||||
)
|
||||
@ -1,18 +1,104 @@
|
||||
from fastapi import APIRouter
|
||||
from fastapi import APIRouter, Path
|
||||
|
||||
from backend.dependecies import SessionDependency
|
||||
from schemas.module import GetAllBuiltInModulesResponse
|
||||
from services.built_in_module import BuiltInModuleService
|
||||
from schemas.module import *
|
||||
from services.module import ModuleService
|
||||
|
||||
router = APIRouter(tags=["modules"])
|
||||
|
||||
|
||||
@router.get(
|
||||
"/built-in/",
|
||||
response_model=GetAllBuiltInModulesResponse,
|
||||
operation_id="get_built_in_modules",
|
||||
"/",
|
||||
response_model=GetAllModulesResponse,
|
||||
operation_id="get_modules",
|
||||
)
|
||||
async def get_built_in_modules(
|
||||
async def get_modules(
|
||||
session: SessionDependency,
|
||||
):
|
||||
return await BuiltInModuleService(session).get_all()
|
||||
return await ModuleService(session).get_all()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/with-attributes",
|
||||
response_model=GetAllWithAttributesResponse,
|
||||
operation_id="get_modules_with_attributes",
|
||||
)
|
||||
async def get_modules_with_attributes(
|
||||
session: SessionDependency,
|
||||
):
|
||||
return await ModuleService(session).get_with_attributes()
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{pk}/with-attributes",
|
||||
response_model=GetByIdWithAttributesResponse,
|
||||
operation_id="get_module_with_attributes",
|
||||
)
|
||||
async def get_module_with_attributes(
|
||||
session: SessionDependency,
|
||||
pk: int = Path(),
|
||||
):
|
||||
return await ModuleService(session).get_by_id_with_attributes(pk)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/",
|
||||
response_model=CreateModuleResponse,
|
||||
operation_id="create_module",
|
||||
)
|
||||
async def create_module(
|
||||
session: SessionDependency,
|
||||
request: CreateModuleRequest,
|
||||
):
|
||||
return await ModuleService(session).create(request)
|
||||
|
||||
|
||||
@router.patch(
|
||||
"/{pk}/common-info",
|
||||
response_model=UpdateModuleCommonInfoResponse,
|
||||
operation_id="update_module",
|
||||
)
|
||||
async def update_module_common_info(
|
||||
session: SessionDependency,
|
||||
request: UpdateModuleCommonInfoRequest,
|
||||
pk: int = Path(),
|
||||
):
|
||||
return await ModuleService(session).update(pk, request)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{pk}",
|
||||
response_model=DeleteModuleResponse,
|
||||
operation_id="delete_module",
|
||||
)
|
||||
async def delete_module(
|
||||
session: SessionDependency,
|
||||
pk: int = Path(),
|
||||
):
|
||||
return await ModuleService(session).delete(pk)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{moduleId}/attribute/{attributeId}",
|
||||
response_model=AddAttributeResponse,
|
||||
operation_id="add_attribute_to_module",
|
||||
)
|
||||
async def add_attribute_to_module(
|
||||
session: SessionDependency,
|
||||
module_id: int = Path(alias="moduleId"),
|
||||
attribute_id: int = Path(alias="attributeId"),
|
||||
):
|
||||
return await ModuleService(session).add_attribute(module_id, attribute_id)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{moduleId}/attribute/{attributeId}",
|
||||
response_model=DeleteAttributeResponse,
|
||||
operation_id="remove_attribute_from_module",
|
||||
)
|
||||
async def remove_attribute_from_module(
|
||||
session: SessionDependency,
|
||||
module_id: int = Path(alias="moduleId"),
|
||||
attribute_id: int = Path(alias="attributeId"),
|
||||
):
|
||||
return await ModuleService(session).delete_attribute(module_id, attribute_id)
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
from fastapi import APIRouter, Path, Query
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Path, Query, File, UploadFile
|
||||
|
||||
from backend.dependecies import SessionDependency, PaginationDependency
|
||||
from modules.fulfillment_base.schemas.product import *
|
||||
@ -46,6 +48,19 @@ async def update_product(
|
||||
return await ProductService(session).update(pk, request)
|
||||
|
||||
|
||||
@router.post(
|
||||
"{pk}/images/upload",
|
||||
response_model=ProductUploadImageResponse,
|
||||
operation_id="upload_product_image",
|
||||
)
|
||||
async def upload_product_image(
|
||||
session: SessionDependency,
|
||||
upload_file: Annotated[UploadFile, File()],
|
||||
pk: int = Path(),
|
||||
):
|
||||
return await ProductService(session).upload_image(pk, upload_file)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/{pk}",
|
||||
response_model=DeleteProductResponse,
|
||||
@ -64,10 +79,52 @@ async def delete_product(
|
||||
response_model=GetProductBarcodePdfResponse,
|
||||
)
|
||||
async def get_product_barcode_pdf(
|
||||
request: GetProductBarcodePdfRequest, session: SessionDependency
|
||||
session: SessionDependency,
|
||||
request: GetProductBarcodePdfRequest,
|
||||
):
|
||||
service = BarcodePrinterService(session)
|
||||
filename, base64_string = await service.generate_base64(request)
|
||||
filename, base64_string = await service.generate_product_base64(request)
|
||||
return GetProductBarcodePdfResponse(
|
||||
base64_string=base64_string, filename=filename, mime_type="application/pdf"
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/barcode/for-deal/get-pdf",
|
||||
operation_id="get_deal_barcodes_pdf",
|
||||
response_model=GetDealBarcodesPdfResponse,
|
||||
)
|
||||
async def get_deal_barcodes_pdf(
|
||||
session: SessionDependency,
|
||||
request: GetDealBarcodesPdfRequest,
|
||||
):
|
||||
service = BarcodePrinterService(session)
|
||||
filename, base64_string = await service.generate_deal_base64(request)
|
||||
return GetProductBarcodePdfResponse(
|
||||
base64_string=base64_string, filename=filename, mime_type="application/pdf"
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"{pk}/barcode/image/upload",
|
||||
response_model=BarcodeUploadImageResponse,
|
||||
operation_id="upload_product_barcode_image",
|
||||
)
|
||||
async def upload_product_barcode_image(
|
||||
upload_file: UploadFile,
|
||||
session: SessionDependency,
|
||||
pk: int,
|
||||
):
|
||||
return await ProductService(session).upload_barcode_image(pk, upload_file)
|
||||
|
||||
|
||||
@router.delete(
|
||||
"{pk}/barcode/image",
|
||||
response_model=DeleteBarcodeImageResponse,
|
||||
operation_id="delete_product_barcode_image",
|
||||
)
|
||||
async def delete_product_barcode_image(
|
||||
session: SessionDependency,
|
||||
pk: int,
|
||||
):
|
||||
return await ProductService(session).delete_barcode_image(pk)
|
||||
|
||||
60
schemas/attr_option.py
Normal file
60
schemas/attr_option.py
Normal file
@ -0,0 +1,60 @@
|
||||
from typing import Optional
|
||||
|
||||
from schemas.base import BaseSchema, BaseResponse
|
||||
|
||||
|
||||
# region Entity
|
||||
|
||||
|
||||
class AttrOptionSchema(BaseSchema):
|
||||
id: int
|
||||
name: str
|
||||
lexorank: str
|
||||
|
||||
|
||||
class CreateAttrOptionSchema(BaseSchema):
|
||||
name: str
|
||||
lexorank: str
|
||||
select_id: int
|
||||
|
||||
|
||||
class UpdateAttrOptionSchema(BaseSchema):
|
||||
name: Optional[str] = None
|
||||
lexorank: Optional[str] = None
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region Request
|
||||
|
||||
|
||||
class CreateAttrOptionRequest(BaseSchema):
|
||||
entity: CreateAttrOptionSchema
|
||||
|
||||
|
||||
class UpdateAttrOptionRequest(BaseSchema):
|
||||
entity: UpdateAttrOptionSchema
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region Response
|
||||
|
||||
|
||||
class GetAllAttrSelectOptionsResponse(BaseSchema):
|
||||
items: list[AttrOptionSchema]
|
||||
|
||||
|
||||
class CreateAttrOptionResponse(BaseSchema):
|
||||
entity: AttrOptionSchema
|
||||
|
||||
|
||||
class UpdateAttrOptionResponse(BaseResponse):
|
||||
pass
|
||||
|
||||
|
||||
class DeleteAttrOptionResponse(BaseSchema):
|
||||
pass
|
||||
|
||||
|
||||
# endregion
|
||||
60
schemas/attr_select.py
Normal file
60
schemas/attr_select.py
Normal file
@ -0,0 +1,60 @@
|
||||
from schemas.attr_option import AttrOptionSchema
|
||||
from schemas.base import BaseSchema, BaseResponse
|
||||
|
||||
|
||||
# region Entity
|
||||
|
||||
|
||||
class AttrSelectSchema(BaseSchema):
|
||||
id: int
|
||||
name: str
|
||||
is_built_in: bool
|
||||
|
||||
|
||||
class CreateAttrSelectSchema(BaseSchema):
|
||||
name: str
|
||||
|
||||
|
||||
class UpdateAttrSelectSchema(BaseSchema):
|
||||
name: str
|
||||
|
||||
|
||||
class AttrSelectWithOptionsSchema(AttrSelectSchema):
|
||||
options: list[AttrOptionSchema]
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region Request
|
||||
|
||||
|
||||
class CreateAttrSelectRequest(BaseSchema):
|
||||
entity: CreateAttrSelectSchema
|
||||
|
||||
|
||||
class UpdateAttrSelectRequest(BaseSchema):
|
||||
entity: UpdateAttrSelectSchema
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region Response
|
||||
|
||||
|
||||
class GetAllAttrSelectsResponse(BaseSchema):
|
||||
items: list[AttrSelectSchema]
|
||||
|
||||
|
||||
class CreateAttrSelectResponse(BaseResponse):
|
||||
entity: AttrSelectSchema
|
||||
|
||||
|
||||
class UpdateAttrSelectResponse(BaseResponse):
|
||||
pass
|
||||
|
||||
|
||||
class DeleteAttrSelectResponse(BaseResponse):
|
||||
pass
|
||||
|
||||
|
||||
# endregion
|
||||
127
schemas/attribute.py
Normal file
127
schemas/attribute.py
Normal file
@ -0,0 +1,127 @@
|
||||
from typing import Optional, Any
|
||||
|
||||
from schemas.attr_select import AttrSelectSchema, AttrOptionSchema
|
||||
from schemas.base import BaseSchema, BaseResponse
|
||||
|
||||
|
||||
# region Entity
|
||||
|
||||
|
||||
class AttributeTypeSchema(BaseSchema):
|
||||
id: int
|
||||
type: str
|
||||
name: str
|
||||
|
||||
|
||||
class CreateAttributeSchema(BaseSchema):
|
||||
label: str
|
||||
is_applicable_to_group: bool
|
||||
is_nullable: bool
|
||||
default_value: Optional[Any]
|
||||
default_option_id: Optional[int] = None
|
||||
description: str
|
||||
type_id: int
|
||||
select_id: Optional[int] = None
|
||||
|
||||
|
||||
class AttributeSchema(CreateAttributeSchema):
|
||||
id: int
|
||||
is_built_in: bool
|
||||
type: AttributeTypeSchema
|
||||
default_option: Optional[AttrOptionSchema] = None
|
||||
select: Optional[AttrSelectSchema]
|
||||
|
||||
|
||||
class UpdateAttributeSchema(BaseSchema):
|
||||
label: Optional[str] = None
|
||||
is_applicable_to_group: Optional[bool] = None
|
||||
is_nullable: Optional[bool] = None
|
||||
default_value: Optional[Any] = None
|
||||
default_option_id: Optional[int] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
|
||||
class ModuleAttributeSchema(AttributeSchema):
|
||||
original_label: str
|
||||
|
||||
|
||||
class DealModuleAttributeSchema(BaseSchema):
|
||||
attribute_id: int
|
||||
label: str
|
||||
original_label: str
|
||||
value: Optional[Any]
|
||||
type: AttributeTypeSchema
|
||||
select: Optional[AttrSelectSchema]
|
||||
default_value: Any
|
||||
description: str
|
||||
is_applicable_to_group: bool
|
||||
is_nullable: bool
|
||||
|
||||
|
||||
class UpdateDealModuleAttributeSchema(BaseSchema):
|
||||
attribute_id: int
|
||||
is_applicable_to_group: bool
|
||||
value: Optional[Any] = None
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region Request
|
||||
|
||||
|
||||
class CreateAttributeRequest(BaseSchema):
|
||||
entity: CreateAttributeSchema
|
||||
|
||||
|
||||
class UpdateAttributeRequest(BaseSchema):
|
||||
entity: UpdateAttributeSchema
|
||||
|
||||
|
||||
class UpdateAttributeLabelRequest(BaseSchema):
|
||||
module_id: int
|
||||
attribute_id: int
|
||||
label: str
|
||||
|
||||
|
||||
class UpdateDealModuleAttributesRequest(BaseSchema):
|
||||
attributes: list[UpdateDealModuleAttributeSchema]
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region Response
|
||||
|
||||
|
||||
class GetAllAttributesResponse(BaseSchema):
|
||||
items: list[AttributeSchema]
|
||||
|
||||
|
||||
class CreateAttributeResponse(BaseResponse):
|
||||
pass
|
||||
|
||||
|
||||
class UpdateAttributeResponse(BaseResponse):
|
||||
pass
|
||||
|
||||
|
||||
class DeleteAttributeResponse(BaseResponse):
|
||||
pass
|
||||
|
||||
|
||||
class UpdateAttributeLabelResponse(BaseResponse):
|
||||
pass
|
||||
|
||||
|
||||
class GetAllAttributeTypesResponse(BaseSchema):
|
||||
items: list[AttributeTypeSchema]
|
||||
|
||||
|
||||
class GetDealModuleAttributesResponse(BaseSchema):
|
||||
attributes: list[DealModuleAttributeSchema]
|
||||
|
||||
|
||||
class UpdateDealModuleAttributesResponse(BaseResponse):
|
||||
pass
|
||||
|
||||
|
||||
# endregion
|
||||
@ -30,6 +30,7 @@ class DealSchema(BaseSchema):
|
||||
|
||||
class CreateDealSchema(BaseSchema):
|
||||
name: str
|
||||
project_id: int
|
||||
board_id: int
|
||||
lexorank: str
|
||||
status_id: int
|
||||
|
||||
@ -1,23 +1,56 @@
|
||||
from schemas.base import BaseSchema
|
||||
from typing import Optional
|
||||
|
||||
from schemas.attribute import ModuleAttributeSchema
|
||||
from schemas.base import BaseSchema, BaseResponse
|
||||
|
||||
|
||||
# region Entity
|
||||
|
||||
|
||||
class BuiltInModuleTabSchema(BaseSchema):
|
||||
class ModuleTabSchema(BaseSchema):
|
||||
id: int
|
||||
key: str
|
||||
label: str
|
||||
icon_name: str
|
||||
icon_name: Optional[str]
|
||||
device: str
|
||||
|
||||
|
||||
class BuiltInModuleSchema(BaseSchema):
|
||||
class ModuleSchema(BaseSchema):
|
||||
id: int
|
||||
key: str
|
||||
label: str
|
||||
description: str
|
||||
depends_on: list["BuiltInModuleSchema"]
|
||||
tabs: list[BuiltInModuleTabSchema]
|
||||
description: Optional[str]
|
||||
is_built_in: bool
|
||||
depends_on: list["ModuleSchema"]
|
||||
tabs: list[ModuleTabSchema]
|
||||
|
||||
|
||||
class ModuleWithAttributesSchema(ModuleSchema):
|
||||
attributes: list[ModuleAttributeSchema]
|
||||
|
||||
|
||||
class CreateModuleSchema(BaseSchema):
|
||||
label: str
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
class UpdateModuleCommonInfoSchema(BaseSchema):
|
||||
label: str
|
||||
description: Optional[str]
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
|
||||
# region Requests
|
||||
|
||||
|
||||
class CreateModuleRequest(BaseSchema):
|
||||
entity: CreateModuleSchema
|
||||
|
||||
|
||||
class UpdateModuleCommonInfoRequest(BaseSchema):
|
||||
entity: UpdateModuleCommonInfoSchema
|
||||
|
||||
|
||||
# endregion
|
||||
@ -25,8 +58,36 @@ class BuiltInModuleSchema(BaseSchema):
|
||||
# region Response
|
||||
|
||||
|
||||
class GetAllBuiltInModulesResponse(BaseSchema):
|
||||
items: list[BuiltInModuleSchema]
|
||||
class GetAllModulesResponse(BaseSchema):
|
||||
items: list[ModuleSchema]
|
||||
|
||||
|
||||
class GetAllWithAttributesResponse(BaseSchema):
|
||||
items: list[ModuleWithAttributesSchema]
|
||||
|
||||
|
||||
class GetByIdWithAttributesResponse(BaseSchema):
|
||||
entity: ModuleWithAttributesSchema
|
||||
|
||||
|
||||
class CreateModuleResponse(BaseResponse):
|
||||
pass
|
||||
|
||||
|
||||
class UpdateModuleCommonInfoResponse(BaseResponse):
|
||||
pass
|
||||
|
||||
|
||||
class DeleteModuleResponse(BaseResponse):
|
||||
pass
|
||||
|
||||
|
||||
class AddAttributeResponse(BaseResponse):
|
||||
pass
|
||||
|
||||
|
||||
class DeleteAttributeResponse(BaseResponse):
|
||||
pass
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
@ -2,7 +2,7 @@ from typing import Optional
|
||||
|
||||
from schemas.base import BaseSchema, BaseResponse
|
||||
from schemas.deal_tag import DealTagSchema
|
||||
from schemas.module import BuiltInModuleSchema
|
||||
from schemas.module import ModuleSchema
|
||||
|
||||
|
||||
# region Entity
|
||||
@ -11,7 +11,7 @@ from schemas.module import BuiltInModuleSchema
|
||||
class ProjectSchema(BaseSchema):
|
||||
id: int
|
||||
name: str
|
||||
built_in_modules: list[BuiltInModuleSchema]
|
||||
modules: list[ModuleSchema]
|
||||
tags: list[DealTagSchema]
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ class CreateProjectSchema(BaseSchema):
|
||||
|
||||
class UpdateProjectSchema(BaseSchema):
|
||||
name: Optional[str] = None
|
||||
built_in_modules: list[BuiltInModuleSchema] = None
|
||||
modules: list[ModuleSchema] = None
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
@ -1,6 +1,9 @@
|
||||
from .attr_select import AttrSelectService as AttrSelectService
|
||||
from .attribute import AttributeService as AttributeService
|
||||
from .board import BoardService as BoardService
|
||||
from .deal import DealService as DealService
|
||||
from .project import ProjectService as ProjectService
|
||||
from .status import StatusService as StatusService
|
||||
from .deal_group import DealGroupService as DealGroupService
|
||||
from .deal_tag import DealTagService as DealTagService
|
||||
from .project import ProjectService as ProjectService
|
||||
from .status import StatusService as StatusService
|
||||
from .attr_option import AttrOptionService as AttrOptionService
|
||||
|
||||
27
services/attr_option.py
Normal file
27
services/attr_option.py
Normal file
@ -0,0 +1,27 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models import AttributeOption
|
||||
from repositories import AttrOptionRepository
|
||||
from schemas.attr_option import (
|
||||
AttrOptionSchema,
|
||||
CreateAttrOptionRequest,
|
||||
UpdateAttrOptionRequest,
|
||||
)
|
||||
from services.mixins import ServiceCrudMixin
|
||||
|
||||
|
||||
class AttrOptionService(
|
||||
ServiceCrudMixin[
|
||||
AttributeOption,
|
||||
AttrOptionSchema,
|
||||
CreateAttrOptionRequest,
|
||||
UpdateAttrOptionRequest,
|
||||
]
|
||||
):
|
||||
schema_class = AttrOptionSchema
|
||||
entity_deleted_msg = "Опция успешно удалена"
|
||||
entity_updated_msg = "Опция успешно обновлена"
|
||||
entity_created_msg = "Опция успешно создана"
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.repository = AttrOptionRepository(session)
|
||||
24
services/attr_select.py
Normal file
24
services/attr_select.py
Normal file
@ -0,0 +1,24 @@
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from models import AttributeSelect
|
||||
from repositories import AttrSelectRepository
|
||||
from schemas.attr_select import (
|
||||
AttrSelectSchema,
|
||||
CreateAttrSelectRequest,
|
||||
UpdateAttrSelectRequest,
|
||||
)
|
||||
from services.mixins import ServiceCrudMixin
|
||||
|
||||
|
||||
class AttrSelectService(
|
||||
ServiceCrudMixin[
|
||||
AttributeSelect,
|
||||
AttrSelectSchema,
|
||||
CreateAttrSelectRequest,
|
||||
UpdateAttrSelectRequest,
|
||||
]
|
||||
):
|
||||
schema_class = AttrSelectSchema
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.repository = AttrSelectRepository(session)
|
||||
76
services/attribute.py
Normal file
76
services/attribute.py
Normal file
@ -0,0 +1,76 @@
|
||||
from models import Attribute, AttributeValue, AttributeLabel
|
||||
from repositories import AttributeRepository, DealRepository
|
||||
from schemas.attribute import *
|
||||
from services.mixins import *
|
||||
|
||||
|
||||
class AttributeService(
|
||||
ServiceCrudMixin[
|
||||
Attribute, AttributeSchema, CreateAttributeRequest, UpdateAttributeRequest
|
||||
]
|
||||
):
|
||||
schema_class = AttributeSchema
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.repository = AttributeRepository(session)
|
||||
|
||||
async def update_attribute_label(
|
||||
self, request: UpdateAttributeLabelRequest
|
||||
) -> UpdateAttributeLabelResponse:
|
||||
await self.repository.create_or_update_attribute_label(
|
||||
request.module_id, request.attribute_id, request.label
|
||||
)
|
||||
return UpdateAttributeLabelResponse(
|
||||
message="Название атрибута в модуле изменено"
|
||||
)
|
||||
|
||||
async def get_attribute_types(self) -> GetAllAttributeTypesResponse:
|
||||
types = await self.repository.get_attribute_types()
|
||||
return GetAllAttributeTypesResponse(
|
||||
items=[AttributeTypeSchema.model_validate(t) for t in types]
|
||||
)
|
||||
|
||||
async def get_deal_module_attributes(
|
||||
self, deal_id: int, module_id: int
|
||||
) -> GetDealModuleAttributesResponse:
|
||||
deal_attributes: list[
|
||||
tuple[Attribute, AttributeValue, AttributeLabel]
|
||||
] = await self.repository.get_deal_module_attributes(deal_id, module_id)
|
||||
|
||||
attributes = []
|
||||
for attr, attr_value, attr_label in deal_attributes:
|
||||
select_schema = (
|
||||
AttrSelectSchema.model_validate(attr.select) if attr.select else None
|
||||
)
|
||||
|
||||
attribute = DealModuleAttributeSchema(
|
||||
attribute_id=attr.id,
|
||||
label=attr_label.label if attr_label else attr.label,
|
||||
original_label=attr.label,
|
||||
value=attr_value.value if attr_value else None,
|
||||
type=AttributeTypeSchema.model_validate(attr.type),
|
||||
select=select_schema,
|
||||
default_value=attr.default_value,
|
||||
description=attr.description,
|
||||
is_applicable_to_group=attr.is_applicable_to_group,
|
||||
is_nullable=attr.is_nullable,
|
||||
)
|
||||
attributes.append(attribute)
|
||||
|
||||
return GetDealModuleAttributesResponse(attributes=attributes)
|
||||
|
||||
async def update_deal_module_attributes(
|
||||
self, deal_id: int, module_id: int, request: UpdateDealModuleAttributesRequest
|
||||
) -> UpdateDealModuleAttributesResponse:
|
||||
deal_repo = DealRepository(self.repository.session)
|
||||
deal = await deal_repo.get_by_id(deal_id)
|
||||
if deal.group_id:
|
||||
deals = await deal_repo.get_by_group_id(deal.group_id)
|
||||
else:
|
||||
deals = [deal]
|
||||
|
||||
group_deal_ids = [d.id for d in deals]
|
||||
await self.repository.update_or_create_deals_attribute_values(
|
||||
deal_id, group_deal_ids, module_id, request.attributes
|
||||
)
|
||||
return UpdateDealModuleAttributesResponse(message="Успешно сохранено")
|
||||
@ -1,11 +0,0 @@
|
||||
from models import BuiltInModule
|
||||
from repositories import BuiltInModuleRepository
|
||||
from schemas.module import BuiltInModuleSchema
|
||||
from services.mixins import *
|
||||
|
||||
|
||||
class BuiltInModuleService(ServiceGetAllMixin[BuiltInModule, BuiltInModuleSchema]):
|
||||
schema_class = BuiltInModuleSchema
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.repository = BuiltInModuleRepository(session)
|
||||
@ -1,5 +1,4 @@
|
||||
from lexorank import lexorank
|
||||
|
||||
import lexorank
|
||||
from models import DealGroup, Deal
|
||||
from repositories import DealGroupRepository, DealRepository, DealTagRepository
|
||||
from schemas.deal_group import *
|
||||
|
||||
93
services/module.py
Normal file
93
services/module.py
Normal file
@ -0,0 +1,93 @@
|
||||
from models import Module, Attribute
|
||||
from repositories import ModuleRepository, AttributeRepository
|
||||
from schemas.module import *
|
||||
from services.mixins import *
|
||||
from utils.exceptions import ForbiddenException
|
||||
|
||||
|
||||
class ModuleService(
|
||||
ServiceGetAllMixin[Module, ModuleSchema],
|
||||
ServiceCreateMixin[Module, CreateModuleRequest, ModuleSchema],
|
||||
ServiceUpdateMixin[Module, UpdateModuleCommonInfoRequest],
|
||||
ServiceDeleteMixin[Module],
|
||||
):
|
||||
schema_class = ModuleSchema
|
||||
entity_updated_msg = "Модуль успешно обновлен"
|
||||
entity_deleted_msg = "Модуль успешно удален"
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.repository = ModuleRepository(session)
|
||||
|
||||
async def get_with_attributes(self) -> GetAllWithAttributesResponse:
|
||||
result = await self.repository.get_with_attributes_as_tuples()
|
||||
modules = self._build_modules_with_attributes(result)
|
||||
return GetAllWithAttributesResponse(items=list(modules.values()))
|
||||
|
||||
async def get_by_id_with_attributes(self, pk: int) -> GetByIdWithAttributesResponse:
|
||||
result = await self.repository.get_with_attributes_as_tuple_by_id(pk)
|
||||
modules = self._build_modules_with_attributes(result)
|
||||
module = next(iter(modules.values()), None)
|
||||
if module is None:
|
||||
raise ObjectNotFoundException(f"Модуль с ID {pk} не найден")
|
||||
return GetByIdWithAttributesResponse(entity=module)
|
||||
|
||||
@staticmethod
|
||||
def _build_modules_with_attributes(
|
||||
result: list[tuple],
|
||||
) -> dict[int, ModuleWithAttributesSchema]:
|
||||
module_attrs_dict: dict[int, ModuleWithAttributesSchema] = {}
|
||||
|
||||
for module, attribute, attribute_label in result:
|
||||
new_attr = None
|
||||
|
||||
if attribute:
|
||||
original_label = attribute.label
|
||||
label = attribute_label.label if attribute_label else original_label
|
||||
attr_values = {
|
||||
**attribute.__dict__,
|
||||
"label": label,
|
||||
"original_label": original_label,
|
||||
}
|
||||
new_attr = ModuleAttributeSchema(**attr_values)
|
||||
|
||||
module_schema = module_attrs_dict.get(module.id)
|
||||
if not module_schema:
|
||||
module_data = module.__dict__
|
||||
del module_data["attributes"]
|
||||
module_schema = ModuleWithAttributesSchema(
|
||||
**module_data,
|
||||
attributes=[new_attr] if new_attr else [],
|
||||
)
|
||||
module_attrs_dict[module.id] = module_schema
|
||||
elif new_attr:
|
||||
module_schema.attributes.append(new_attr)
|
||||
|
||||
return module_attrs_dict
|
||||
|
||||
async def is_soft_delete(self, module: Module) -> bool:
|
||||
if module.is_built_in:
|
||||
raise ForbiddenException("Нельзя менять встроенный модуль")
|
||||
return True
|
||||
|
||||
async def add_attribute(self, module_id: int, attribute_id: int) -> AddAttributeResponse:
|
||||
module, attribute = await self._get_module_and_attr_from_request(module_id, attribute_id)
|
||||
await self.repository.add_attribute_to_module(module, attribute)
|
||||
return AddAttributeResponse(message="Аттрибут успешно добавлен к модулю")
|
||||
|
||||
async def delete_attribute(
|
||||
self, module_id: int, attribute_id: int
|
||||
) -> DeleteAttributeResponse:
|
||||
module, attribute = await self._get_module_and_attr_from_request(module_id, attribute_id)
|
||||
await self.repository.delete_attribute_from_module(module, attribute)
|
||||
return DeleteAttributeResponse(message="Аттрибут успешно удален из модуля")
|
||||
|
||||
async def _get_module_and_attr_from_request(
|
||||
self, module_id: int, attribute_id: int,
|
||||
) -> tuple[Module, Attribute]:
|
||||
module = await self.repository.get_by_id(module_id)
|
||||
if module.is_built_in:
|
||||
raise ForbiddenException("Нельзя менять встроенный модуль")
|
||||
|
||||
attr_repo = AttributeRepository(self.repository.session)
|
||||
attribute = await attr_repo.get_by_id(attribute_id)
|
||||
return module, attribute
|
||||
1
task_management/__init__.py
Normal file
1
task_management/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
from .taskiq_broker import broker as broker
|
||||
15
task_management/taskiq_broker.py
Normal file
15
task_management/taskiq_broker.py
Normal file
@ -0,0 +1,15 @@
|
||||
from taskiq_aio_pika import AioPikaBroker
|
||||
from taskiq_postgresql import PostgresqlResultBackend
|
||||
|
||||
from backend.config import RABBITMQ_URL
|
||||
from backend.session import DATABASE_URL
|
||||
|
||||
result_backend = PostgresqlResultBackend(
|
||||
dsn=DATABASE_URL,
|
||||
)
|
||||
|
||||
broker = AioPikaBroker(
|
||||
RABBITMQ_URL,
|
||||
).with_result_backend(
|
||||
result_backend,
|
||||
)
|
||||
@ -1,3 +1,11 @@
|
||||
class ObjectNotFoundException(Exception):
|
||||
class BaseResponseException(Exception):
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
|
||||
|
||||
class ObjectNotFoundException(BaseResponseException):
|
||||
pass
|
||||
|
||||
|
||||
class ForbiddenException(BaseResponseException):
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user