Files
plantillas-proyectos/backend/api/v1/modules/a76/items/service.py
AlexeerCT 30b8daf16e Refactor item management: Update schemas, models, and API routes
- Renamed customs-related schemas in line_items to LineCustomCreate, LineCustomUpdate, and LineCustomResponse.
- Adjusted models to streamline invoice_id mapping in Item model.
- Enhanced item routes to include company_id in summary statistics endpoint.
- Refactored ItemService to improve item creation and update logic, removing redundant methods.
- Updated frontend components for item management, including new item creation and editing functionalities.
- Added API client for items with CRUD operations and improved error handling.
2025-12-30 09:22:52 -06:00

347 lines
13 KiB
Python

"""
Service layer for Items business logic
Handles CRUD operations for Item with complete one-to-one relationships:
Item -> LineItem -> LineFinancial
-> LineQuantity
-> LineCustoms
-> LineDescription
-> LineReference
"""
import logging
from typing import Optional, List, Tuple
from fastapi import HTTPException
from sqlalchemy import and_, or_
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session, joinedload
from api.v1.modules.a76.items.line_items.schemas import LineItemCreate, LineItemUpdate
from .schemas import ItemCreate, ItemUpdate
from .line_items.models import LineItem
from .line_financials.models import LineFinancial
from .line_quantities.models import LineQuantity
from .line_customs.models import LineCustom
from .line_descriptions.models import LineDescription
from .line_references.models import LineReference
from .models import Item
logger = logging.getLogger(__name__)
class ItemService:
"""
Service for managing Items and related entities with tenant/company isolation
"""
@staticmethod
def get_by_id(
db: Session,
item_id: int,
tenant_id: int,
company_id: int
) -> Optional[Item]:
"""Get an item by ID with tenant/company validation"""
return (
db.query(Item)
.options(
joinedload(Item.lines).joinedload(LineItem.financial),
joinedload(Item.lines).joinedload(LineItem.quantity),
joinedload(Item.lines).joinedload(LineItem.customs),
joinedload(Item.lines).joinedload(LineItem.description),
joinedload(Item.lines).joinedload(LineItem.reference),
)
.filter(
Item.id == item_id,
Item.tenant_id == tenant_id,
Item.company_id == company_id,
)
.first()
)
@staticmethod
def get_all(
db: Session,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 100,
filters: Optional[dict] = None,
) -> Tuple[List[Item], int]:
"""Get all items for a tenant/company with pagination and optional filters"""
query = (
db.query(Item)
.options(
joinedload(Item.lines).joinedload(LineItem.financial),
joinedload(Item.lines).joinedload(LineItem.quantity),
joinedload(Item.lines).joinedload(LineItem.customs),
joinedload(Item.lines).joinedload(LineItem.description),
joinedload(Item.lines).joinedload(LineItem.reference),
)
.filter(
Item.tenant_id == tenant_id,
Item.company_id == company_id,
)
)
# Apply filters if provided
if filters:
if filters.get("invoice_id"):
query = query.filter(Item.invoice_id == filters["invoice_id"])
if filters.get("item_type"):
query = query.filter(Item.item_type == filters["item_type"])
if filters.get("system_origin"):
query = query.filter(Item.system_origin ==
filters["system_origin"])
if filters.get("search"):
search_term = f"%{filters['search']}%"
query = query.filter(
or_(
Item.invoice_number.ilike(search_term),
Item.reference_number.ilike(search_term),
Item.order.ilike(search_term),
Item.guide_number.ilike(search_term),
)
)
total = query.count()
items = query.offset(skip).limit(limit).all()
return items, total
@staticmethod
def get_by_invoice(
db: Session,
invoice_id: int,
tenant_id: int,
company_id: int,
skip: int = 0,
limit: int = 100,
) -> Tuple[List[Item], int]:
"""Get all items for a specific invoice"""
query = (
db.query(Item)
.options(
joinedload(Item.lines).joinedload(LineItem.financial),
joinedload(Item.lines).joinedload(LineItem.quantity),
joinedload(Item.lines).joinedload(LineItem.customs),
joinedload(Item.lines).joinedload(LineItem.description),
joinedload(Item.lines).joinedload(LineItem.reference),
)
.filter(
Item.invoice_id == invoice_id,
Item.tenant_id == tenant_id,
Item.company_id == company_id,
)
)
total = query.count()
items = query.offset(skip).limit(limit).all()
return items, total
@staticmethod
def create(
db: Session,
item_data: ItemCreate,
tenant_id: int,
company_id: int,
) -> Item:
"""Create a new item with all related nested data (multiple lines)"""
try:
# Extract lines data
lines_data = item_data.lines or []
item_dict = item_data.model_dump(exclude={"lines"})
# Add tenant and company
item_dict["tenant_id"] = tenant_id
item_dict["company_id"] = company_id
# Create the item
db_item = Item(**item_dict)
db.add(db_item)
db.flush() # Get the item ID
# Create line items if provided
for line_data in lines_data:
# Extract nested data from line
financial_data = line_data.financial
quantity_data = line_data.quantity
customs_data = line_data.customs
description_data = line_data.description
reference_data = line_data.reference
line_dict = line_data.model_dump(
exclude={"financial", "quantity",
"customs", "description", "reference"}
)
line_dict["item_id"] = db_item.id
# Create line item
db_line = LineItem(**line_dict)
db.add(db_line)
db.flush() # Get the line ID
# Create financial data if provided
if financial_data:
financial_dict = financial_data.model_dump()
financial_dict["item_line_id"] = db_line.id
db_financial = LineFinancial(**financial_dict)
db.add(db_financial)
# Create quantity data if provided
if quantity_data:
quantity_dict = quantity_data.model_dump()
quantity_dict["item_line_id"] = db_line.id
db_quantity = LineQuantity(**quantity_dict)
db.add(db_quantity)
# Create customs data if provided
if customs_data:
customs_dict = customs_data.model_dump()
customs_dict["item_line_id"] = db_line.id
db_customs = LineCustom(**customs_dict)
db.add(db_customs)
# Create description data if provided
if description_data:
description_dict = description_data.model_dump()
description_dict["item_line_id"] = db_line.id
db_description = LineDescription(**description_dict)
db.add(db_description)
# Create reference data if provided
if reference_data:
reference_dict = reference_data.model_dump()
reference_dict["item_line_id"] = db_line.id
db_reference = LineReference(**reference_dict)
db.add(db_reference)
db.commit()
db.refresh(db_item)
return db_item
except IntegrityError as e:
db.rollback()
logger.error(f"Error creating item: {e}")
raise HTTPException(
status_code=400,
detail="Item creation failed - integrity constraint violated",
)
except Exception as e:
db.rollback()
logger.error(f"Unexpected error creating item: {e}")
raise HTTPException(status_code=500, detail="Error creating item")
@staticmethod
def update(
db: Session,
item_id: int,
item_data: ItemUpdate,
tenant_id: int,
company_id: int,
) -> Item:
"""Update an item and optionally its nested data (multiple lines)"""
try:
# Get existing item
db_item = ItemService.get_by_id(db, item_id, tenant_id, company_id)
if not db_item:
raise HTTPException(status_code=404, detail="Item not found")
# Extract lines data
lines_data = item_data.lines
item_dict = item_data.model_dump(
exclude={"lines"}, exclude_unset=True)
# Update item fields
for key, value in item_dict.items():
setattr(db_item, key, value)
# Update lines if provided (replace all lines)
if lines_data is not None:
# Delete existing lines (cascade will handle nested data)
for existing_line in db_item.lines:
db.delete(existing_line)
db.flush()
# Create new lines
for line_data in lines_data:
# Extract nested data from line
financial_data = line_data.financial
quantity_data = line_data.quantity
customs_data = line_data.customs
description_data = line_data.description
reference_data = line_data.reference
line_dict = line_data.model_dump(
exclude={"financial", "quantity",
"customs", "description", "reference"},
exclude_unset=True
)
line_dict["item_id"] = db_item.id
db_line = LineItem(**line_dict)
db.add(db_line)
db.flush()
# Create nested data if provided
if financial_data is not None:
financial_dict = financial_data.model_dump(
exclude_unset=True)
financial_dict["item_line_id"] = db_line.id
db.add(LineFinancial(**financial_dict))
if quantity_data is not None:
quantity_dict = quantity_data.model_dump(
exclude_unset=True)
quantity_dict["item_line_id"] = db_line.id
db.add(LineQuantity(**quantity_dict))
if customs_data is not None:
customs_dict = customs_data.model_dump(
exclude_unset=True)
customs_dict["item_line_id"] = db_line.id
db.add(LineCustom(**customs_dict))
if description_data is not None:
description_dict = description_data.model_dump(
exclude_unset=True)
description_dict["item_line_id"] = db_line.id
db.add(LineDescription(**description_dict))
if reference_data is not None:
reference_dict = reference_data.model_dump(
exclude_unset=True)
reference_dict["item_line_id"] = db_line.id
db.add(LineReference(**reference_dict))
db.commit()
db.refresh(db_item)
return db_item
except HTTPException:
raise
except Exception as e:
db.rollback()
logger.error(f"Unexpected error updating item: {e}")
raise HTTPException(status_code=500, detail="Error updating item")
@staticmethod
def delete(
db: Session,
item_id: int,
tenant_id: int,
company_id: int,
) -> bool:
"""Delete an item and all its related data (cascade delete)"""
try:
db_item = ItemService.get_by_id(db, item_id, tenant_id, company_id)
if not db_item:
return False
db.delete(db_item)
db.commit()
return True
except Exception as e:
db.rollback()
logger.error(f"Error deleting item: {e}")
raise HTTPException(status_code=500, detail="Error deleting item")