From 863dd226c3abc848cdc2edbde3b3fac09f1df1d6 Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Mon, 9 Dec 2024 16:45:10 +0400 Subject: [PATCH] feat: pallets and boxes for deals --- constants.py | 1 + .../base_pdf_card_generator.py | 65 ++++++++ .../shipping_qr_code_generator/__init__.py | 1 + .../shipping_qr_code_generator/generator.py | 139 ++++++++++++++++++ .../generator.py | 48 +----- main.py | 1 + models/deal.py | 4 + models/shipping.py | 58 ++++++++ routers/__init__.py | 3 +- routers/shipping.py | 128 ++++++++++++++++ schemas/deal.py | 3 + schemas/shipping.py | 94 ++++++++++++ services/deal.py | 9 +- services/shipping.py | 121 +++++++++++++++ 14 files changed, 631 insertions(+), 44 deletions(-) create mode 100644 generators/base_pdf_card_generator/base_pdf_card_generator.py create mode 100644 generators/shipping_qr_code_generator/__init__.py create mode 100644 generators/shipping_qr_code_generator/generator.py create mode 100644 models/shipping.py create mode 100644 routers/shipping.py create mode 100644 schemas/shipping.py create mode 100644 services/shipping.py diff --git a/constants.py b/constants.py index b3b7879..cd58dce 100644 --- a/constants.py +++ b/constants.py @@ -12,6 +12,7 @@ ENV.globals['now'] = datetime.now ENV.globals['encode128'] = encode128 ENV.globals['format_number'] = lambda x: '{:,}'.format(x).replace(',', ' ') +DOMAIN_NAME = "crm.denco.store" API_ROOT = "/api" APP_PATH = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(__file__) diff --git a/generators/base_pdf_card_generator/base_pdf_card_generator.py b/generators/base_pdf_card_generator/base_pdf_card_generator.py new file mode 100644 index 0000000..a34dcf3 --- /dev/null +++ b/generators/base_pdf_card_generator/base_pdf_card_generator.py @@ -0,0 +1,65 @@ +import os + +from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet +from reportlab.lib.units import mm +from reportlab.pdfbase import pdfmetrics +from reportlab.pdfbase.ttfonts import TTFont +from reportlab.platypus import SimpleDocTemplate +from sqlalchemy.ext.asyncio import AsyncSession + +from constants import APP_PATH + + +class BasePdfCardGenerator: + def __init__(self, session: AsyncSession): + self._session = session + assets_folder = os.path.join(APP_PATH, 'assets') + fonts_folder = os.path.join(assets_folder, 'fonts') + font_file_path = os.path.join(fonts_folder, 'DejaVuSans.ttf') + self.page_width = 58 * mm + self.page_height = 40 * mm + pdfmetrics.registerFont(TTFont('DejaVuSans', font_file_path)) + + self.styles = getSampleStyleSheet() + self._set_small_paragraph_styles() + self._set_medium_paragraph_styles() + + def _set_small_paragraph_styles(self): + common_paragraph_style = { + "parent": self.styles['Normal'], + "fontName": "DejaVuSans", + "spaceAfter": 4, + "fontSize": 9, + } + + self.small_style = ParagraphStyle( + 'Small', + alignment=0, + **common_paragraph_style, + ) + + self.small_centered_style = ParagraphStyle( + 'SmallCentered', + alignment=1, + **common_paragraph_style, + ) + + def _set_medium_paragraph_styles(self): + self.medium_style = ParagraphStyle( + 'Medium', + parent=self.styles['Normal'], + fontName="DejaVuSans", + spaceAfter=6, + fontSize=12, + alignment=0, + ) + + def _create_doc(self, buffer): + return SimpleDocTemplate( + buffer, + pagesize=(self.page_width, self.page_height), + rightMargin=1, + leftMargin=1, + topMargin=1, + bottomMargin=1 + ) diff --git a/generators/shipping_qr_code_generator/__init__.py b/generators/shipping_qr_code_generator/__init__.py new file mode 100644 index 0000000..798f97f --- /dev/null +++ b/generators/shipping_qr_code_generator/__init__.py @@ -0,0 +1 @@ +from .generator import ShippingQRCodeGenerator diff --git a/generators/shipping_qr_code_generator/generator.py b/generators/shipping_qr_code_generator/generator.py new file mode 100644 index 0000000..df1b428 --- /dev/null +++ b/generators/shipping_qr_code_generator/generator.py @@ -0,0 +1,139 @@ +from io import BytesIO +from typing import Optional + +from fastapi import HTTPException +from reportlab.lib.units import mm +from reportlab.pdfgen.canvas import Canvas +from reportlab.platypus import Paragraph, SimpleDocTemplate, Frame, PageBreak +from reportlab_qrcode import QRCodeImage +from sqlalchemy import select, func +from sqlalchemy.orm import joinedload, selectinload + +from constants import DOMAIN_NAME +from generators.base_pdf_card_generator.base_pdf_card_generator import BasePdfCardGenerator +from models import Deal, ShippingWarehouse, Pallet +from models.shipping import Box + + +class ShippingQRCodeGenerator(BasePdfCardGenerator): + async def _get_deal_by_id(self, deal_id: int) -> Optional[Deal]: + stmt = ( + select(Deal) + .where(Deal.id == deal_id) + .options( + joinedload(Deal.shipping_warehouse), + selectinload(Deal.pallets), + ) + ) + deal = (await self._session.execute(stmt)).one_or_none() + return deal[0] if deal else None + + async def generate_deal(self, deal_id: int) -> BytesIO: + deal = await self._get_deal_by_id(deal_id) + if not deal: + raise HTTPException(status_code=404, detail=f"Сделка с ID {deal_id}a не найдена") + + buffer = BytesIO() + doc: SimpleDocTemplate = self._create_doc(buffer) + + deal_link = f"{DOMAIN_NAME}/deals/{deal_id}" + shipping_warehouse = await self._session.get(ShippingWarehouse, deal.shipping_warehouse_id) + warehouse_name = shipping_warehouse.name if shipping_warehouse else "" + + def on_first_page(canvas: Canvas, doc): + qr = QRCodeImage(deal_link, size=30 * mm) + qr.drawOn(canvas, 0, 30) + + deal_id_paragraph = Paragraph(f"ID: {deal_id}", self.small_centered_style) + deal_name_paragraph = Paragraph(str(deal.name), self.small_centered_style) + + frame = Frame(x1=28 * mm, y1=5 * mm, width=30 * mm, height=30 * mm) + frame.addFromList([deal_id_paragraph, deal_name_paragraph], canvas) + + warehouse_paragraph = Paragraph(warehouse_name, self.small_centered_style) + frame = Frame(x1=0 * mm, y1=-7 * mm, width=58 * mm, height=20 * mm) + frame.addFromList([warehouse_paragraph], canvas) + + empty_paragraph = Paragraph("", self.small_centered_style) + elements = [empty_paragraph] + doc.build(elements, on_first_page) + + buffer.seek(0) + return buffer + + async def generate_pallets(self, deal_id: int): + deal = await self._get_deal_by_id(deal_id) + if not deal: + raise HTTPException(status_code=404, detail=f"Сделка с ID {deal_id}a не найдена") + + buffer = BytesIO() + doc: SimpleDocTemplate = self._create_doc(buffer) + + shipping_warehouse = await self._session.get(ShippingWarehouse, deal.shipping_warehouse_id) + warehouse_name = shipping_warehouse.name if shipping_warehouse else "" + + total_pallets = len(deal.pallets) + elements = [] + + for pallet_counter in range(total_pallets): + elements.append(Paragraph(f"ID: {deal_id}", self.medium_style)) + elements.append(Paragraph(str(deal.name), self.medium_style)) + elements.append(Paragraph(f"Паллет {pallet_counter + 1}/{total_pallets}", self.medium_style)) + elements.append(Paragraph(warehouse_name, self.medium_style)) + elements.append(PageBreak()) + + doc.build(elements) + + buffer.seek(0) + return buffer + + async def _get_boxes_on_pallets_count(self, deal_id): + stmt_boxes_on_pallets = ( + select( + Pallet.id, + func.count(Box.id).label("box_count"), + ) + .join(Box, isouter=True) + .where(Pallet.deal_id == deal_id) + .group_by(Pallet.id) + ) + pallets = (await self._session.execute(stmt_boxes_on_pallets)).all() + return pallets + + async def generate_boxes(self, deal_id: int) -> BytesIO: + deal = await self._get_deal_by_id(deal_id) + if not deal: + raise HTTPException(status_code=404, detail=f"Сделка с ID {deal_id}a не найдена") + + shipping_warehouse = await self._session.get(ShippingWarehouse, deal.shipping_warehouse_id) + warehouse_name = shipping_warehouse.name if shipping_warehouse else "" + + buffer = BytesIO() + doc: SimpleDocTemplate = self._create_doc(buffer) + + elements = [] + + total_pallets = len(deal.pallets) + boxes_on_pallets = await self._get_boxes_on_pallets_count(deal_id) + boxes_without_pallets = len(deal.boxes) + + for box_on_pallet in range(boxes_without_pallets): + elements.append(Paragraph(f"ID: {deal_id}", self.medium_style)) + elements.append(Paragraph(str(deal.name), self.medium_style)) + elements.append(Paragraph(f"Короб {box_on_pallet + 1}/{boxes_without_pallets}", self.medium_style)) + elements.append(Paragraph(warehouse_name, self.medium_style)) + elements.append(PageBreak()) + + for pallet_idx, [_, box_count] in enumerate(boxes_on_pallets): + for box_on_pallet in range(box_count): + elements.append(Paragraph(f"ID: {deal_id}", self.medium_style)) + elements.append(Paragraph(str(deal.name), self.medium_style)) + box_label = f"Паллет {pallet_idx + 1}/{total_pallets}, Короб {box_on_pallet + 1}/{box_count}" + elements.append(Paragraph(box_label, self.medium_style)) + elements.append(Paragraph(warehouse_name, self.medium_style)) + elements.append(PageBreak()) + + doc.build(elements) + + buffer.seek(0) + return buffer diff --git a/generators/work_shifts_qr_code_generator/generator.py b/generators/work_shifts_qr_code_generator/generator.py index 6bac8de..f992c8e 100644 --- a/generators/work_shifts_qr_code_generator/generator.py +++ b/generators/work_shifts_qr_code_generator/generator.py @@ -1,52 +1,16 @@ -import os from io import BytesIO -from reportlab.pdfgen.canvas import Canvas -from reportlab_qrcode import QRCodeImage -from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet from reportlab.lib.units import mm -from reportlab.pdfbase import pdfmetrics -from reportlab.pdfbase.ttfonts import TTFont -from reportlab.platypus import SimpleDocTemplate, Paragraph -from sqlalchemy.ext.asyncio import AsyncSession +from reportlab.pdfgen.canvas import Canvas +from reportlab.platypus import Paragraph +from reportlab_qrcode import QRCodeImage -from constants import APP_PATH +from generators.base_pdf_card_generator.base_pdf_card_generator import BasePdfCardGenerator from models import User from services.user import UserService -class WorkShiftsQRCodeGenerator: - def __init__(self, session: AsyncSession): - self._session = session - assets_folder = os.path.join(APP_PATH, 'assets') - fonts_folder = os.path.join(assets_folder, 'fonts') - font_file_path = os.path.join(fonts_folder, 'DejaVuSans.ttf') - self.page_width = 58 * mm - self.page_height = 40 * mm - pdfmetrics.registerFont(TTFont('DejaVuSans', font_file_path)) - - styles = getSampleStyleSheet() - self.small_style = ParagraphStyle( - 'Small', - parent=styles['Normal'], - alignment=1, - fontName='DejaVuSans', # Specify the new font - fontSize=9, - leading=12, - spaceAfter=2, - rightIndent=2, - ) - - def _create_doc(self, buffer): - return SimpleDocTemplate( - buffer, - pagesize=(self.page_width, self.page_height), - rightMargin=1, - leftMargin=1, - topMargin=1, - bottomMargin=1 - ) - +class WorkShiftsQRCodeGenerator(BasePdfCardGenerator): async def generate(self, user_id: int) -> BytesIO: buffer = BytesIO() doc = self._create_doc(buffer) @@ -59,7 +23,7 @@ class WorkShiftsQRCodeGenerator: position = user.position.name if user.position else "" user_info = Paragraph( f"{user.first_name} {user.second_name}\n{position}", - self.small_style + self.small_centered_style ) doc.build([user_info], on_first_page) diff --git a/main.py b/main.py index e3de9b6..3dbd91f 100644 --- a/main.py +++ b/main.py @@ -48,6 +48,7 @@ routers_list = [ routers.statistics_router, routers.work_shifts_router, routers.expense_router, + routers.shipping_router, ] for router in routers_list: app.include_router(router) diff --git a/models/deal.py b/models/deal.py index 82b8689..4c12f2a 100644 --- a/models/deal.py +++ b/models/deal.py @@ -7,6 +7,7 @@ from sqlalchemy.orm import relationship, backref, Mapped, mapped_column from models.base import BaseModel from .marketplace import BaseMarketplace +from .shipping import Pallet, Box from .shipping_warehouse import ShippingWarehouse if TYPE_CHECKING: @@ -104,6 +105,9 @@ class Deal(BaseModel): manager_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=True) manager: Mapped[Optional["User"]] = relationship(back_populates='managed_deals', lazy='joined') + pallets: Mapped[list[Pallet]] = relationship(back_populates='deal', lazy='selectin') + boxes: Mapped[list[Box]] = relationship(back_populates='deal', lazy='selectin') + class DealStatusHistory(BaseModel): __tablename__ = 'deals_status_history' diff --git a/models/shipping.py b/models/shipping.py new file mode 100644 index 0000000..38d79f1 --- /dev/null +++ b/models/shipping.py @@ -0,0 +1,58 @@ +from typing import TYPE_CHECKING, Optional + +from sqlalchemy import ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship +from models import BaseModel + +if TYPE_CHECKING: + from models import Deal, Product + + +class Pallet(BaseModel): + __tablename__ = 'pallets' + id: Mapped[int] = mapped_column(primary_key=True) + + deal_id: Mapped[int] = mapped_column(ForeignKey('deals.id')) + deal: Mapped['Deal'] = relationship(back_populates='pallets') + + boxes: Mapped[list['Box']] = relationship( + back_populates='pallet', + uselist=True, + lazy='joined', + cascade='all, delete-orphan', + ) + + shipping_products: Mapped[list['ShippingProduct']] = relationship( + back_populates='pallet', + uselist=True, + lazy='joined', + cascade='all, delete-orphan', + ) + + +class ShippingProduct(BaseModel): + __tablename__ = 'shipping_products' + id: Mapped[int] = mapped_column(primary_key=True) + quantity: Mapped[int] = mapped_column() + + product_id: Mapped[int] = mapped_column(ForeignKey('products.id')) + product: Mapped['Product'] = relationship(lazy='joined') + + pallet_id: Mapped[int] = mapped_column(ForeignKey('pallets.id')) + pallet: Mapped['Pallet'] = relationship(lazy='joined') + + +class Box(BaseModel): + __tablename__ = 'boxes' + id: Mapped[int] = mapped_column(primary_key=True) + + quantity: Mapped[int] = mapped_column() + + product_id: Mapped[int] = mapped_column(ForeignKey('products.id')) + product: Mapped['Product'] = relationship(lazy='joined') + + pallet_id: Mapped[Optional[int]] = mapped_column(ForeignKey('pallets.id')) + pallet: Mapped[Pallet] = relationship(back_populates='boxes') + + deal_id: Mapped[Optional[int]] = mapped_column(ForeignKey('deals.id')) + deal: Mapped['Deal'] = relationship(back_populates='boxes') diff --git a/routers/__init__.py b/routers/__init__.py index 79c9806..4461d7e 100644 --- a/routers/__init__.py +++ b/routers/__init__.py @@ -15,4 +15,5 @@ from .billing import billing_router from .task import task_router from .work_shifts import work_shifts_router from .statistics import statistics_router -from .expense import expense_router \ No newline at end of file +from .expense import expense_router +from .shipping import shipping_router diff --git a/routers/shipping.py b/routers/shipping.py new file mode 100644 index 0000000..d8ff2f7 --- /dev/null +++ b/routers/shipping.py @@ -0,0 +1,128 @@ +from io import BytesIO + +from fastapi import APIRouter, Depends, Response + +from backend.dependecies import SessionDependency +from generators.shipping_qr_code_generator import ShippingQRCodeGenerator +from schemas.shipping import * +from services.auth import authorized_user +from services.shipping import ShippingService + +shipping_router = APIRouter( + prefix="/shipping", + tags=["shipping"], +) + + +@shipping_router.post( + '/pallet/{deal_id}', + response_model=CreatePalletResponse, + operation_id='create_pallet', + dependencies=[Depends(authorized_user)], +) +async def create_pallet( + session: SessionDependency, + deal_id: int, +): + return await ShippingService(session).create_pallet(deal_id) + + +@shipping_router.delete( + '/pallet/{pallet_id}', + response_model=DeletePalletResponse, + operation_id='delete_pallet', + dependencies=[Depends(authorized_user)], +) +async def delete_pallet( + session: SessionDependency, + pallet_id: int, +): + return await ShippingService(session).delete_pallet(pallet_id) + + +@shipping_router.post( + '/product', + response_model=UpdateShippingProductResponse, + operation_id='update_shipping_product', + dependencies=[Depends(authorized_user)], +) +async def update_shipping_product( + session: SessionDependency, + request: UpdateShippingProductRequest, +): + return await ShippingService(session).update_shipping_product(request) + + +@shipping_router.delete( + '/product/{shipping_product_id}', + response_model=DeleteShippingProductResponse, + operation_id='delete_shipping_product', + dependencies=[Depends(authorized_user)], +) +async def delete_shipping_product( + session: SessionDependency, + shipping_product_id: int, +): + return await ShippingService(session).delete_shipping_product(shipping_product_id) + + +@shipping_router.post( + '/box', + response_model=UpdateBoxResponse, + operation_id='update_box', + dependencies=[Depends(authorized_user)], +) +async def update_box( + session: SessionDependency, + request: UpdateBoxRequest, +): + return await ShippingService(session).update_box(request) + + +@shipping_router.delete( + '/box/{box_id}', + response_model=DeleteBoxResponse, + operation_id='delete_box', + dependencies=[Depends(authorized_user)], +) +async def delete_box( + session: SessionDependency, + box_id: int, +): + return await ShippingService(session).delete_box(box_id) + + +@shipping_router.get( + "/pdf/deal/{deal_id}", + operation_id="get_deal_qr_code_pdf", +) +async def generate_deal_qr_code_pdf( + session: SessionDependency, + deal_id: int, +): + pdf_file: BytesIO = await ShippingQRCodeGenerator(session).generate_deal(deal_id) + return Response(pdf_file.getvalue(), media_type="application/pdf") + + +@shipping_router.get( + "/pdf/pallets/{deal_id}", + operation_id="get_pallets_pdf", +) +async def generate_pallets_pdf( + session: SessionDependency, + deal_id: int, +): + pdf_file: BytesIO = await ShippingQRCodeGenerator(session).generate_pallets(deal_id) + return Response(pdf_file.getvalue(), media_type="application/pdf") + + +@shipping_router.get( + "/pdf/boxes/{deal_id}", + operation_id="get_boxes_pdf", +) +async def generate_boxes_pdf( + session: SessionDependency, + deal_id: int, +): + pdf_file: BytesIO = await ShippingQRCodeGenerator(session).generate_boxes(deal_id) + return Response(pdf_file.getvalue(), media_type="application/pdf") diff --git a/schemas/deal.py b/schemas/deal.py index 739ee2d..5800c27 100644 --- a/schemas/deal.py +++ b/schemas/deal.py @@ -11,6 +11,7 @@ from schemas.client import ClientSchema from schemas.marketplace import BaseMarketplaceSchema from schemas.product import ProductSchema from schemas.service import ServiceSchema, ServicePriceCategorySchema +from schemas.shipping import PalletSchema, BoxSchema from schemas.shipping_warehouse import ShippingWarehouseSchema from schemas.user import UserSchema @@ -102,6 +103,8 @@ class DealSchema(BaseSchema): category: Optional[ServicePriceCategorySchema] = None group: Optional[DealGroupSchema] = None manager: Optional[UserSchema] = None + pallets: List[PalletSchema] = [] + boxes: List[BoxSchema] = [] delivery_date: Optional[datetime.datetime] = None receiving_slot_date: Optional[datetime.datetime] = None diff --git a/schemas/shipping.py b/schemas/shipping.py new file mode 100644 index 0000000..252a86e --- /dev/null +++ b/schemas/shipping.py @@ -0,0 +1,94 @@ +from typing import Optional + +from schemas.base import BaseSchema, OkMessageSchema +from schemas.product import ProductSchema + + +# region Entities + +class ProductAndQuantitySchema(BaseSchema): + product_id: Optional[int] + quantity: Optional[int] + + +class BoxSchema(BaseSchema): + id: int + quantity: int + product: ProductSchema + pallet_id: Optional[int] + deal_id: Optional[int] + + +class ShippingProductSchema(BaseSchema): + id: int + quantity: int + product: ProductSchema + pallet_id: int + + +class PalletSchema(BaseSchema): + id: int + boxes: list[BoxSchema] + shipping_products: list[ShippingProductSchema] + + +class CreateShippingProductSchema(ProductAndQuantitySchema): + pallet_id: int + + +class UpdateShippingProductSchema(ProductAndQuantitySchema): + shipping_product_id: int + + +class CreateBoxInPalletSchema(ProductAndQuantitySchema): + pallet_id: Optional[int] + + +class CreateBoxInDealSchema(ProductAndQuantitySchema): + deal_id: Optional[int] + + +class UpdateBoxSchema(ProductAndQuantitySchema): + box_id: Optional[int] + + +# endregion + +# region Requests + +class UpdateShippingProductRequest(BaseSchema): + data: CreateShippingProductSchema | UpdateShippingProductSchema + + +class UpdateBoxRequest(BaseSchema): + data: CreateBoxInDealSchema | CreateBoxInPalletSchema | UpdateBoxSchema + + +# endregion + +# region Responses + +class CreatePalletResponse(OkMessageSchema): + pass + + +class DeletePalletResponse(OkMessageSchema): + pass + + +class UpdateShippingProductResponse(OkMessageSchema): + pass + + +class UpdateBoxResponse(OkMessageSchema): + pass + + +class DeleteBoxResponse(OkMessageSchema): + pass + + +class DeleteShippingProductResponse(OkMessageSchema): + pass + +# endregion diff --git a/services/deal.py b/services/deal.py index e176122..8d01472 100644 --- a/services/deal.py +++ b/services/deal.py @@ -10,6 +10,7 @@ import models.secondary from models import User, Service, Client, DealProductService, deal_relations, GroupBillRequest from models.deal import * from models.deal_group import DealGroup +from models.shipping import ShippingProduct from schemas.client import ClientDetailsSchema from schemas.deal import * from services.auth import AuthService @@ -331,7 +332,13 @@ class DealService(BaseService): .joinedload(DealStatusHistory.user), selectinload(Deal.status_history) .noload(DealStatusHistory.deal), - + selectinload(Deal.pallets) + .selectinload(Pallet.shipping_products) + .selectinload(ShippingProduct.product) + .noload(Product.barcodes), + selectinload(Deal.boxes) + .selectinload(Box.product) + .noload(Product.barcodes), ) .where(Deal.id == deal_id) ) diff --git a/services/shipping.py b/services/shipping.py new file mode 100644 index 0000000..543666c --- /dev/null +++ b/services/shipping.py @@ -0,0 +1,121 @@ +from sqlalchemy import select, and_ + +from models import Deal, Pallet, Box +from models.shipping import ShippingProduct +from schemas.shipping import * +from services.base import BaseService + + +class ShippingService(BaseService): + async def create_pallet(self, deal_id: int) -> CreatePalletResponse: + deal = await self.session.get(Deal, deal_id) + if not deal: + return CreatePalletResponse(ok=False, message="Сделка не найдена") + + pallet = Pallet(deal_id=deal_id) + self.session.add(pallet) + await self.session.commit() + return CreatePalletResponse(ok=True, message="Паллет успешно создан") + + async def delete_pallet(self, pallet_id: int) -> DeletePalletResponse: + pallet = await self.session.get(Pallet, pallet_id) + if not pallet: + return DeletePalletResponse(ok=False, message="Паллет не найден") + + await self.session.delete(pallet) + await self.session.commit() + return DeletePalletResponse(ok=True, message="Паллет успешно удален") + + async def _update_box(self, data: UpdateBoxSchema) -> tuple[bool, str]: + box = await self.session.get(Box, data.box_id) + if not box: + return False, f"Короб с ID:{data.box_id} не найден" + + box.quantity = data.quantity + box.product_id = data.product_id + await self.session.commit() + return True, "Короб обновлен" + + async def _create_box(self, data: CreateBoxInDealSchema | CreateBoxInPalletSchema): + box = Box(**data.model_dump()) + self.session.add(box) + await self.session.commit() + + async def _create_box_in_deal(self, data: CreateBoxInDealSchema) -> tuple[bool, str]: + deal = await self.session.get(Deal, data.deal_id) + if not deal: + return False, f"Сделка с ID:{data.deal_id} не найдена" + + await self._create_box(data) + + return True, f"Короб для сделки ID:{data.deal_id} добавлен" + + async def _create_box_in_pallet(self, data: CreateBoxInPalletSchema) -> tuple[bool, str]: + pallet = await self.session.get(Pallet, data.pallet_id) + if not pallet: + return False, f"Паллет с ID:{data.pallet_id} не найден" + + await self._create_box(data) + + return True, f"Короб добавлен в паллет" + + async def update_box(self, request: UpdateBoxRequest) -> UpdateBoxResponse: + data_keys = request.data.model_dump().keys() + if "box_id" in data_keys: + ok, message = await self._update_box(request.data) + elif "pallet_id" in data_keys: + ok, message = await self._create_box_in_pallet(CreateBoxInPalletSchema.model_validate(request.data)) + else: + ok, message = await self._create_box_in_deal(CreateBoxInDealSchema.model_validate(request.data)) + + return UpdateBoxResponse(ok=ok, message=message) + + async def delete_box(self, deal_id: int) -> DeleteBoxResponse: + box = await self.session.get(Box, deal_id) + if not box: + return DeleteBoxResponse(ok=False, message=f"Короб с ID:{deal_id} не найден") + + await self.session.delete(box) + await self.session.commit() + return DeleteBoxResponse(ok=True, message="Короб успешно удален") + + async def _get_shipping_product(self, pallet_id: int, product_id: int) -> Optional[ShippingProduct]: + stmt = ( + select(ShippingProduct) + .where( + and_(ShippingProduct.pallet_id == pallet_id, ShippingProduct.product_id == product_id) + ) + ) + shipping_product = (await self.session.execute(stmt)).unique().one_or_none() + return shipping_product[0] if shipping_product else None + + async def _update_shipping_product(self, data: UpdateShippingProductSchema) -> tuple[bool, str]: + shipping_product = await self.session.get(ShippingProduct, data.shipping_product_id) + shipping_product.product_id = data.product_id + shipping_product.quantity = data.quantity + await self.session.commit() + return True, "Запись о товаре на паллете успешно изменена" + + async def _create_shipping_product(self, data: CreateShippingProductSchema) -> tuple[bool, str]: + shipping_product = ShippingProduct(**data.model_dump()) + self.session.add(shipping_product) + await self.session.commit() + return True, "Запись о товаре на паллете успешно добавлена" + + async def update_shipping_product(self, request: UpdateShippingProductRequest) -> UpdateShippingProductResponse: + data_keys = request.data.model_dump().keys() + if "shipping_product_id" in data_keys: + ok, message = await self._update_shipping_product(request.data) + else: + ok, message = await self._create_shipping_product(request.data) + + return UpdateShippingProductResponse(ok=ok, message=message) + + async def delete_shipping_product(self, shipping_product_id: int) -> DeleteShippingProductResponse: + shipping_product = await self.session.get(ShippingProduct, shipping_product_id) + if not shipping_product: + return DeleteShippingProductResponse(ok=False, message=f"Запись для данного паллета и товара не найдена") + + await self.session.delete(shipping_product) + await self.session.commit() + return DeleteShippingProductResponse(ok=True, message="Запись о товаре на паллете успешно удалена")