feat: product barcode images

This commit is contained in:
2025-10-21 11:10:08 +04:00
parent 90c0bae8f1
commit 83f3b55f49
6 changed files with 115 additions and 24 deletions

View File

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

View File

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

View File

@ -1,7 +1,7 @@
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.models.product import ProductImage
from modules.fulfillment_base.schemas.product import ( from modules.fulfillment_base.schemas.product import (
CreateProductSchema, CreateProductSchema,
@ -106,6 +106,13 @@ class ProductRepository(
else: else:
await self.session.flush() 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: async def create_image(self, product_id: int, image_url: str) -> ProductImage:
product_image = ProductImage( product_image = ProductImage(
product_id=product_id, product_id=product_id,
@ -114,3 +121,12 @@ class ProductRepository(
self.session.add(product_image) self.session.add(product_image)
await self.session.commit() await self.session.commit()
return product_image 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

View File

@ -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,21 +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] = []
image_url: str | None = None
images: list[ProductImageSchema] | None = []
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") @model_validator(mode="after")
def images_list_to_image_url(cls, values): def images_list_to_image_url(cls, values):
images = values.images if values.images:
if not images: latest_image = values.images[-1]
return values values.image_url = latest_image.image_url
latest_image = images[-1]
values.image_url = latest_image.image_url if values.barcode_image:
values.barcode_image_url = values.barcode_image.image_url
return values return values
class ProductSchema(CreateProductSchema): class ProductSchema(BaseProductSchema):
id: int id: int
barcode_template: BarcodeTemplateSchema barcode_template: BarcodeTemplateSchema
@ -116,4 +127,12 @@ class GetProductBarcodePdfResponse(BasePdfResponse):
pass pass
class BarcodeUploadImageResponse(BaseResponse):
image_url: Optional[str] = None
class DeleteBarcodeImageResponse(BaseResponse):
pass
# endregion # endregion

View File

@ -48,19 +48,51 @@ class ProductService(
async def upload_image( async def upload_image(
self, product_id: int, upload_file: UploadFile self, product_id: int, upload_file: UploadFile
) -> ProductUploadImageResponse: ) -> ProductUploadImageResponse:
try: product: Product = await self.repository.get_by_id(product_id)
product: Product = await self.repository.get_by_id(product_id) s3_uploader = S3Uploader()
s3_uploader = S3Uploader()
if len(product.images) > 0:
for image in product.images: for image in product.images:
s3_key = image.image_url.split("/")[-1] s3_key = image.image_url.split("/")[-1]
await s3_uploader.delete_image(s3_key) await s3_uploader.delete_image(s3_key)
await self.repository.delete_images(product.images) await self.repository.delete_images(product.images)
image_url = await s3_uploader.upload_from_upload_file_obj(upload_file) image_url = await s3_uploader.upload_from_upload_file_obj(upload_file)
await self.repository.create_image(product_id, image_url) await self.repository.create_image(product_id, image_url)
return ProductUploadImageResponse( return ProductUploadImageResponse(
ok=True, message="Изображение успешно загружено", image_url=image_url 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="У товара нет изображения штрихкода"
) )
except Exception as e:
return ProductUploadImageResponse(ok=False, message=str(e)) 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="Изображение штрихкода успешно удалено"
)

View File

@ -49,16 +49,16 @@ async def update_product(
@router.post( @router.post(
"/images/upload/{productId}", "{pk}/images/upload",
response_model=ProductUploadImageResponse, response_model=ProductUploadImageResponse,
operation_id="upload_product_image", operation_id="upload_product_image",
) )
async def upload_product_image( async def upload_product_image(
session: SessionDependency, session: SessionDependency,
upload_file: Annotated[UploadFile, File()], upload_file: Annotated[UploadFile, File()],
product_id: int = Path(alias="productId"), pk: int = Path(),
): ):
return await ProductService(session).upload_image(product_id, upload_file) return await ProductService(session).upload_image(pk, upload_file)
@router.delete( @router.delete(
@ -86,3 +86,28 @@ async def get_product_barcode_pdf(
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(
"{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)