feat: barcode templates

This commit is contained in:
2025-10-04 10:13:24 +04:00
parent 9c9b3f4706
commit 66b50fb951
11 changed files with 432 additions and 3 deletions

View File

@ -1,6 +1,16 @@
from .deal_product import DealProduct as DealProduct, DealProductService as DealProductService from .barcode import (
from .deal_service import ( ProductBarcode as ProductBarcode,
DealService as DealService, 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 .product import Product as Product
from .service import Service as Service, ServiceCategory as ServiceCategory from .service import Service as Service, ServiceCategory as ServiceCategory

View File

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

View File

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

View File

@ -1,8 +1,15 @@
from typing import Optional
from sqlalchemy import ForeignKey from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm import Mapped, mapped_column, relationship
from models.base import BaseModel from models.base import BaseModel
from models.mixins import IdMixin, SoftDeleteMixin from models.mixins import IdMixin, SoftDeleteMixin
from modules.fulfillment_base.models import (
ProductBarcode,
BarcodeTemplate,
ProductBarcodeImage,
)
class Product(BaseModel, IdMixin, SoftDeleteMixin): class Product(BaseModel, IdMixin, SoftDeleteMixin):
@ -28,6 +35,22 @@ class Product(BaseModel, IdMixin, SoftDeleteMixin):
cascade="all, delete-orphan", 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): class ProductImage(BaseModel, IdMixin):
__tablename__ = "fulfillment_base_product_images" __tablename__ = "fulfillment_base_product_images"

View File

@ -5,3 +5,4 @@ from .product import ProductRepository as ProductRepository
from .service import ServiceRepository as ServiceRepository from .service import ServiceRepository as ServiceRepository
from .services_kit import ServicesKitRepository as ServicesKitRepository from .services_kit import ServicesKitRepository as ServicesKitRepository
from .service_category import ServiceCategoryRepository as ServiceCategoryRepository from .service_category import ServiceCategoryRepository as ServiceCategoryRepository
from .barcode_template import BarcodeTemplateRepository as BarcodeTemplateRepository

View File

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

View File

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

View File

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

View File

@ -5,3 +5,4 @@ from .product_service import ProductServiceService as ProductServiceService
from .service import ServiceModelService as ServiceModelService from .service import ServiceModelService as ServiceModelService
from .services_kit import ServicesKitService as ServicesKitService from .services_kit import ServicesKitService as ServicesKitService
from .service_category import ServiceCategoryService as ServiceCategoryService from .service_category import ServiceCategoryService as ServiceCategoryService
from .barcode_template import BarcodeTemplateService as BarcodeTemplateService

View File

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

View File

@ -17,7 +17,12 @@ class RepBaseMixin(Generic[EntityType]):
class RepDeleteMixin(Generic[EntityType], RepBaseMixin[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: async def delete(self, obj: EntityType, is_soft: bool) -> None:
await self._before_delete(obj)
if not is_soft: if not is_soft:
await self.session.delete(obj) await self.session.delete(obj)
await self.session.commit() await self.session.commit()