Compare commits
26 Commits
d7c7d1775f
...
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 | |||
| 44f00b1057 | |||
| ffee658349 |
11
.env.example
11
.env.example
@ -5,3 +5,14 @@ PG_DATABASE=
|
|||||||
PG_HOST=
|
PG_HOST=
|
||||||
|
|
||||||
SECRET_KEY=
|
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")
|
PG_HOST = os.environ.get("PG_HOST")
|
||||||
|
|
||||||
SECRET_KEY = os.environ.get("SECRET_KEY")
|
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.formatter import JsonFormatter
|
||||||
from logger.gunzip_rotating_file_handler import GunZipRotatingFileHandler
|
from logger.gunzip_rotating_file_handler import GunZipRotatingFileHandler
|
||||||
from logger.filters import LevelFilter, RequestIdFilter
|
from logger.filters import LevelFilter, RequestIdFilter
|
||||||
from core.singleton import Singleton
|
from utils.singleton import Singleton
|
||||||
|
|
||||||
|
|
||||||
class LoggerBuilder(metaclass=Singleton):
|
class LoggerBuilder(metaclass=Singleton):
|
||||||
|
|||||||
49
main.py
49
main.py
@ -1,41 +1,28 @@
|
|||||||
from fastapi import FastAPI, Request
|
import taskiq_fastapi
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi import FastAPI
|
||||||
from fastapi.middleware.gzip import GZipMiddleware
|
|
||||||
from fastapi.responses import ORJSONResponse
|
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
from starlette.responses import JSONResponse
|
|
||||||
|
|
||||||
import modules
|
|
||||||
import routers
|
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.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,
|
separate_input_output_schemas=True,
|
||||||
default_response_class=ORJSONResponse,
|
default_response_class=settings.DEFAULT_RESPONSE_CLASS,
|
||||||
root_path="/api",
|
root_path=settings.ROOT_PATH,
|
||||||
)
|
# lifespan=lifespan,
|
||||||
|
)
|
||||||
|
|
||||||
app.add_middleware(
|
register_middlewares(app)
|
||||||
CORSMiddleware,
|
register_exception_handlers(app)
|
||||||
allow_origins=origins,
|
auto_include_routers(app, routers, full_path=True)
|
||||||
allow_credentials=True,
|
app.mount("/static", StaticFiles(directory=settings.STATIC_DIR), name="static")
|
||||||
allow_methods=["*"],
|
|
||||||
allow_headers=["*"],
|
taskiq_fastapi.init(broker, "main:app")
|
||||||
)
|
return app
|
||||||
app.add_middleware(
|
|
||||||
GZipMiddleware,
|
|
||||||
minimum_size=1_000,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.exception_handler(ObjectNotFoundException)
|
app = create_app()
|
||||||
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")
|
|
||||||
|
|||||||
@ -1,15 +1,33 @@
|
|||||||
from sqlalchemy.orm import configure_mappers
|
from sqlalchemy.orm import configure_mappers
|
||||||
|
|
||||||
from modules.fulfillment_base.models import * # noqa: F401
|
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 .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_group import DealGroup as DealGroup
|
||||||
|
from .deal_tag import (
|
||||||
|
DealTag as DealTag,
|
||||||
|
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 .project import Project as Project
|
||||||
from .status import Status as Status, DealStatusHistory as DealStatusHistory
|
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):
|
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")
|
||||||
|
|||||||
@ -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
|
from models.mixins import SoftDeleteMixin, CreatedAtMixin, IdMixin
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from models import Status, Board, DealStatusHistory, DealGroup
|
from models import (
|
||||||
|
Status,
|
||||||
|
Board,
|
||||||
|
DealStatusHistory,
|
||||||
|
DealGroup,
|
||||||
|
DealTag,
|
||||||
|
AttributeValue,
|
||||||
|
)
|
||||||
from modules.clients.models import Client
|
from modules.clients.models import Client
|
||||||
|
|
||||||
|
|
||||||
@ -41,6 +48,18 @@ class Deal(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin):
|
|||||||
lazy="noload", back_populates="deals"
|
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)",
|
||||||
|
)
|
||||||
|
|
||||||
|
attributes_values: Mapped[list["AttributeValue"]] = relationship(
|
||||||
|
back_populates="deal",
|
||||||
|
)
|
||||||
|
|
||||||
# module client
|
# module client
|
||||||
client_id: Mapped[Optional[int]] = mapped_column(
|
client_id: Mapped[Optional[int]] = mapped_column(
|
||||||
ForeignKey("clients.id", ondelete="CASCADE"),
|
ForeignKey("clients.id", ondelete="CASCADE"),
|
||||||
|
|||||||
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"),
|
||||||
|
)
|
||||||
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
|
from models.mixins import SoftDeleteMixin, CreatedAtMixin, IdMixin
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from models import Board, BuiltInModule
|
from models import Board, Module, DealTag
|
||||||
|
|
||||||
|
|
||||||
class Project(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin):
|
class Project(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin):
|
||||||
@ -19,9 +19,16 @@ class Project(BaseModel, IdMixin, SoftDeleteMixin, CreatedAtMixin):
|
|||||||
lazy="noload",
|
lazy="noload",
|
||||||
)
|
)
|
||||||
|
|
||||||
built_in_modules: Mapped[list["BuiltInModule"]] = relationship(
|
modules: Mapped[list["Module"]] = relationship(
|
||||||
secondary="project_built_in_module",
|
secondary="project_module",
|
||||||
back_populates="projects",
|
back_populates="projects",
|
||||||
lazy="selectin",
|
lazy="selectin",
|
||||||
order_by="asc(BuiltInModule.id)",
|
order_by="asc(Module.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",
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
from reportlab.graphics.barcode import code128
|
from reportlab.graphics.barcode import code128
|
||||||
from reportlab.lib.units import mm
|
from reportlab.lib.units import mm
|
||||||
from reportlab.platypus import Spacer, PageBreak, Paragraph
|
from reportlab.platypus import Spacer, PageBreak, Paragraph
|
||||||
@ -24,7 +25,7 @@ class BarcodePdfGenerator(PDFGenerator):
|
|||||||
return None
|
return None
|
||||||
return value
|
return value
|
||||||
|
|
||||||
def generate(
|
async def generate(
|
||||||
self, barcodes_data: list[BarcodeData | PdfBarcodeImageGenData]
|
self, barcodes_data: list[BarcodeData | PdfBarcodeImageGenData]
|
||||||
) -> BytesIO:
|
) -> BytesIO:
|
||||||
pdf_barcodes_gen_data: list[PdfBarcodeGenData | PdfBarcodeImageGenData] = []
|
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]
|
self, barcodes_data: list[PdfBarcodeGenData | PdfBarcodeImageGenData]
|
||||||
) -> BytesIO:
|
) -> BytesIO:
|
||||||
pdf_maker = PdfMaker((self.page_width, self.page_height))
|
pdf_maker = PdfMaker((self.page_width, self.page_height))
|
||||||
@ -63,9 +64,10 @@ class BarcodePdfGenerator(PDFGenerator):
|
|||||||
|
|
||||||
for barcode_data in barcodes_data:
|
for barcode_data in barcodes_data:
|
||||||
if "barcode_value" in barcode_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:
|
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())
|
pdf_files.append(self._generate_spacers())
|
||||||
|
|
||||||
for file in pdf_files[:-1]:
|
for file in pdf_files[:-1]:
|
||||||
@ -138,11 +140,18 @@ class BarcodePdfGenerator(PDFGenerator):
|
|||||||
buffer.seek(0)
|
buffer.seek(0)
|
||||||
return buffer
|
return buffer
|
||||||
|
|
||||||
def _generate_for_one_product_using_img(
|
async def _generate_for_one_product_using_img(
|
||||||
self, barcode_data: PdfBarcodeImageGenData
|
self, barcode_data: PdfBarcodeImageGenData
|
||||||
) -> BytesIO:
|
) -> BytesIO:
|
||||||
with open(barcode_data["barcode_image_url"], "rb") as pdf_file:
|
pdf_url = barcode_data["barcode_image_url"]
|
||||||
pdf_bytes = pdf_file.read()
|
|
||||||
|
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))
|
pdf_maker = PdfMaker((self.page_width, self.page_height))
|
||||||
for _ in range(barcode_data["num_duplicates"]):
|
for _ in range(barcode_data["num_duplicates"]):
|
||||||
|
|||||||
@ -33,4 +33,4 @@ class ProductBarcodeImage(BaseModel):
|
|||||||
)
|
)
|
||||||
product: Mapped["Product"] = relationship(back_populates="barcode_image")
|
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(
|
barcode_image: Mapped["ProductBarcodeImage"] = relationship(
|
||||||
back_populates="product",
|
back_populates="product",
|
||||||
lazy="joined",
|
lazy="joined",
|
||||||
uselist=False,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -26,6 +26,7 @@ class DealProductRepository(
|
|||||||
return (
|
return (
|
||||||
stmt.options(
|
stmt.options(
|
||||||
joinedload(DealProduct.product).selectinload(Product.barcodes),
|
joinedload(DealProduct.product).selectinload(Product.barcodes),
|
||||||
|
joinedload(DealProduct.product).joinedload(Product.client),
|
||||||
selectinload(DealProduct.product_services).joinedload(
|
selectinload(DealProduct.product_services).joinedload(
|
||||||
DealProductService.service
|
DealProductService.service
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
from sqlalchemy import or_, delete
|
from sqlalchemy import or_, delete
|
||||||
from sqlalchemy.orm import selectinload, joinedload
|
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 (
|
from modules.fulfillment_base.schemas.product import (
|
||||||
CreateProductSchema,
|
CreateProductSchema,
|
||||||
UpdateProductSchema,
|
UpdateProductSchema,
|
||||||
@ -16,6 +17,7 @@ class ProductRepository(
|
|||||||
RepUpdateMixin[Product, UpdateProductSchema],
|
RepUpdateMixin[Product, UpdateProductSchema],
|
||||||
RepGetByIdMixin[Product],
|
RepGetByIdMixin[Product],
|
||||||
):
|
):
|
||||||
|
session: AsyncSession
|
||||||
entity_class = Product
|
entity_class = Product
|
||||||
entity_not_found_msg = "Товар не найден"
|
entity_not_found_msg = "Товар не найден"
|
||||||
|
|
||||||
@ -95,3 +97,36 @@ class ProductRepository(
|
|||||||
await self._update_barcodes(product, data.barcodes)
|
await self._update_barcodes(product, data.barcodes)
|
||||||
del data.barcodes
|
del data.barcodes
|
||||||
return await self._apply_update_data_to_model(product, data, True)
|
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 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.models import ProductBarcode
|
||||||
from modules.fulfillment_base.schemas.barcode_template import BarcodeTemplateSchema
|
from modules.fulfillment_base.schemas.barcode_template import BarcodeTemplateSchema
|
||||||
@ -16,6 +16,11 @@ class ProductImageSchema(BaseSchema):
|
|||||||
image_url: str
|
image_url: str
|
||||||
|
|
||||||
|
|
||||||
|
class ProductBarcodeImageSchema(BaseSchema):
|
||||||
|
product_id: int
|
||||||
|
image_url: str
|
||||||
|
|
||||||
|
|
||||||
class CreateProductSchema(BaseSchema):
|
class CreateProductSchema(BaseSchema):
|
||||||
name: str
|
name: str
|
||||||
article: str
|
article: str
|
||||||
@ -27,10 +32,27 @@ class CreateProductSchema(BaseSchema):
|
|||||||
composition: Optional[str]
|
composition: Optional[str]
|
||||||
size: Optional[str]
|
size: Optional[str]
|
||||||
additional_info: 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
|
id: int
|
||||||
barcode_template: BarcodeTemplateSchema
|
barcode_template: BarcodeTemplateSchema
|
||||||
|
|
||||||
@ -75,6 +97,10 @@ class GetProductBarcodePdfRequest(BaseSchema):
|
|||||||
barcode: str
|
barcode: str
|
||||||
|
|
||||||
|
|
||||||
|
class GetDealBarcodesPdfRequest(BaseSchema):
|
||||||
|
deal_id: int
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|
||||||
# region Response
|
# region Response
|
||||||
@ -93,6 +119,10 @@ class UpdateProductResponse(BaseResponse):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ProductUploadImageResponse(BaseResponse):
|
||||||
|
image_url: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class DeleteProductResponse(BaseResponse):
|
class DeleteProductResponse(BaseResponse):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -101,4 +131,16 @@ class GetProductBarcodePdfResponse(BasePdfResponse):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class GetDealBarcodesPdfResponse(BasePdfResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class BarcodeUploadImageResponse(BaseResponse):
|
||||||
|
image_url: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteBarcodeImageResponse(BaseResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|||||||
@ -4,9 +4,16 @@ from io import BytesIO
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from modules.fulfillment_base.barcodes_pdf_gen import BarcodePdfGenerator, BarcodeData
|
from modules.fulfillment_base.barcodes_pdf_gen import BarcodePdfGenerator, BarcodeData
|
||||||
from modules.fulfillment_base.models import Product
|
from modules.fulfillment_base.barcodes_pdf_gen.types import PdfBarcodeImageGenData
|
||||||
from modules.fulfillment_base.repositories import ProductRepository
|
from modules.fulfillment_base.models import Product, DealProduct
|
||||||
from modules.fulfillment_base.schemas.product import GetProductBarcodePdfRequest
|
from modules.fulfillment_base.repositories import (
|
||||||
|
ProductRepository,
|
||||||
|
DealProductRepository,
|
||||||
|
)
|
||||||
|
from modules.fulfillment_base.schemas.product import (
|
||||||
|
GetProductBarcodePdfRequest,
|
||||||
|
GetDealBarcodesPdfRequest,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class BarcodePrinterService:
|
class BarcodePrinterService:
|
||||||
@ -15,27 +22,75 @@ class BarcodePrinterService:
|
|||||||
def __init__(self, session: AsyncSession):
|
def __init__(self, session: AsyncSession):
|
||||||
self.session = session
|
self.session = session
|
||||||
|
|
||||||
async def generate_pdf(
|
async def generate_product_pdf(
|
||||||
self, request: GetProductBarcodePdfRequest
|
self, request: GetProductBarcodePdfRequest
|
||||||
) -> tuple[str, BytesIO]:
|
) -> tuple[str, BytesIO]:
|
||||||
product: Product = await ProductRepository(self.session).get_by_id(
|
product: Product = await ProductRepository(self.session).get_by_id(
|
||||||
request.product_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_data: BarcodeData = {
|
||||||
"barcode": request.barcode,
|
"barcode": request.barcode,
|
||||||
"template": product.barcode_template,
|
"template": product.barcode_template,
|
||||||
"product": product,
|
"product": product,
|
||||||
"num_duplicates": request.quantity,
|
"num_duplicates": request.quantity,
|
||||||
}
|
}
|
||||||
|
|
||||||
filename = f"{product.id}_barcode.pdf"
|
filename = f"{product.id}_barcode.pdf"
|
||||||
|
|
||||||
size = product.barcode_template.size
|
size = product.barcode_template.size
|
||||||
generator = BarcodePdfGenerator(size.width, size.height)
|
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
|
self, request: GetProductBarcodePdfRequest
|
||||||
) -> tuple[str, str]:
|
) -> 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")
|
base64_string = base64.b64encode(pdf_buffer.read()).decode("utf-8")
|
||||||
return filename, base64_string
|
return filename, base64_string
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
import math
|
import math
|
||||||
|
|
||||||
|
from fastapi import UploadFile
|
||||||
|
|
||||||
|
from external.s3_uploader import S3Uploader
|
||||||
from modules.fulfillment_base.models import Product
|
from modules.fulfillment_base.models import Product
|
||||||
from modules.fulfillment_base.repositories import ProductRepository
|
from modules.fulfillment_base.repositories import ProductRepository
|
||||||
from modules.fulfillment_base.schemas.product import (
|
from modules.fulfillment_base.schemas.product import *
|
||||||
CreateProductRequest,
|
|
||||||
ProductSchema,
|
|
||||||
UpdateProductRequest, GetProductsResponse,
|
|
||||||
)
|
|
||||||
from schemas.base import PaginationSchema, PaginationInfoSchema
|
from schemas.base import PaginationSchema, PaginationInfoSchema
|
||||||
from services.mixins import *
|
from services.mixins import *
|
||||||
|
|
||||||
@ -46,5 +45,54 @@ class ProductService(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def is_soft_delete(self, product: ProductSchema) -> bool:
|
async def upload_image(
|
||||||
return True
|
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",
|
"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",
|
||||||
|
"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]
|
[dependency-groups]
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
|
from .attr_select import AttrSelectRepository as AttrSelectRepository
|
||||||
|
from .attribute import AttributeRepository as AttributeRepository
|
||||||
from .board import BoardRepository as BoardRepository
|
from .board import BoardRepository as BoardRepository
|
||||||
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 .module import ModuleRepository as ModuleRepository
|
||||||
from .project import ProjectRepository as ProjectRepository
|
from .project import ProjectRepository as ProjectRepository
|
||||||
from .status import StatusRepository as StatusRepository
|
from .status import StatusRepository as StatusRepository
|
||||||
from .built_in_module import BuiltInModuleRepository as BuiltInModuleRepository
|
from .attr_option import AttrOptionRepository as AttrOptionRepository
|
||||||
from .deal_group import DealGroupRepository as DealGroupRepository
|
|
||||||
|
|||||||
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,
|
DealProductService,
|
||||||
DealProduct,
|
DealProduct,
|
||||||
)
|
)
|
||||||
|
from repositories import AttributeRepository
|
||||||
from repositories.mixins import *
|
from repositories.mixins import *
|
||||||
from schemas.base import SortDir
|
from schemas.base import SortDir
|
||||||
from schemas.deal import UpdateDealSchema, CreateDealSchema
|
from schemas.deal import UpdateDealSchema, CreateDealSchema
|
||||||
@ -99,6 +100,7 @@ class DealRepository(
|
|||||||
joinedload(Deal.status),
|
joinedload(Deal.status),
|
||||||
joinedload(Deal.board),
|
joinedload(Deal.board),
|
||||||
selectinload(Deal.group),
|
selectinload(Deal.group),
|
||||||
|
selectinload(Deal.tags),
|
||||||
)
|
)
|
||||||
.where(Deal.is_deleted.is_(False))
|
.where(Deal.is_deleted.is_(False))
|
||||||
)
|
)
|
||||||
@ -145,6 +147,15 @@ class DealRepository(
|
|||||||
result = await self.session.execute(stmt)
|
result = await self.session.execute(stmt)
|
||||||
return list(result.scalars().all())
|
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:
|
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
|
||||||
return stmt.options(joinedload(Deal.status), joinedload(Deal.board))
|
return stmt.options(joinedload(Deal.status), joinedload(Deal.board))
|
||||||
|
|
||||||
|
|||||||
@ -14,8 +14,10 @@ class DealGroupRepository(
|
|||||||
async def create(self, deals: list[Deal], lexorank: str) -> DealGroup:
|
async def create(self, deals: list[Deal], lexorank: str) -> DealGroup:
|
||||||
group = DealGroup(deals=deals, lexorank=lexorank)
|
group = DealGroup(deals=deals, lexorank=lexorank)
|
||||||
self.session.add(group)
|
self.session.add(group)
|
||||||
await self.session.commit()
|
await self.session.flush()
|
||||||
await self.session.refresh(group)
|
await self.session.refresh(group)
|
||||||
|
group.name = "Группа ID: " + str(group.id)
|
||||||
|
await self.session.commit()
|
||||||
return group
|
return group
|
||||||
|
|
||||||
async def update(self, entity: DealGroup, data: UpdateDealGroupSchema) -> DealGroup:
|
async def update(self, entity: DealGroup, data: UpdateDealGroupSchema) -> DealGroup:
|
||||||
|
|||||||
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
|
||||||
@ -65,13 +65,14 @@ class RepUpdateMixin(Generic[EntityType, UpdateSchemaType], RepBaseMixin[EntityT
|
|||||||
data: UpdateSchemaType,
|
data: UpdateSchemaType,
|
||||||
with_commit: Optional[bool] = False,
|
with_commit: Optional[bool] = False,
|
||||||
fields: Optional[list[str]] = None,
|
fields: Optional[list[str]] = None,
|
||||||
|
set_if_value_is_not_none: Optional[bool] = True,
|
||||||
) -> EntityType:
|
) -> EntityType:
|
||||||
if fields is None:
|
if fields is None:
|
||||||
fields = data.model_dump().keys()
|
fields = data.model_dump().keys()
|
||||||
|
|
||||||
for field in fields:
|
for field in fields:
|
||||||
value = getattr(data, field)
|
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)
|
setattr(model, field, value)
|
||||||
|
|
||||||
if with_commit:
|
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()
|
||||||
@ -1,7 +1,8 @@
|
|||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
|
from models import DealTag
|
||||||
from models.project import Project
|
from models.project import Project
|
||||||
from repositories.built_in_module import BuiltInModuleRepository
|
from repositories.module import ModuleRepository
|
||||||
from repositories.mixins import *
|
from repositories.mixins import *
|
||||||
from schemas.project import CreateProjectSchema, UpdateProjectSchema
|
from schemas.project import CreateProjectSchema, UpdateProjectSchema
|
||||||
|
|
||||||
@ -12,17 +13,23 @@ class ProjectRepository(
|
|||||||
entity_class = Project
|
entity_class = Project
|
||||||
entity_not_found_msg = "Проект не найден"
|
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:
|
def _process_get_all_stmt(self, stmt: Select) -> Select:
|
||||||
return stmt.order_by(Project.id)
|
return self._apply_options(stmt).order_by(Project.id)
|
||||||
|
|
||||||
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
|
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
|
||||||
return stmt.options(selectinload(Project.boards))
|
return self._apply_options(stmt)
|
||||||
|
|
||||||
async def update(self, project: Project, data: UpdateProjectSchema) -> Project:
|
async def update(self, project: Project, data: UpdateProjectSchema) -> Project:
|
||||||
if data.built_in_modules is not None:
|
if data.modules is not None:
|
||||||
built_in_modules = data.built_in_modules
|
modules = data.modules
|
||||||
module_ids = [module.id for module in built_in_modules]
|
module_ids = [module.id for module in modules]
|
||||||
data.built_in_modules = await BuiltInModuleRepository(
|
data.modules = await ModuleRepository(
|
||||||
self.session
|
self.session
|
||||||
).get_by_ids(module_ids)
|
).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
|
||||||
|
)
|
||||||
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()
|
||||||
@ -1,18 +1,104 @@
|
|||||||
from fastapi import APIRouter
|
from fastapi import APIRouter, Path
|
||||||
|
|
||||||
from backend.dependecies import SessionDependency
|
from backend.dependecies import SessionDependency
|
||||||
from schemas.module import GetAllBuiltInModulesResponse
|
from schemas.module import *
|
||||||
from services.built_in_module import BuiltInModuleService
|
from services.module import ModuleService
|
||||||
|
|
||||||
router = APIRouter(tags=["modules"])
|
router = APIRouter(tags=["modules"])
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
@router.get(
|
||||||
"/built-in/",
|
"/",
|
||||||
response_model=GetAllBuiltInModulesResponse,
|
response_model=GetAllModulesResponse,
|
||||||
operation_id="get_built_in_modules",
|
operation_id="get_modules",
|
||||||
)
|
)
|
||||||
async def get_built_in_modules(
|
async def get_modules(
|
||||||
session: SessionDependency,
|
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 backend.dependecies import SessionDependency, PaginationDependency
|
||||||
from modules.fulfillment_base.schemas.product import *
|
from modules.fulfillment_base.schemas.product import *
|
||||||
@ -46,6 +48,19 @@ async def update_product(
|
|||||||
return await ProductService(session).update(pk, request)
|
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(
|
@router.delete(
|
||||||
"/{pk}",
|
"/{pk}",
|
||||||
response_model=DeleteProductResponse,
|
response_model=DeleteProductResponse,
|
||||||
@ -64,10 +79,52 @@ async def delete_product(
|
|||||||
response_model=GetProductBarcodePdfResponse,
|
response_model=GetProductBarcodePdfResponse,
|
||||||
)
|
)
|
||||||
async def get_product_barcode_pdf(
|
async def get_product_barcode_pdf(
|
||||||
request: GetProductBarcodePdfRequest, session: SessionDependency
|
session: SessionDependency,
|
||||||
|
request: GetProductBarcodePdfRequest,
|
||||||
):
|
):
|
||||||
service = BarcodePrinterService(session)
|
service = BarcodePrinterService(session)
|
||||||
filename, base64_string = await service.generate_base64(request)
|
filename, base64_string = await service.generate_product_base64(request)
|
||||||
return GetProductBarcodePdfResponse(
|
return GetProductBarcodePdfResponse(
|
||||||
base64_string=base64_string, filename=filename, mime_type="application/pdf"
|
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
|
||||||
@ -5,6 +5,7 @@ from modules.clients.schemas.client import ClientSchema
|
|||||||
from schemas.base import BaseSchema, BaseResponse, PaginationInfoSchema
|
from schemas.base import BaseSchema, BaseResponse, PaginationInfoSchema
|
||||||
from schemas.board import BoardSchema
|
from schemas.board import BoardSchema
|
||||||
from schemas.deal_group import DealGroupSchema
|
from schemas.deal_group import DealGroupSchema
|
||||||
|
from schemas.deal_tag import DealTagSchema
|
||||||
from schemas.status import StatusSchema
|
from schemas.status import StatusSchema
|
||||||
|
|
||||||
|
|
||||||
@ -19,6 +20,7 @@ class DealSchema(BaseSchema):
|
|||||||
board: BoardSchema
|
board: BoardSchema
|
||||||
created_at: datetime
|
created_at: datetime
|
||||||
group: Optional[DealGroupSchema]
|
group: Optional[DealGroupSchema]
|
||||||
|
tags: list[DealTagSchema]
|
||||||
# FF module
|
# FF module
|
||||||
products_quantity: int = 0
|
products_quantity: int = 0
|
||||||
total_price: float = 0
|
total_price: float = 0
|
||||||
@ -28,6 +30,7 @@ class DealSchema(BaseSchema):
|
|||||||
|
|
||||||
class CreateDealSchema(BaseSchema):
|
class CreateDealSchema(BaseSchema):
|
||||||
name: str
|
name: str
|
||||||
|
project_id: int
|
||||||
board_id: int
|
board_id: int
|
||||||
lexorank: str
|
lexorank: str
|
||||||
status_id: int
|
status_id: int
|
||||||
|
|||||||
79
schemas/deal_tag.py
Normal file
79
schemas/deal_tag.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from schemas.base import BaseSchema, BaseResponse
|
||||||
|
|
||||||
|
|
||||||
|
# region Entities
|
||||||
|
|
||||||
|
|
||||||
|
class DealTagColorSchema(BaseSchema):
|
||||||
|
id: int
|
||||||
|
color: str
|
||||||
|
background_color: str
|
||||||
|
label: str
|
||||||
|
|
||||||
|
|
||||||
|
class CreateDealTagSchema(BaseSchema):
|
||||||
|
name: str
|
||||||
|
project_id: int
|
||||||
|
tag_color_id: int
|
||||||
|
|
||||||
|
|
||||||
|
class DealTagSchema(CreateDealTagSchema):
|
||||||
|
id: int
|
||||||
|
tag_color: DealTagColorSchema
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateDealTagSchema(BaseSchema):
|
||||||
|
name: Optional[str] = None
|
||||||
|
tag_color: Optional[DealTagColorSchema] = None
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
# region Requests
|
||||||
|
|
||||||
|
|
||||||
|
class CreateDealTagRequest(BaseSchema):
|
||||||
|
entity: CreateDealTagSchema
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateDealTagRequest(BaseSchema):
|
||||||
|
entity: UpdateDealTagSchema
|
||||||
|
|
||||||
|
|
||||||
|
class SwitchDealTagRequest(BaseSchema):
|
||||||
|
tag_id: int
|
||||||
|
deal_id: Optional[int] = None
|
||||||
|
group_id: Optional[int] = None
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
|
|
||||||
|
# region Responses
|
||||||
|
|
||||||
|
class GetDealTagsResponse(BaseSchema):
|
||||||
|
items: list[DealTagSchema]
|
||||||
|
|
||||||
|
|
||||||
|
class CreateDealTagResponse(BaseResponse):
|
||||||
|
entity: DealTagSchema
|
||||||
|
|
||||||
|
|
||||||
|
class UpdateDealTagResponse(BaseResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class DeleteDealTagResponse(BaseResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class SwitchDealTagResponse(BaseResponse):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class GetTagColorsResponse(BaseSchema):
|
||||||
|
items: list[DealTagColorSchema]
|
||||||
|
|
||||||
|
|
||||||
|
# endregion
|
||||||
@ -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
|
# region Entity
|
||||||
|
|
||||||
|
|
||||||
class BuiltInModuleTabSchema(BaseSchema):
|
class ModuleTabSchema(BaseSchema):
|
||||||
id: int
|
id: int
|
||||||
key: str
|
key: str
|
||||||
label: str
|
label: str
|
||||||
icon_name: str
|
icon_name: Optional[str]
|
||||||
device: str
|
device: str
|
||||||
|
|
||||||
|
|
||||||
class BuiltInModuleSchema(BaseSchema):
|
class ModuleSchema(BaseSchema):
|
||||||
id: int
|
id: int
|
||||||
key: str
|
key: str
|
||||||
label: str
|
label: str
|
||||||
description: str
|
description: Optional[str]
|
||||||
depends_on: list["BuiltInModuleSchema"]
|
is_built_in: bool
|
||||||
tabs: list[BuiltInModuleTabSchema]
|
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
|
# endregion
|
||||||
@ -25,8 +58,36 @@ class BuiltInModuleSchema(BaseSchema):
|
|||||||
# region Response
|
# region Response
|
||||||
|
|
||||||
|
|
||||||
class GetAllBuiltInModulesResponse(BaseSchema):
|
class GetAllModulesResponse(BaseSchema):
|
||||||
items: list[BuiltInModuleSchema]
|
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
|
# endregion
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from schemas.base import BaseSchema, BaseResponse
|
from schemas.base import BaseSchema, BaseResponse
|
||||||
from schemas.module import BuiltInModuleSchema
|
from schemas.deal_tag import DealTagSchema
|
||||||
|
from schemas.module import ModuleSchema
|
||||||
|
|
||||||
|
|
||||||
# region Entity
|
# region Entity
|
||||||
@ -10,7 +11,8 @@ from schemas.module import BuiltInModuleSchema
|
|||||||
class ProjectSchema(BaseSchema):
|
class ProjectSchema(BaseSchema):
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
built_in_modules: list[BuiltInModuleSchema]
|
modules: list[ModuleSchema]
|
||||||
|
tags: list[DealTagSchema]
|
||||||
|
|
||||||
|
|
||||||
class CreateProjectSchema(BaseSchema):
|
class CreateProjectSchema(BaseSchema):
|
||||||
@ -19,7 +21,7 @@ class CreateProjectSchema(BaseSchema):
|
|||||||
|
|
||||||
class UpdateProjectSchema(BaseSchema):
|
class UpdateProjectSchema(BaseSchema):
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
built_in_modules: list[BuiltInModuleSchema] = None
|
modules: list[ModuleSchema] = None
|
||||||
|
|
||||||
|
|
||||||
# endregion
|
# endregion
|
||||||
|
|||||||
@ -1,5 +1,9 @@
|
|||||||
|
from .attr_select import AttrSelectService as AttrSelectService
|
||||||
|
from .attribute import AttributeService as AttributeService
|
||||||
from .board import BoardService as BoardService
|
from .board import BoardService as BoardService
|
||||||
from .deal import DealService as DealService
|
from .deal import DealService as DealService
|
||||||
|
from .deal_group import DealGroupService as DealGroupService
|
||||||
|
from .deal_tag import DealTagService as DealTagService
|
||||||
from .project import ProjectService as ProjectService
|
from .project import ProjectService as ProjectService
|
||||||
from .status import StatusService as StatusService
|
from .status import StatusService as StatusService
|
||||||
from .deal_group import DealGroupService as DealGroupService
|
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,7 +1,6 @@
|
|||||||
from lexorank import lexorank
|
import lexorank
|
||||||
|
|
||||||
from models import DealGroup, Deal
|
from models import DealGroup, Deal
|
||||||
from repositories import DealGroupRepository, DealRepository
|
from repositories import DealGroupRepository, DealRepository, DealTagRepository
|
||||||
from schemas.deal_group import *
|
from schemas.deal_group import *
|
||||||
from services.mixins import *
|
from services.mixins import *
|
||||||
|
|
||||||
@ -21,6 +20,10 @@ class DealGroupService(
|
|||||||
if deal.status_id != main_deal.status_id:
|
if deal.status_id != main_deal.status_id:
|
||||||
await deal_repo.update_status(deal, main_deal.status_id)
|
await deal_repo.update_status(deal, main_deal.status_id)
|
||||||
|
|
||||||
|
await DealTagRepository(self.repository.session).sync_deals_tags(
|
||||||
|
[main_deal, *other_deals]
|
||||||
|
)
|
||||||
|
|
||||||
async def create(self, request: CreateDealGroupRequest) -> CreateDealGroupResponse:
|
async def create(self, request: CreateDealGroupRequest) -> CreateDealGroupResponse:
|
||||||
deal_repo = DealRepository(self.repository.session)
|
deal_repo = DealRepository(self.repository.session)
|
||||||
main_deal: Deal = await deal_repo.get_by_id(request.main_deal_id)
|
main_deal: Deal = await deal_repo.get_by_id(request.main_deal_id)
|
||||||
|
|||||||
37
services/deal_tag.py
Normal file
37
services/deal_tag.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from models import DealTag
|
||||||
|
from repositories import DealTagRepository, DealRepository
|
||||||
|
from schemas.deal_tag import *
|
||||||
|
from services.mixins import *
|
||||||
|
|
||||||
|
|
||||||
|
class DealTagService(
|
||||||
|
ServiceCrudMixin[DealTag, DealTagSchema, CreateDealTagRequest, UpdateDealTagRequest]
|
||||||
|
):
|
||||||
|
schema_class = DealTagSchema
|
||||||
|
entity_deleted_msg = "Тег успешно удален"
|
||||||
|
entity_updated_msg = "Тег успешно обновлен"
|
||||||
|
entity_created_msg = "Тег успешно создан"
|
||||||
|
|
||||||
|
def __init__(self, session: AsyncSession):
|
||||||
|
self.repository = DealTagRepository(session)
|
||||||
|
|
||||||
|
async def switch_tag(self, request: SwitchDealTagRequest) -> SwitchDealTagResponse:
|
||||||
|
tag: DealTag = await self.repository.get_by_id(request.tag_id)
|
||||||
|
if request.deal_id:
|
||||||
|
deal = await DealRepository(self.repository.session).get_by_id(
|
||||||
|
request.deal_id
|
||||||
|
)
|
||||||
|
await self.repository.switch_tag_in_deal(tag, deal)
|
||||||
|
else:
|
||||||
|
deals = await DealRepository(self.repository.session).get_by_group_id(
|
||||||
|
request.group_id
|
||||||
|
)
|
||||||
|
await self.repository.switch_tag_in_deals(tag, deals)
|
||||||
|
|
||||||
|
return SwitchDealTagResponse(ok=True, message="Успешно")
|
||||||
|
|
||||||
|
async def get_tag_colors(self) -> GetTagColorsResponse:
|
||||||
|
colors = await self.repository.get_tag_colors()
|
||||||
|
return GetTagColorsResponse(
|
||||||
|
items=[DealTagColorSchema.model_validate(color) for color in colors]
|
||||||
|
)
|
||||||
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):
|
def __init__(self, name: str):
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectNotFoundException(BaseResponseException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ForbiddenException(BaseResponseException):
|
||||||
|
pass
|
||||||
|
|||||||
Reference in New Issue
Block a user