feat: residues accounting

This commit is contained in:
2025-01-14 21:35:39 +04:00
parent 1f26f94d96
commit d609c10edb
15 changed files with 776 additions and 10 deletions

View File

@@ -0,0 +1 @@
from .generator import ResidualQRCodeGenerator

View File

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

View File

@@ -50,6 +50,7 @@ routers_list = [
routers.transaction_router, routers.transaction_router,
routers.shipping_router, routers.shipping_router,
routers.department_router, routers.department_router,
routers.residues_router,
] ]
for router in routers_list: for router in routers_list:
app.include_router(router) app.include_router(router)

View File

@@ -14,5 +14,6 @@ from .billing import *
from .marketplace_products import * from .marketplace_products import *
from .deal_group import * from .deal_group import *
from .transaction import * from .transaction import *
from .residues import *
configure_mappers() configure_mappers()

View File

@@ -1,10 +1,13 @@
from typing import Optional from typing import Optional, TYPE_CHECKING
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey from sqlalchemy import Column, Integer, String, DateTime, ForeignKey
from sqlalchemy.orm import relationship, Mapped, mapped_column from sqlalchemy.orm import relationship, Mapped, mapped_column
from models import BaseModel from models import BaseModel
if TYPE_CHECKING:
from models import ResidualPallet, ResidualBox
class Client(BaseModel): class Client(BaseModel):
__tablename__ = 'clients' __tablename__ = 'clients'
@@ -28,6 +31,9 @@ class Client(BaseModel):
comment: Mapped[Optional[str]] = mapped_column(nullable=True, server_default=None, comment='Комментарий') 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): class ClientDetails(BaseModel):
__tablename__ = 'client_details' __tablename__ = 'client_details'

62
models/residues.py Normal file
View File

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

View File

@@ -5,7 +5,7 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship
from models import BaseModel from models import BaseModel
if TYPE_CHECKING: if TYPE_CHECKING:
from models import Deal, Product from models import Deal, Product, Client
class Pallet(BaseModel): class Pallet(BaseModel):

View File

@@ -18,3 +18,4 @@ from .statistics import statistics_router
from .transaction import transaction_router from .transaction import transaction_router
from .shipping import shipping_router from .shipping import shipping_router
from .department import department_router from .department import department_router
from .residues import residues_router

View File

@@ -16,8 +16,6 @@ client_router = APIRouter(
) )
@client_router.get('/search', operation_id='search_clients') @client_router.get('/search', operation_id='search_clients')
async def search_clients( async def search_clients(
name: str, name: str,
@@ -52,6 +50,18 @@ async def get_all_clients(
return await ClientService(session).get_all() 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( @client_router.post(
'/create', '/create',
operation_id='create_client_api', operation_id='create_client_api',

157
routers/residues.py Normal file
View File

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

View File

@@ -4,6 +4,7 @@ from pydantic import field_validator
from schemas.barcode import BarcodeTemplateSchema from schemas.barcode import BarcodeTemplateSchema
from schemas.base import BaseSchema, OkMessageSchema from schemas.base import BaseSchema, OkMessageSchema
from schemas.residues import ResidualBoxSchema, ResidualPalletSchema
# region Entities # region Entities
@@ -27,6 +28,10 @@ class ClientSchema(BaseSchema):
details: ClientDetailsSchema | None = None details: ClientDetailsSchema | None = None
class ClientDetailedSchema(ClientSchema):
pallets: List[ResidualPalletSchema] = []
boxes: List[ResidualBoxSchema] = []
# endregion # endregion
# region Requests # region Requests
@@ -66,6 +71,10 @@ class ClientGetAllResponse(BaseSchema):
clients: List[ClientSchema] clients: List[ClientSchema]
class ClientGetResponse(BaseSchema):
client: ClientDetailedSchema
class ClientCreateResponse(OkMessageSchema): class ClientCreateResponse(OkMessageSchema):
pass pass

133
schemas/residues.py Normal file
View File

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

View File

@@ -1,10 +1,11 @@
import datetime import datetime
from typing import Union from typing import Union
from fastapi import HTTPException
from sqlalchemy import select, update 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 schemas.client import *
from services.base import BaseService from services.base import BaseService
@@ -15,9 +16,35 @@ class ClientService(BaseService):
client = await self.session.scalar(select(Client).where(Client.name == name)) client = await self.session.scalar(select(Client).where(Client.name == name))
return client 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) 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]: 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)) details = await self.session.scalar(select(ClientDetails).where(ClientDetails.client_id == client_id))
return details return details
@@ -101,7 +128,7 @@ class ClientService(BaseService):
async def update(self, request: ClientUpdateRequest, user: User) -> ClientUpdateResponse: async def update(self, request: ClientUpdateRequest, user: User) -> ClientUpdateResponse:
try: try:
client = await self.get_by_id(request.data.id) client = await self._get_by_id(request.data.id)
if not client: if not client:
return ClientUpdateResponse(ok=False, message='Клиент не найден') return ClientUpdateResponse(ok=False, message='Клиент не найден')
request_dict = request.data.dict() request_dict = request.data.dict()
@@ -124,7 +151,7 @@ class ClientService(BaseService):
async def delete(self, request: ClientDeleteRequest) -> ClientDeleteResponse: async def delete(self, request: ClientDeleteRequest) -> ClientDeleteResponse:
try: try:
client = await self.get_by_id(request.client_id) client = await self._get_by_id(request.client_id)
if not client: if not client:
return ClientDeleteResponse(ok=False, message='Клиент не найден') return ClientDeleteResponse(ok=False, message='Клиент не найден')
await self.session.delete(client) await self.session.delete(client)

View File

@@ -105,7 +105,7 @@ class ProductService(BaseService):
is_pagination_valid = is_valid_pagination(pagination) is_pagination_valid = is_valid_pagination(pagination)
total_pages = 0 total_pages = 0
total_items = 0 total_items = 0
stmt: Query = ( stmt = (
select( select(
Product Product
) )

192
services/residues.py Normal file
View File

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