feat: residues accounting
This commit is contained in:
1
generators/residual_qr_code_generator/__init__.py
Normal file
1
generators/residual_qr_code_generator/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
from .generator import ResidualQRCodeGenerator
|
||||
166
generators/residual_qr_code_generator/generator.py
Normal file
166
generators/residual_qr_code_generator/generator.py
Normal 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()
|
||||
1
main.py
1
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)
|
||||
|
||||
@@ -14,5 +14,6 @@ from .billing import *
|
||||
from .marketplace_products import *
|
||||
from .deal_group import *
|
||||
from .transaction import *
|
||||
from .residues import *
|
||||
|
||||
configure_mappers()
|
||||
|
||||
@@ -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'
|
||||
|
||||
62
models/residues.py
Normal file
62
models/residues.py
Normal 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')
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
157
routers/residues.py
Normal file
157
routers/residues.py
Normal 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")
|
||||
@@ -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
|
||||
|
||||
|
||||
133
schemas/residues.py
Normal file
133
schemas/residues.py
Normal 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
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
192
services/residues.py
Normal file
192
services/residues.py
Normal 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)
|
||||
Reference in New Issue
Block a user