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 .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

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.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"

View File

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

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 .services_kit import ServicesKitService as ServicesKitService
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]
)