From 66b50fb951eddbbcf485b1722fe84254185d7e68 Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Sat, 4 Oct 2025 10:13:24 +0400 Subject: [PATCH] feat: barcode templates --- modules/fulfillment_base/models/__init__.py | 16 ++- modules/fulfillment_base/models/barcode.py | 36 ++++++ .../models/barcode_template.py | 42 +++++++ modules/fulfillment_base/models/product.py | 23 ++++ .../fulfillment_base/repositories/__init__.py | 1 + .../repositories/barcode_template.py | 113 ++++++++++++++++++ .../routers/barcode_template.py | 77 ++++++++++++ .../schemas/barcode_template.py | 82 +++++++++++++ modules/fulfillment_base/services/__init__.py | 1 + .../services/barcode_template.py | 39 ++++++ repositories/mixins.py | 5 + 11 files changed, 432 insertions(+), 3 deletions(-) create mode 100644 modules/fulfillment_base/models/barcode.py create mode 100644 modules/fulfillment_base/models/barcode_template.py create mode 100644 modules/fulfillment_base/repositories/barcode_template.py create mode 100644 modules/fulfillment_base/routers/barcode_template.py create mode 100644 modules/fulfillment_base/schemas/barcode_template.py create mode 100644 modules/fulfillment_base/services/barcode_template.py diff --git a/modules/fulfillment_base/models/__init__.py b/modules/fulfillment_base/models/__init__.py index bb29616..cfa5105 100644 --- a/modules/fulfillment_base/models/__init__.py +++ b/modules/fulfillment_base/models/__init__.py @@ -1,6 +1,16 @@ -from .deal_product import DealProduct as DealProduct, DealProductService as DealProductService -from .deal_service import ( - DealService as DealService, +from .barcode import ( + ProductBarcode as ProductBarcode, + ProductBarcodeImage as ProductBarcodeImage, ) +from .barcode_template import ( + BarcodeTemplateAttribute as BarcodeTemplateAttribute, + BarcodeTemplateSize as BarcodeTemplateSize, + BarcodeTemplate as BarcodeTemplate, +) +from .deal_product import ( + DealProduct as DealProduct, + DealProductService as DealProductService, +) +from .deal_service import DealService as DealService from .product import Product as Product from .service import Service as Service, ServiceCategory as ServiceCategory diff --git a/modules/fulfillment_base/models/barcode.py b/modules/fulfillment_base/models/barcode.py new file mode 100644 index 0000000..ba1a8c1 --- /dev/null +++ b/modules/fulfillment_base/models/barcode.py @@ -0,0 +1,36 @@ +from typing import TYPE_CHECKING + +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from models.base import BaseModel + +if TYPE_CHECKING: + from modules.fulfillment_base.models import Product + + +class ProductBarcode(BaseModel): + __tablename__ = "fulfillment_base_product_barcodes" + + product_id: Mapped[int] = mapped_column( + ForeignKey("fulfillment_base_products.id"), + primary_key=True, + ) + product: Mapped["Product"] = relationship(back_populates="barcodes") + + barcode: Mapped[str] = mapped_column( + primary_key=True, index=True, comment="ШК товара" + ) + + +class ProductBarcodeImage(BaseModel): + __tablename__ = "fulfillment_base_product_barcode_images" + + product_id: Mapped[int] = mapped_column( + ForeignKey("fulfillment_base_products.id"), + primary_key=True, + comment="ID товара", + ) + product: Mapped["Product"] = relationship(back_populates="barcode_image") + + filename: Mapped[str] = mapped_column() diff --git a/modules/fulfillment_base/models/barcode_template.py b/modules/fulfillment_base/models/barcode_template.py new file mode 100644 index 0000000..82a163b --- /dev/null +++ b/modules/fulfillment_base/models/barcode_template.py @@ -0,0 +1,42 @@ +from sqlalchemy import ForeignKey, Table, Column +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from models.base import BaseModel +from models.mixins import IdMixin, SoftDeleteMixin + +barcode_template_attribute_link = Table( + "barcode_template_attribute_links", + BaseModel.metadata, + Column("barcode_template_id", ForeignKey("barcode_templates.id")), + Column("attribute_id", ForeignKey("barcode_template_attributes.id")), +) + + +class BarcodeTemplateAttribute(BaseModel, IdMixin): + __tablename__ = "barcode_template_attributes" + + key: Mapped[str] = mapped_column(index=True, comment="Ключ атрибута") + name: Mapped[str] = mapped_column(index=True, comment="Метка атрибута") + + +class BarcodeTemplateSize(BaseModel, IdMixin): + __tablename__ = "barcode_template_sizes" + + name: Mapped[str] = mapped_column(index=True, comment="Название размера") + width: Mapped[int] = mapped_column(comment="Ширина в мм") + height: Mapped[int] = mapped_column(comment="Высота в мм") + + +class BarcodeTemplate(BaseModel, IdMixin, SoftDeleteMixin): + __tablename__ = "barcode_templates" + + name: Mapped[str] = mapped_column(index=True, comment="Название шаблона") + attributes: Mapped[list["BarcodeTemplateAttribute"]] = relationship( + secondary=barcode_template_attribute_link, + lazy="selectin", + ) + + is_default: Mapped[bool] = mapped_column(default=False, comment="По умолчанию") + + size_id: Mapped[int] = mapped_column(ForeignKey("barcode_template_sizes.id")) + size: Mapped["BarcodeTemplateSize"] = relationship(lazy="joined") diff --git a/modules/fulfillment_base/models/product.py b/modules/fulfillment_base/models/product.py index 15536ce..9ac2e95 100644 --- a/modules/fulfillment_base/models/product.py +++ b/modules/fulfillment_base/models/product.py @@ -1,8 +1,15 @@ +from typing import Optional + from sqlalchemy import ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship from models.base import BaseModel from models.mixins import IdMixin, SoftDeleteMixin +from modules.fulfillment_base.models import ( + ProductBarcode, + BarcodeTemplate, + ProductBarcodeImage, +) class Product(BaseModel, IdMixin, SoftDeleteMixin): @@ -28,6 +35,22 @@ class Product(BaseModel, IdMixin, SoftDeleteMixin): cascade="all, delete-orphan", ) + barcodes: Mapped[list["ProductBarcode"]] = relationship( + back_populates="product", + cascade="all, delete-orphan", + ) + + barcode_template_id: Mapped[Optional[int]] = mapped_column( + ForeignKey("barcode_templates.id") + ) + barcode_template: Mapped["BarcodeTemplate"] = relationship(lazy="joined") + + barcode_image: Mapped["ProductBarcodeImage"] = relationship( + back_populates="product", + lazy="joined", + uselist=False, + ) + class ProductImage(BaseModel, IdMixin): __tablename__ = "fulfillment_base_product_images" diff --git a/modules/fulfillment_base/repositories/__init__.py b/modules/fulfillment_base/repositories/__init__.py index ad1eb04..3385055 100644 --- a/modules/fulfillment_base/repositories/__init__.py +++ b/modules/fulfillment_base/repositories/__init__.py @@ -5,3 +5,4 @@ from .product import ProductRepository as ProductRepository from .service import ServiceRepository as ServiceRepository from .services_kit import ServicesKitRepository as ServicesKitRepository from .service_category import ServiceCategoryRepository as ServiceCategoryRepository +from .barcode_template import BarcodeTemplateRepository as BarcodeTemplateRepository diff --git a/modules/fulfillment_base/repositories/barcode_template.py b/modules/fulfillment_base/repositories/barcode_template.py new file mode 100644 index 0000000..47a888f --- /dev/null +++ b/modules/fulfillment_base/repositories/barcode_template.py @@ -0,0 +1,113 @@ +from sqlalchemy import update +from sqlalchemy.orm import joinedload, selectinload + +from modules.fulfillment_base.models import ( + BarcodeTemplate, + BarcodeTemplateAttribute, + BarcodeTemplateSize, +) +from modules.fulfillment_base.schemas.barcode_template import ( + CreateBarcodeTemplateSchema, + UpdateBarcodeTemplateSchema, +) +from repositories.mixins import * + + +class BarcodeTemplateRepository( + RepCrudMixin[ + BarcodeTemplate, CreateBarcodeTemplateSchema, UpdateBarcodeTemplateSchema + ], +): + session: AsyncSession + entity_class = BarcodeTemplate + entity_not_found_msg = "Шаблон штрихкода не найден" + + def _process_get_all_stmt(self, stmt: Select) -> Select: + return ( + stmt.options( + selectinload(BarcodeTemplate.attributes), + joinedload(BarcodeTemplate.size), + ) + .where(BarcodeTemplate.is_deleted.is_(False)) + .order_by(BarcodeTemplate.id) + ) + + async def _get_size_by_id(self, size_id: int) -> Optional[BarcodeTemplateSize]: + stmt = select(BarcodeTemplateSize).where(BarcodeTemplateSize.id == size_id) + result = await self.session.scalars(stmt) + return result.one_or_none() + + async def _get_attrs_by_ids( + self, attrs_ids: list[int] + ) -> list[BarcodeTemplateAttribute]: + stmt = select(BarcodeTemplateAttribute).where( + BarcodeTemplateAttribute.id.in_(attrs_ids) + ) + result = await self.session.execute(stmt) + return list(result.scalars().all()) + + async def create(self, data: CreateBarcodeTemplateSchema) -> int: + if data.is_default is not None and data.is_default: + await self._turn_off_defaults() + + data_dict = data.model_dump() + data_dict["size"] = await self._get_size_by_id(data.size.id) + data_dict["attributes"] = await self._get_attrs_by_ids( + [a.id for a in data.attributes] + ) + + obj = BarcodeTemplate(**data_dict) + self.session.add(obj) + await self.session.commit() + await self.session.refresh(obj) + return obj.id + + async def _turn_off_defaults(self): + stmt = ( + update(BarcodeTemplate) + .where(BarcodeTemplate.is_default.is_(True)) + .values({"is_default": False}) + ) + await self.session.execute(stmt) + + async def _set_first_as_default(self, with_commit: bool = False): + stmt = select(BarcodeTemplate).limit(1) + result = await self.session.execute(stmt) + obj = result.scalar() + if not obj: + return + obj.is_default = True + self.session.add(obj) + if with_commit: + await self.session.commit() + await self.session.refresh(obj) + + async def update( + self, template: BarcodeTemplate, data: UpdateBarcodeTemplateSchema + ) -> BarcodeTemplate: + if data.size is not None: + data.size = await self._get_size_by_id(data.size.id) + if data.attributes is not None: + data.attributes = await self._get_attrs_by_ids( + [a.id for a in data.attributes] + ) + if data.is_default is not None: + if data.is_default: + await self._turn_off_defaults() + else: + await self._set_first_as_default() + return await self._apply_update_data_to_model(template, data, True) + + async def _before_delete(self, template: BarcodeTemplate): + if template.is_default: + await self._set_first_as_default() + + async def get_attributes(self) -> list[BarcodeTemplateAttribute]: + stmt = select(BarcodeTemplateAttribute) + result = await self.session.execute(stmt) + return list(result.scalars().all()) + + async def get_sizes(self) -> list[BarcodeTemplateSize]: + stmt = select(BarcodeTemplateSize) + result = await self.session.execute(stmt) + return list(result.scalars().all()) diff --git a/modules/fulfillment_base/routers/barcode_template.py b/modules/fulfillment_base/routers/barcode_template.py new file mode 100644 index 0000000..765a673 --- /dev/null +++ b/modules/fulfillment_base/routers/barcode_template.py @@ -0,0 +1,77 @@ +from fastapi import APIRouter, Path + +from backend.dependecies import SessionDependency +from modules.fulfillment_base.schemas.barcode_template import * +from modules.fulfillment_base.services import BarcodeTemplateService + +router = APIRouter(tags=["barcode_template"]) + + +@router.get( + "/", + response_model=GetBarcodeTemplatesResponse, + operation_id="get_barcode_templates", +) +async def get_barcode_templates( + session: SessionDependency, +): + return await BarcodeTemplateService(session).get_all() + + +@router.post( + "/", + response_model=CreateBarcodeTemplateResponse, + operation_id="create_barcode_template", +) +async def create_barcode_template( + session: SessionDependency, + request: CreateBarcodeTemplateRequest, +): + return await BarcodeTemplateService(session).create(request) + + +@router.patch( + "/{pk}", + response_model=UpdateBarcodeTemplateResponse, + operation_id="update_barcode_template", +) +async def update_barcode_template( + session: SessionDependency, + request: UpdateBarcodeTemplateRequest, + pk: int = Path(), +): + return await BarcodeTemplateService(session).update(pk, request) + + +@router.delete( + "/{pk}", + response_model=DeleteBarcodeTemplateResponse, + operation_id="delete_barcode_template", +) +async def delete_barcode_template( + session: SessionDependency, + pk: int = Path(), +): + return await BarcodeTemplateService(session).delete(pk) + + +@router.get( + "/attributes", + response_model=GetBarcodeAttributesResponse, + operation_id="get_barcode_template_attributes", +) +async def get_barcode_template_attributes( + session: SessionDependency, +): + return await BarcodeTemplateService(session).get_attributes() + + +@router.get( + "/sizes", + response_model=GetBarcodeTemplateSizesResponse, + operation_id="get_barcode_template_sizes", +) +async def get_barcode_template_sizes( + session: SessionDependency, +): + return await BarcodeTemplateService(session).get_sizes() diff --git a/modules/fulfillment_base/schemas/barcode_template.py b/modules/fulfillment_base/schemas/barcode_template.py new file mode 100644 index 0000000..abd03ca --- /dev/null +++ b/modules/fulfillment_base/schemas/barcode_template.py @@ -0,0 +1,82 @@ +from typing import Optional + +from schemas.base import BaseSchema, BaseResponse + + +# region Entity + + +class BarcodeTemplateAttributeSchema(BaseSchema): + id: int + key: str + name: str + + +class BarcodeTemplateSizeSchema(BaseSchema): + id: int + name: str + width: int + height: int + + +class CreateBarcodeTemplateSchema(BaseSchema): + name: str + attributes: list[BarcodeTemplateAttributeSchema] + is_default: bool + size: BarcodeTemplateSizeSchema + + +class BarcodeTemplateSchema(CreateBarcodeTemplateSchema): + id: int + + +class UpdateBarcodeTemplateSchema(BaseSchema): + name: Optional[str] = None + attributes: Optional[list[BarcodeTemplateAttributeSchema]] = None + is_default: Optional[bool] = None + size: Optional[BarcodeTemplateSizeSchema] = None + + +# endregion + +# region Request + + +class CreateBarcodeTemplateRequest(BaseSchema): + entity: CreateBarcodeTemplateSchema + + +class UpdateBarcodeTemplateRequest(BaseSchema): + entity: UpdateBarcodeTemplateSchema + + +# endregion + +# region Response + + +class GetBarcodeTemplatesResponse(BaseSchema): + items: list[BarcodeTemplateSchema] + + +class CreateBarcodeTemplateResponse(BaseResponse): + entity: BarcodeTemplateSchema + + +class UpdateBarcodeTemplateResponse(BaseResponse): + pass + + +class DeleteBarcodeTemplateResponse(BaseResponse): + pass + + +class GetBarcodeAttributesResponse(BaseSchema): + items: list[BarcodeTemplateAttributeSchema] + + +class GetBarcodeTemplateSizesResponse(BaseSchema): + items: list[BarcodeTemplateSizeSchema] + + +# endregion diff --git a/modules/fulfillment_base/services/__init__.py b/modules/fulfillment_base/services/__init__.py index dc0664c..1df9aa0 100644 --- a/modules/fulfillment_base/services/__init__.py +++ b/modules/fulfillment_base/services/__init__.py @@ -5,3 +5,4 @@ from .product_service import ProductServiceService as ProductServiceService from .service import ServiceModelService as ServiceModelService from .services_kit import ServicesKitService as ServicesKitService from .service_category import ServiceCategoryService as ServiceCategoryService +from .barcode_template import BarcodeTemplateService as BarcodeTemplateService diff --git a/modules/fulfillment_base/services/barcode_template.py b/modules/fulfillment_base/services/barcode_template.py new file mode 100644 index 0000000..4958e21 --- /dev/null +++ b/modules/fulfillment_base/services/barcode_template.py @@ -0,0 +1,39 @@ +from modules.fulfillment_base.models import BarcodeTemplate +from modules.fulfillment_base.repositories import BarcodeTemplateRepository +from modules.fulfillment_base.schemas.barcode_template import * +from services.mixins import * + + +class BarcodeTemplateService( + ServiceCrudMixin[ + BarcodeTemplate, + BarcodeTemplateSchema, + CreateBarcodeTemplateRequest, + UpdateBarcodeTemplateRequest, + ] +): + schema_class = BarcodeTemplateSchema + entity_deleted_msg = "Шаблон штрихкода успешно удален" + entity_updated_msg = "Шаблон штрихкода успешно обновлен" + entity_created_msg = "Шаблон штрихкода успешно создан" + + def __init__(self, session: AsyncSession): + self.repository = BarcodeTemplateRepository(session) + + async def is_soft_delete(self, template: BarcodeTemplate) -> bool: + return True + + async def get_attributes(self) -> GetBarcodeAttributesResponse: + attributes = await self.repository.get_attributes() + return GetBarcodeAttributesResponse( + items=[ + BarcodeTemplateAttributeSchema.model_validate(attr) + for attr in attributes + ] + ) + + async def get_sizes(self) -> GetBarcodeTemplateSizesResponse: + sizes = await self.repository.get_sizes() + return GetBarcodeTemplateSizesResponse( + items=[BarcodeTemplateSizeSchema.model_validate(size) for size in sizes] + ) diff --git a/repositories/mixins.py b/repositories/mixins.py index 122002a..f9d156a 100644 --- a/repositories/mixins.py +++ b/repositories/mixins.py @@ -17,7 +17,12 @@ class RepBaseMixin(Generic[EntityType]): class RepDeleteMixin(Generic[EntityType], RepBaseMixin[EntityType]): + async def _before_delete(self, obj: EntityType) -> None: + pass + async def delete(self, obj: EntityType, is_soft: bool) -> None: + await self._before_delete(obj) + if not is_soft: await self.session.delete(obj) await self.session.commit()