From d609c10edbd2658c730902d34ff690acdd96f08d Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Tue, 14 Jan 2025 21:35:39 +0400 Subject: [PATCH] feat: residues accounting --- .../residual_qr_code_generator/__init__.py | 1 + .../residual_qr_code_generator/generator.py | 166 +++++++++++++++ main.py | 1 + models/__init__.py | 1 + models/client.py | 8 +- models/residues.py | 62 ++++++ models/shipping.py | 2 +- routers/__init__.py | 1 + routers/client.py | 14 +- routers/residues.py | 157 ++++++++++++++ schemas/client.py | 9 + schemas/residues.py | 133 ++++++++++++ services/client.py | 37 +++- services/product.py | 2 +- services/residues.py | 192 ++++++++++++++++++ 15 files changed, 776 insertions(+), 10 deletions(-) create mode 100644 generators/residual_qr_code_generator/__init__.py create mode 100644 generators/residual_qr_code_generator/generator.py create mode 100644 models/residues.py create mode 100644 routers/residues.py create mode 100644 schemas/residues.py create mode 100644 services/residues.py diff --git a/generators/residual_qr_code_generator/__init__.py b/generators/residual_qr_code_generator/__init__.py new file mode 100644 index 0000000..6422176 --- /dev/null +++ b/generators/residual_qr_code_generator/__init__.py @@ -0,0 +1 @@ +from .generator import ResidualQRCodeGenerator diff --git a/generators/residual_qr_code_generator/generator.py b/generators/residual_qr_code_generator/generator.py new file mode 100644 index 0000000..988599b --- /dev/null +++ b/generators/residual_qr_code_generator/generator.py @@ -0,0 +1,166 @@ +from io import BytesIO +from typing import Optional + +from reportlab.lib.units import mm +from reportlab.pdfgen.canvas import Canvas +from reportlab.platypus import Paragraph, SimpleDocTemplate, PageBreak, Frame +from reportlab_qrcode import QRCodeImage +from sqlalchemy import select +from sqlalchemy.orm import selectinload, joinedload + +from barcodes.pdf.pdf_maker import PdfMaker +from generators.base_pdf_card_generator.base_pdf_card_generator import BasePdfCardGenerator +from models import Client, ResidualPallet, ResidualBox + + +class ResidualQRCodeGenerator(BasePdfCardGenerator): + async def _get_client_by_id(self, client_id: int) -> Optional[Client]: + stmt = ( + select(Client) + .where(Client.id == client_id) + .options( + selectinload(Client.boxes), + selectinload(Client.pallets) + .selectinload(ResidualPallet.boxes), + ) + ) + client = (await self._session.execute(stmt)).one_or_none() + return client[0] if client else None + + @staticmethod + def _split_string(string: str) -> list[int]: + if not string: + return [] + return [int(item) for item in string.split(",")] + + async def generate(self, pallet_ids_str: str, box_ids_str: str): + pallet_ids = self._split_string(pallet_ids_str) + box_ids = self._split_string(box_ids_str) + + pallets_buffer = await self.generate_pallets(pallet_ids) + boxes_buffer = await self.generate_boxes(box_ids) + return self._merge_pdfs([pallets_buffer, boxes_buffer]) + + async def _get_pallets(self, pallet_ids: list[int]) -> list[ResidualPallet]: + stmt = ( + select(ResidualPallet) + .options( + joinedload(ResidualPallet.client), + ) + .where(ResidualPallet.id.in_(pallet_ids)) + .order_by(ResidualPallet.id.asc()) + ) + pallets = await self._session.execute(stmt) + return list(pallets.unique().scalars().all()) + + def _generate_empty_doc(self) -> BytesIO: + buffer = BytesIO() + doc: SimpleDocTemplate = self._create_doc(buffer) + doc.build([]) + buffer.seek(0) + return buffer + + async def generate_pallets(self, pallet_ids: list[int]) -> BytesIO: + if not pallet_ids: + return self._generate_empty_doc() + + buffer = BytesIO() + doc: SimpleDocTemplate = self._create_doc(buffer) + + pallet_idx = 0 + pallets = await self._get_pallets(pallet_ids) + client = pallets[0].client + + def on_page(canvas: Canvas, _): + nonlocal pallet_idx, pallets + pallet_id = pallets[pallet_idx].id + + qr = QRCodeImage(f"П{pallet_id}", size=30 * mm) + qr.drawOn(canvas, 0, 30) + + object_name = Paragraph(f"Паллет", self.small_centered_style) + pallet_id = Paragraph(f"ID: П{pallet_id}", self.small_centered_style) + + frame = Frame(x1=28 * mm, y1=3 * mm, width=30 * mm, height=30 * mm) + frame.addFromList([object_name, pallet_id], canvas) + + client_name = Paragraph(f"Клиент: {client.name}", self.small_centered_style) + frame = Frame(x1=0 * mm, y1=-7 * mm, width=58 * mm, height=20 * mm) + frame.addFromList([client_name], canvas) + + pallet_idx += 1 + + elements = [] + for _ in range(len(pallets)): + elements.append(Paragraph("", self.medium_style)) + elements.append(PageBreak()) + + doc.build(elements, on_page, on_page) + + buffer.seek(0) + return buffer + + async def _get_boxes(self, box_ids: list[int]) -> list[ResidualBox]: + stmt = ( + select(ResidualBox) + .options( + joinedload(ResidualBox.client), + selectinload(ResidualBox.pallet) + .joinedload(ResidualPallet.client), + ) + .where(ResidualBox.id.in_(box_ids)) + .order_by(ResidualBox.id.asc()) + ) + boxes = await self._session.execute(stmt) + return list(boxes.unique().scalars().all()) + + async def generate_boxes(self, box_ids: list[int]) -> BytesIO: + if not box_ids: + return self._generate_empty_doc() + + buffer = BytesIO() + doc: SimpleDocTemplate = self._create_doc(buffer) + + box_idx = 0 + boxes = await self._get_boxes(box_ids) + client = boxes[0].client or boxes[0].pallet.client + + def on_page(canvas: Canvas, _): + nonlocal box_idx + box_id = boxes[box_idx].id + + qr = QRCodeImage(f"П{box_id}", size=30 * mm) + qr.drawOn(canvas, 0, 30) + + box_info = [ + Paragraph("Короб", self.small_centered_style), + Paragraph(f"ID: К{box_id}", self.small_centered_style), + ] + if boxes[box_idx].pallet_id: + box_info.append(Paragraph("На паллете", self.small_centered_style)) + box_info.append(Paragraph(f"ID: П{boxes[box_idx].pallet_id}", self.small_centered_style)) + + frame = Frame(x1=28 * mm, y1=8 * mm, width=30 * mm, height=30 * mm) + frame.addFromList(box_info, canvas) + + client_name = Paragraph(f"Клиент: {client.name}", self.small_centered_style) + frame = Frame(x1=0 * mm, y1=-7 * mm, width=58 * mm, height=20 * mm) + frame.addFromList([client_name], canvas) + + box_idx += 1 + + elements = [] + for _ in range(len(boxes)): + elements.append(Paragraph("", self.medium_style)) + elements.append(PageBreak()) + + doc.build(elements, on_page, on_page) + + buffer.seek(0) + return buffer + + def _merge_pdfs(self, buffers: list[BytesIO]) -> BytesIO: + pdf_maker = PdfMaker((self.page_width, self.page_height)) + for buffer in buffers: + pdf_maker.add_pdfs(buffer) + return pdf_maker.get_bytes() diff --git a/main.py b/main.py index 520d333..ab0e46d 100644 --- a/main.py +++ b/main.py @@ -50,6 +50,7 @@ routers_list = [ routers.transaction_router, routers.shipping_router, routers.department_router, + routers.residues_router, ] for router in routers_list: app.include_router(router) diff --git a/models/__init__.py b/models/__init__.py index c916de0..a5ab0dc 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -14,5 +14,6 @@ from .billing import * from .marketplace_products import * from .deal_group import * from .transaction import * +from .residues import * configure_mappers() diff --git a/models/client.py b/models/client.py index 96d7e02..0f9ef19 100644 --- a/models/client.py +++ b/models/client.py @@ -1,10 +1,13 @@ -from typing import Optional +from typing import Optional, TYPE_CHECKING from sqlalchemy import Column, Integer, String, DateTime, ForeignKey from sqlalchemy.orm import relationship, Mapped, mapped_column from models import BaseModel +if TYPE_CHECKING: + from models import ResidualPallet, ResidualBox + class Client(BaseModel): __tablename__ = 'clients' @@ -28,6 +31,9 @@ class Client(BaseModel): comment: Mapped[Optional[str]] = mapped_column(nullable=True, server_default=None, comment='Комментарий') + pallets: Mapped[list["ResidualPallet"]] = relationship(back_populates='client', lazy='selectin') + boxes: Mapped[list["ResidualBox"]] = relationship(back_populates='client', lazy='selectin') + class ClientDetails(BaseModel): __tablename__ = 'client_details' diff --git a/models/residues.py b/models/residues.py new file mode 100644 index 0000000..14f6ac4 --- /dev/null +++ b/models/residues.py @@ -0,0 +1,62 @@ +import datetime +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 Product, Client + + +class ResidualPallet(BaseModel): + __tablename__ = 'residual_pallets' + id: Mapped[int] = mapped_column(primary_key=True) + created_at: Mapped[datetime.datetime] = mapped_column(nullable=False) + + client_id: Mapped[Optional[int]] = mapped_column(ForeignKey('clients.id')) + client: Mapped['Client'] = relationship(back_populates='pallets') + + boxes: Mapped[list['ResidualBox']] = relationship( + back_populates='pallet', + uselist=True, + lazy='joined', + cascade='all, delete-orphan', + ) + + residual_products: Mapped[list['ResidualProduct']] = relationship( + back_populates='pallet', + uselist=True, + lazy='joined', + cascade='all, delete-orphan', + ) + + +class ResidualProduct(BaseModel): + __tablename__ = 'residual_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[Optional[int]] = mapped_column(ForeignKey('residual_pallets.id')) + pallet: Mapped[ResidualPallet] = relationship(lazy='joined', back_populates="residual_products") + + box_id: Mapped[Optional[int]] = mapped_column(ForeignKey('residual_boxes.id')) + box: Mapped['ResidualBox'] = relationship(back_populates='residual_products') + + +class ResidualBox(BaseModel): + __tablename__ = 'residual_boxes' + id: Mapped[int] = mapped_column(primary_key=True) + created_at: Mapped[datetime.datetime] = mapped_column(nullable=False) + + pallet_id: Mapped[Optional[int]] = mapped_column(ForeignKey('residual_pallets.id')) + pallet: Mapped[ResidualPallet] = relationship(back_populates='boxes') + + client_id: Mapped[Optional[int]] = mapped_column(ForeignKey('clients.id')) + client: Mapped['Client'] = relationship(back_populates='boxes') + + residual_products: Mapped[list['ResidualProduct']] = relationship(back_populates='box') diff --git a/models/shipping.py b/models/shipping.py index 243bbc5..1f367a9 100644 --- a/models/shipping.py +++ b/models/shipping.py @@ -5,7 +5,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship from models import BaseModel if TYPE_CHECKING: - from models import Deal, Product + from models import Deal, Product, Client class Pallet(BaseModel): diff --git a/routers/__init__.py b/routers/__init__.py index c6ceeed..b425640 100644 --- a/routers/__init__.py +++ b/routers/__init__.py @@ -18,3 +18,4 @@ from .statistics import statistics_router from .transaction import transaction_router from .shipping import shipping_router from .department import department_router +from .residues import residues_router diff --git a/routers/client.py b/routers/client.py index 924f935..cfa131d 100644 --- a/routers/client.py +++ b/routers/client.py @@ -16,8 +16,6 @@ client_router = APIRouter( ) - - @client_router.get('/search', operation_id='search_clients') async def search_clients( name: str, @@ -52,6 +50,18 @@ async def get_all_clients( return await ClientService(session).get_all() +@client_router.get( + '/get/{client_id}', + operation_id='get_client', + response_model=ClientGetResponse +) +async def get_client( + session: Annotated[AsyncSession, Depends(get_session)], + client_id: int, +): + return await ClientService(session).get_by_id(client_id) + + @client_router.post( '/create', operation_id='create_client_api', diff --git a/routers/residues.py b/routers/residues.py new file mode 100644 index 0000000..5f8f4c9 --- /dev/null +++ b/routers/residues.py @@ -0,0 +1,157 @@ +from io import BytesIO + +from fastapi import APIRouter, Depends, Response, Query + +from backend.dependecies import SessionDependency +from generators.residual_qr_code_generator import ResidualQRCodeGenerator +from schemas.residues import * +from services.auth import authorized_user +from services.residues import ResiduesService + +residues_router = APIRouter( + prefix="/residues", + tags=["residues"], +) + + +@residues_router.get( + '/pallet/{pallet_id}', + response_model=GetResidualPalletResponse, + operation_id='get_residual_pallet', + dependencies=[Depends(authorized_user)], +) +async def get_pallet( + session: SessionDependency, + pallet_id: int, +): + return await ResiduesService(session).get_pallet(pallet_id) + + +@residues_router.post( + '/pallet', + response_model=CreateResidualPalletResponse, + operation_id='create_residual_pallet', + dependencies=[Depends(authorized_user)], +) +async def create_pallet( + session: SessionDependency, + request: CreateResidualPalletRequest, +): + return await ResiduesService(session).create_pallet(request) + + +@residues_router.delete( + '/pallet/{pallet_id}', + response_model=DeleteResidualPalletResponse, + operation_id='delete_residual_pallet', + dependencies=[Depends(authorized_user)], +) +async def delete_pallet( + session: SessionDependency, + pallet_id: int, +): + return await ResiduesService(session).delete_pallet(pallet_id) + + +@residues_router.post( + '/product', + response_model=CreateResidualProductResponse, + operation_id='create_residual_product', + dependencies=[Depends(authorized_user)], +) +async def create_residual_product( + session: SessionDependency, + request: CreateResidualProductRequest, +): + return await ResiduesService(session).create_residual_product(request) + + +@residues_router.post( + '/product/{residual_product_id}', + response_model=UpdateResidualProductResponse, + operation_id='update_residual_product', + dependencies=[Depends(authorized_user)], +) +async def update_residual_product( + session: SessionDependency, + request: UpdateResidualProductRequest, + residual_product_id: int, +): + return await ResiduesService(session).update_residual_product(request, residual_product_id) + + +@residues_router.delete( + '/product/{residual_product_id}', + response_model=DeleteResidualProductResponse, + operation_id='delete_residual_product', + dependencies=[Depends(authorized_user)], +) +async def delete_residual_product( + session: SessionDependency, + residual_product_id: int, +): + return await ResiduesService(session).delete_residual_product(residual_product_id) + + +@residues_router.get( + '/box/{box_id}', + response_model=GetResidualBoxResponse, + operation_id='get_residual_box', + dependencies=[Depends(authorized_user)], +) +async def get_box( + session: SessionDependency, + box_id: int, +): + return await ResiduesService(session).get_box(box_id) + + +@residues_router.post( + '/box', + response_model=CreateResidualBoxResponse, + operation_id='create_residual_box', + dependencies=[Depends(authorized_user)], +) +async def create_box( + session: SessionDependency, + request: CreateResidualBoxRequest, +): + return await ResiduesService(session).create_box(request) + + +@residues_router.delete( + '/box/{box_id}', + response_model=DeleteResidualBoxResponse, + operation_id='delete_residual_box', + dependencies=[Depends(authorized_user)], +) +async def delete_box( + session: SessionDependency, + box_id: int, +): + return await ResiduesService(session).delete_box(box_id) + + +@residues_router.post( + '/receipt', + response_model=LoadReceiptResponse, + operation_id='receipt', +) +async def receipt( + session: SessionDependency, + request: LoadReceiptRequest, +): + return await ResiduesService(session).load_receipt(request) + + +@residues_router.get( + "/pdf/", + operation_id="get_pdf", +) +async def generate_pdf( + session: SessionDependency, + pallet_ids: str = Query(""), + box_ids: str = Query(""), +): + pdf_file: BytesIO = await ResidualQRCodeGenerator(session).generate(pallet_ids, box_ids) + return Response(pdf_file.getvalue(), media_type="application/pdf") diff --git a/schemas/client.py b/schemas/client.py index 3b95f4e..6707d75 100644 --- a/schemas/client.py +++ b/schemas/client.py @@ -4,6 +4,7 @@ from pydantic import field_validator from schemas.barcode import BarcodeTemplateSchema from schemas.base import BaseSchema, OkMessageSchema +from schemas.residues import ResidualBoxSchema, ResidualPalletSchema # region Entities @@ -27,6 +28,10 @@ class ClientSchema(BaseSchema): details: ClientDetailsSchema | None = None +class ClientDetailedSchema(ClientSchema): + pallets: List[ResidualPalletSchema] = [] + boxes: List[ResidualBoxSchema] = [] + # endregion # region Requests @@ -66,6 +71,10 @@ class ClientGetAllResponse(BaseSchema): clients: List[ClientSchema] +class ClientGetResponse(BaseSchema): + client: ClientDetailedSchema + + class ClientCreateResponse(OkMessageSchema): pass diff --git a/schemas/residues.py b/schemas/residues.py new file mode 100644 index 0000000..bf855a0 --- /dev/null +++ b/schemas/residues.py @@ -0,0 +1,133 @@ +from datetime import datetime +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 ResidualProductSchema(BaseSchema): + id: int + quantity: int + product: ProductSchema + pallet_id: Optional[int] + box_id: Optional[int] + + +class ResidualBoxSchema(BaseSchema): + id: int + created_at: datetime + pallet_id: Optional[int] + client_id: Optional[int] + residual_products: list[ResidualProductSchema] + + +class ResidualPalletSchema(BaseSchema): + id: int + created_at: datetime + boxes: list[ResidualBoxSchema] + residual_products: list[ResidualProductSchema] + + +class CreateResidualProductSchema(ProductAndQuantitySchema): + pallet_id: Optional[int] + box_id: Optional[int] + + +class UpdateResidualProductSchema(ProductAndQuantitySchema): + pass + + +class ReceiptBoxSchema(BaseSchema): + products: list[ProductAndQuantitySchema] + + +class ReceiptPalletSchema(BaseSchema): + products: list[ProductAndQuantitySchema] + boxes: list[ReceiptBoxSchema] + + +# endregion + +# region Requests + +class CreateResidualPalletRequest(BaseSchema): + client_id: int + + +class CreateResidualProductRequest(BaseSchema): + data: CreateResidualProductSchema + + +class UpdateResidualProductRequest(BaseSchema): + data: UpdateResidualProductSchema + + +class CreateResidualBoxRequest(BaseSchema): + client_id: Optional[int] + pallet_id: Optional[int] + + +class LoadReceiptRequest(BaseSchema): + pallets: list[ReceiptPalletSchema] + boxes: list[ReceiptBoxSchema] + client_id: int + + +class GetPdfRequest(BaseSchema): + pallet_ids: list[int] + box_ids: list[int] + + +# endregion + +# region Responses + +class GetResidualPalletResponse(BaseSchema): + pallet: ResidualPalletSchema + client_id: int + + +class CreateResidualPalletResponse(OkMessageSchema): + pass + + +class DeleteResidualPalletResponse(OkMessageSchema): + pass + + +class CreateResidualProductResponse(OkMessageSchema): + pass + + +class UpdateResidualProductResponse(OkMessageSchema): + pass + + +class GetResidualBoxResponse(BaseSchema): + box: ResidualBoxSchema + client_id: int + + +class CreateResidualBoxResponse(OkMessageSchema): + pass + + +class DeleteResidualBoxResponse(OkMessageSchema): + pass + + +class DeleteResidualProductResponse(OkMessageSchema): + pass + + +class LoadReceiptResponse(OkMessageSchema): + pass + +# endregion diff --git a/services/client.py b/services/client.py index 5d6fa61..6dcf9f5 100644 --- a/services/client.py +++ b/services/client.py @@ -1,10 +1,11 @@ import datetime from typing import Union +from fastapi import HTTPException from sqlalchemy import select, update -from sqlalchemy.orm import joinedload +from sqlalchemy.orm import joinedload, selectinload, noload -from models import Client, ClientDetails, User +from models import Client, ClientDetails, User, ResidualPallet, ResidualBox, ResidualProduct, Product from schemas.client import * from services.base import BaseService @@ -15,9 +16,35 @@ class ClientService(BaseService): client = await self.session.scalar(select(Client).where(Client.name == name)) return client - async def get_by_id(self, client_id: int) -> Union[Client, None]: + async def _get_by_id(self, client_id: int) -> Union[Client, None]: return await self.session.get(Client, client_id) + async def get_by_id(self, client_id: int) -> ClientGetResponse: + stmt = ( + select(Client) + .options( + selectinload(Client.pallets) + .selectinload(ResidualPallet.residual_products) + .selectinload(ResidualProduct.product) + .noload(Product.barcodes), + selectinload(Client.pallets) + .selectinload(ResidualPallet.boxes) + .selectinload(ResidualBox.residual_products) + .selectinload(ResidualProduct.product) + .noload(Product.barcodes), + selectinload(Client.boxes) + .selectinload(ResidualBox.residual_products) + .selectinload(ResidualProduct.product) + .noload(Product.barcodes), + ) + .where(Client.id == client_id) + ) + client = (await self.session.execute(stmt)).one_or_none() + client = client[0] if client else None + if not client: + raise HTTPException(status_code=404, detail="Клиент не найден") + return ClientGetResponse(client=client) + async def get_details_by_client_id(self, client_id: int) -> Union[ClientDetails, None]: details = await self.session.scalar(select(ClientDetails).where(ClientDetails.client_id == client_id)) return details @@ -101,7 +128,7 @@ class ClientService(BaseService): async def update(self, request: ClientUpdateRequest, user: User) -> ClientUpdateResponse: try: - client = await self.get_by_id(request.data.id) + client = await self._get_by_id(request.data.id) if not client: return ClientUpdateResponse(ok=False, message='Клиент не найден') request_dict = request.data.dict() @@ -124,7 +151,7 @@ class ClientService(BaseService): async def delete(self, request: ClientDeleteRequest) -> ClientDeleteResponse: try: - client = await self.get_by_id(request.client_id) + client = await self._get_by_id(request.client_id) if not client: return ClientDeleteResponse(ok=False, message='Клиент не найден') await self.session.delete(client) diff --git a/services/product.py b/services/product.py index 9570b93..ad5473f 100644 --- a/services/product.py +++ b/services/product.py @@ -105,7 +105,7 @@ class ProductService(BaseService): is_pagination_valid = is_valid_pagination(pagination) total_pages = 0 total_items = 0 - stmt: Query = ( + stmt = ( select( Product ) diff --git a/services/residues.py b/services/residues.py new file mode 100644 index 0000000..8705c11 --- /dev/null +++ b/services/residues.py @@ -0,0 +1,192 @@ +from fastapi import HTTPException +from sqlalchemy import select +from sqlalchemy.orm import selectinload, joinedload +from starlette import status + +from models import ResidualPallet, ResidualBox, ResidualProduct, Client, Product +from schemas.residues import * +from services.base import BaseService + + +class ResiduesService(BaseService): + async def _get_pallet_by_id(self, pallet_id: int) -> Optional[ResidualPallet]: + stmt = ( + select(ResidualPallet) + .options( + selectinload(ResidualPallet.boxes) + .selectinload(ResidualBox.residual_products) + .selectinload(ResidualProduct.product) + .noload(Product.barcodes), + joinedload(ResidualPallet.client), + selectinload(ResidualPallet.residual_products) + .selectinload(ResidualProduct.product) + .noload(Product.barcodes), + ) + .where(ResidualPallet.id == pallet_id) + ) + pallet = (await self.session.execute(stmt)).one_or_none() + return pallet[0] if pallet else None + + async def get_pallet(self, pallet_id: int) -> GetResidualPalletResponse: + pallet = await self._get_pallet_by_id(pallet_id) + if not pallet: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f'Паллет с ID:{pallet_id} не найден') + + return GetResidualPalletResponse(pallet=pallet, client_id=pallet.client_id) + + async def create_pallet(self, request: CreateResidualPalletRequest) -> CreateResidualPalletResponse: + client = await self.session.get(Client, request.client_id) + if not client: + return CreateResidualPalletResponse(ok=False, message=f'Клиент с ID:{request.client_id} не найден') + + pallet = ResidualPallet(client_id=request.client_id, created_at=datetime.now()) + self.session.add(pallet) + await self.session.commit() + return CreateResidualPalletResponse(ok=True, message='Паллет успешно создан') + + async def delete_pallet(self, pallet_id: int) -> DeleteResidualPalletResponse: + pallet = await self.session.get(ResidualPallet, pallet_id) + if not pallet: + return DeleteResidualPalletResponse(ok=False, message='Паллет не найден') + + await self.session.delete(pallet) + await self.session.commit() + return DeleteResidualPalletResponse(ok=True, message='Паллет успешно удален') + + async def _get_box_by_id(self, box_id: int) -> Optional[ResidualBox]: + stmt = ( + select(ResidualBox) + .options( + selectinload(ResidualBox.pallet) + .noload(ResidualPallet.boxes), + selectinload(ResidualBox.pallet) + .joinedload(ResidualPallet.client), + selectinload(ResidualBox.residual_products) + .selectinload(ResidualProduct.product) + .noload(Product.barcodes), + ) + .where(ResidualBox.id == box_id) + ) + box = (await self.session.execute(stmt)).one_or_none() + return box[0] if box else None + + async def get_box(self, box_id: int) -> GetResidualBoxResponse: + box = await self._get_box_by_id(box_id) + if not box: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f'Короб с ID:{box_id} не найден') + + if box.client_id: + client_id = box.client_id + else: + pallet = await self._get_pallet_by_id(box.pallet_id) + client_id = pallet.client_id + + return GetResidualBoxResponse(box=box, client_id=client_id) + + async def create_box(self, request: CreateResidualBoxRequest) -> CreateResidualBoxResponse: + if request.client_id: + client = await self.session.get(Client, request.client_id) + if not client: + return CreateResidualBoxResponse(ok=False, message=f'Клиент с ID:{request.client_id} не найден') + else: + pallet = await self.session.get(ResidualPallet, request.pallet_id) + if not pallet: + return CreateResidualBoxResponse(ok=False, message=f'Паллет с ID:{request.pallet_id} не найден') + + box = ResidualBox(created_at=datetime.now(), **request.model_dump()) + self.session.add(box) + await self.session.commit() + return CreateResidualBoxResponse(ok=True, message='Короб успешно создан') + + async def delete_box(self, box_id: int) -> DeleteResidualBoxResponse: + box = await self.session.get(ResidualBox, box_id) + if not box: + return DeleteResidualBoxResponse(ok=False, message=f'Короб с ID:{box_id} не найден') + + await self.session.delete(box) + await self.session.commit() + return DeleteResidualBoxResponse(ok=True, message='Короб успешно удален') + + async def _create_residual_product( + self, + obj: ResidualBox | ResidualPallet, + request: CreateResidualProductRequest, + ) -> tuple[bool, str]: + try: + existing_residual = next(p for p in obj.residual_products if p.product_id == request.data.product_id) + existing_residual.quantity += request.data.quantity + self.session.add(existing_residual) + except StopIteration: + residual_product = ResidualProduct(**request.data.model_dump()) + self.session.add(residual_product) + await self.session.commit() + return True, "Товар успешно добавлен" + + async def create_residual_product(self, request: CreateResidualProductRequest) -> CreateResidualProductResponse: + if request.data.box_id: + obj = await self._get_box_by_id(request.data.box_id) + if not obj: + return CreateResidualProductResponse(ok=False, message=f'Короб с ID:{request.data.box_id} не найден') + else: + obj = await self.session.get(ResidualPallet, request.data.pallet_id) + if not obj: + return CreateResidualProductResponse( + ok=False, message=f'Паллет с ID:{request.data.pallet_id} не найден', + ) + ok, message = await self._create_residual_product(obj, request) + return CreateResidualProductResponse(ok=ok, message=message) + + async def update_residual_product( + self, + request: UpdateResidualProductRequest, + residual_product_id: int + ) -> UpdateResidualProductResponse: + residual_product = await self.session.get(ResidualProduct, residual_product_id) + residual_product.product_id = request.data.product_id + residual_product.quantity = request.data.quantity + await self.session.commit() + return UpdateResidualProductResponse(ok=True, message='Запись о товаре на паллете успешно изменена') + + async def delete_residual_product(self, residual_product_id: int) -> DeleteResidualProductResponse: + residual_product = await self.session.get(ResidualProduct, residual_product_id) + if not residual_product: + return DeleteResidualProductResponse(ok=False, message=f'Запись для данного паллета и товара не найдена') + + await self.session.delete(residual_product) + await self.session.commit() + return DeleteResidualProductResponse(ok=True, message='Запись о товаре на паллете успешно удалена') + + async def load_receipt(self, request: LoadReceiptRequest) -> LoadReceiptResponse: + if not await self.session.get(Client, request.client_id): + return LoadReceiptResponse(ok=False, message=f'Клиент с ID {request.client_id}') + + await self._load_receipt_boxes(request.boxes, request.client_id) + await self._load_receipt_pallets(request.pallets, request.client_id) + await self.session.commit() + + return LoadReceiptResponse(ok=True, message='Приемка успешно завершена') + + async def _load_receipt_boxes(self, boxes: list[ReceiptBoxSchema], client_id: int = None, pallet_id: int = None): + for receipt_box in boxes: + box = ResidualBox(client_id=client_id, pallet_id=pallet_id, created_at=datetime.now()) + self.session.add(box) + await self.session.flush() + await self._load_receipt_products(receipt_box.products, box_id=box.id) + + async def _load_receipt_pallets(self, pallets: list[ReceiptPalletSchema], client_id: int): + for receipt_pallet in pallets: + pallet = ResidualPallet(client_id=client_id, created_at=datetime.now()) + self.session.add(pallet) + await self.session.flush() + await self._load_receipt_boxes(receipt_pallet.boxes, pallet.id) + await self._load_receipt_products(receipt_pallet.products, pallet_id=pallet.id) + + async def _load_receipt_products( + self, + products: list[ReceiptPalletSchema], + box_id: int = None, + pallet_id: int = None, + ): + for receipt_product in products: + product = ResidualProduct(box_id=box_id, pallet_id=pallet_id, **receipt_product.model_dump()) + self.session.add(product)