feat: add fixed asset classes management page and embed functionality
- Implemented a new page for managing fixed asset classes with full CRUD functionality. - Added filtering options for searching classes by code, description, type, and fraction. - Integrated dialogs for inserting, editing, and deleting classes with validation. - Enhanced error handling and user feedback with toast notifications. - Created an embedded iframe for the fixed asset classes page in the merchandise section.
This commit is contained in:
107
backend/api/v1/modules/a24/fa/fa_classes/dto.py
Normal file
107
backend/api/v1/modules/a24/fa/fa_classes/dto.py
Normal file
@@ -0,0 +1,107 @@
|
||||
"""
|
||||
DTOs (Data Transfer Objects) para módulo de clases de activos fijos (FA)
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
|
||||
class FAClassCreateDTO(BaseModel):
|
||||
"""DTO para crear una clase de activo fijo"""
|
||||
|
||||
class_id: int = Field(..., description="ID de la clase base en a76.classes")
|
||||
|
||||
import_tariff_code: Optional[str] = Field(
|
||||
None, max_length=10, description="Código de fracción de importación"
|
||||
)
|
||||
import_tariff_type: Optional[str] = Field(
|
||||
None, max_length=6, description="Tipo de fracción de importación"
|
||||
)
|
||||
export_tariff_code: Optional[str] = Field(
|
||||
None, max_length=10, description="Código de fracción de exportación"
|
||||
)
|
||||
export_tariff_type: Optional[str] = Field(
|
||||
None, max_length=6, description="Tipo de fracción de exportación"
|
||||
)
|
||||
depreciation_rate: Optional[Decimal] = Field(
|
||||
None, description="Tasa de depreciación anual", ge=0, le=100
|
||||
)
|
||||
fda_code: Optional[str] = Field(
|
||||
None, max_length=20, description="Código FDA"
|
||||
)
|
||||
eccn_code: Optional[str] = Field(
|
||||
None, max_length=20, description="Código ECCN (Export Control Classification Number)"
|
||||
)
|
||||
class_enabled: Optional[bool] = Field(
|
||||
True, description="Indica si la clase está habilitada"
|
||||
)
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class FAClassUpdateDTO(BaseModel):
|
||||
"""DTO para actualizar una clase de activo fijo"""
|
||||
|
||||
import_tariff_code: Optional[str] = Field(
|
||||
None, max_length=10, description="Código de fracción de importación"
|
||||
)
|
||||
import_tariff_type: Optional[str] = Field(
|
||||
None, max_length=6, description="Tipo de fracción de importación"
|
||||
)
|
||||
export_tariff_code: Optional[str] = Field(
|
||||
None, max_length=10, description="Código de fracción de exportación"
|
||||
)
|
||||
export_tariff_type: Optional[str] = Field(
|
||||
None, max_length=6, description="Tipo de fracción de exportación"
|
||||
)
|
||||
depreciation_rate: Optional[Decimal] = Field(
|
||||
None, description="Tasa de depreciación anual", ge=0, le=100
|
||||
)
|
||||
fda_code: Optional[str] = Field(
|
||||
None, max_length=20, description="Código FDA"
|
||||
)
|
||||
eccn_code: Optional[str] = Field(
|
||||
None, max_length=20, description="Código ECCN"
|
||||
)
|
||||
class_enabled: Optional[bool] = Field(
|
||||
None, description="Indica si la clase está habilitada"
|
||||
)
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class FAClassResponseDTO(BaseModel):
|
||||
"""DTO para respuesta de clase de activo fijo"""
|
||||
|
||||
id: int
|
||||
tenant_id: int
|
||||
company_id: int
|
||||
class_id: int
|
||||
import_tariff_code: Optional[str] = None
|
||||
import_tariff_type: Optional[str] = None
|
||||
export_tariff_code: Optional[str] = None
|
||||
export_tariff_type: Optional[str] = None
|
||||
depreciation_rate: Optional[Decimal] = None
|
||||
fda_code: Optional[str] = None
|
||||
eccn_code: Optional[str] = None
|
||||
class_enabled: Optional[bool] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class FAClassListResponseDTO(BaseModel):
|
||||
"""DTO para respuesta de lista paginada de clases de activos fijos"""
|
||||
|
||||
items: list[FAClassResponseDTO]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
@@ -1,4 +1,5 @@
|
||||
from decimal import Decimal
|
||||
from typing import Optional
|
||||
|
||||
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
|
||||
from core.database import Base
|
||||
@@ -24,11 +25,11 @@ class QClasses(Base, TenantScopedMixin, TimestampMixin):
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
class_id: Mapped[int] = mapped_column(Integer, nullable=False)
|
||||
|
||||
import_tariff_code: Mapped[str] = mapped_column(String(10)) # FRACCIONIMPO
|
||||
import_tariff_type: Mapped[str] = mapped_column(String(6)) # TIPOFRACIMPO
|
||||
export_tariff_code: Mapped[str] = mapped_column(String(10)) # FRACCIONEXPO
|
||||
export_tariff_type: Mapped[str] = mapped_column(String(6)) # TIPOFRACEXPO
|
||||
depreciation_rate: Mapped[Decimal] = mapped_column(Numeric(5, 2)) # TASADEPRECIA
|
||||
fda_code: Mapped[str] = mapped_column(String(20)) # FDA
|
||||
eccn_code: Mapped[str] = mapped_column(String(20)) # ECCN
|
||||
class_enabled: Mapped[bool] = mapped_column(Boolean) # HABILITADESHABILITACLASE
|
||||
import_tariff_code: Mapped[Optional[str]] = mapped_column(String(10), nullable=True) # FRACCIONIMPO
|
||||
import_tariff_type: Mapped[Optional[str]] = mapped_column(String(6), nullable=True) # TIPOFRACIMPO
|
||||
export_tariff_code: Mapped[Optional[str]] = mapped_column(String(10), nullable=True) # FRACCIONEXPO
|
||||
export_tariff_type: Mapped[Optional[str]] = mapped_column(String(6), nullable=True) # TIPOFRACEXPO
|
||||
depreciation_rate: Mapped[Optional[Decimal]] = mapped_column(Numeric(5, 2), nullable=True) # TASADEPRECIA
|
||||
fda_code: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) # FDA
|
||||
eccn_code: Mapped[Optional[str]] = mapped_column(String(20), nullable=True) # ECCN
|
||||
class_enabled: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) # HABILITADESHABILITACLASE
|
||||
|
||||
32
backend/api/v1/modules/a24/fa/fa_classes/routes.py
Normal file
32
backend/api/v1/modules/a24/fa/fa_classes/routes.py
Normal file
@@ -0,0 +1,32 @@
|
||||
"""
|
||||
Endpoints API para gestión de clases de activos fijos (FA)
|
||||
"""
|
||||
|
||||
from typing import Any, Dict
|
||||
from fastapi import Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from core.database import get_core_db
|
||||
from core.security import get_current_user
|
||||
from api.v1.common.tenant_crud_routes import TenantCRUDRoutes, validate_access_to_resource
|
||||
|
||||
from .dto import FAClassCreateDTO, FAClassResponseDTO, FAClassUpdateDTO
|
||||
from .service import FAClassService
|
||||
|
||||
# Create router with generic CRUD routes
|
||||
crud_routes = TenantCRUDRoutes(
|
||||
service=FAClassService,
|
||||
create_schema=FAClassCreateDTO,
|
||||
update_schema=FAClassUpdateDTO,
|
||||
response_schema=FAClassResponseDTO,
|
||||
prefix="/fa/classes",
|
||||
tags=["a24 / fa / classes"],
|
||||
resource_name="Fixed Asset Class",
|
||||
id_name="fa_class_id",
|
||||
enable_list=True,
|
||||
enable_filters=True,
|
||||
default_page_size=50,
|
||||
max_page_size=100,
|
||||
)
|
||||
|
||||
router = crud_routes.router
|
||||
223
backend/api/v1/modules/a24/fa/fa_classes/service.py
Normal file
223
backend/api/v1/modules/a24/fa/fa_classes/service.py
Normal file
@@ -0,0 +1,223 @@
|
||||
"""
|
||||
Capa de servicio para lógica de negocio de clases de activos fijos (FA)
|
||||
"""
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import HTTPException
|
||||
from sqlalchemy import and_
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from .dto import FAClassCreateDTO, FAClassResponseDTO, FAClassUpdateDTO
|
||||
from .models import QClasses
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FAClassService:
|
||||
"""Servicio para gestión de clases de activos fijos"""
|
||||
|
||||
@staticmethod
|
||||
def get_all(
|
||||
db: Session,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
filters: Optional[Dict[str, Any]] = None,
|
||||
) -> tuple[List[QClasses], int]:
|
||||
"""
|
||||
Obtener todas las clases de activos fijos con paginación y filtros
|
||||
"""
|
||||
query = db.query(QClasses).filter(
|
||||
QClasses.tenant_id == tenant_id, QClasses.company_id == company_id
|
||||
)
|
||||
|
||||
if filters:
|
||||
if filters.get("class_id"):
|
||||
query = query.filter(QClasses.class_id == filters["class_id"])
|
||||
if filters.get("fda_code"):
|
||||
query = query.filter(
|
||||
QClasses.fda_code.ilike(f"%{filters['fda_code']}%")
|
||||
)
|
||||
if filters.get("class_enabled") is not None:
|
||||
query = query.filter(
|
||||
QClasses.class_enabled == filters["class_enabled"]
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
items = query.offset(skip).limit(limit).all()
|
||||
|
||||
return items, total
|
||||
|
||||
@staticmethod
|
||||
def get_by_id(
|
||||
db: Session, fa_class_id: int, tenant_id: int, company_id: int
|
||||
) -> Optional[QClasses]:
|
||||
"""Obtener una clase de activo fijo por ID"""
|
||||
return (
|
||||
db.query(QClasses)
|
||||
.filter(
|
||||
QClasses.id == fa_class_id,
|
||||
QClasses.tenant_id == tenant_id,
|
||||
QClasses.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def get_by_class_id(
|
||||
db: Session, class_id: int, tenant_id: int, company_id: int
|
||||
) -> Optional[QClasses]:
|
||||
"""Obtener una clase de activo fijo por class_id de a76"""
|
||||
return (
|
||||
db.query(QClasses)
|
||||
.filter(
|
||||
QClasses.class_id == class_id,
|
||||
QClasses.tenant_id == tenant_id,
|
||||
QClasses.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
db: Session, fa_class_data: FAClassCreateDTO, tenant_id: int, company_id: int
|
||||
) -> QClasses:
|
||||
"""Crear una nueva clase de activo fijo"""
|
||||
try:
|
||||
# Verificar que la clase base existe en a76.classes
|
||||
from api.v1.modules.a76.classes.models import Class
|
||||
|
||||
base_class = (
|
||||
db.query(Class)
|
||||
.filter(
|
||||
Class.id == fa_class_data.class_id,
|
||||
Class.tenant_id == tenant_id,
|
||||
Class.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not base_class:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Base class with id {fa_class_data.class_id} not found"
|
||||
)
|
||||
|
||||
# Verificar que no exista ya una clase de activo fijo para esta clase base
|
||||
existing = FAClassService.get_by_class_id(
|
||||
db, fa_class_data.class_id, tenant_id, company_id
|
||||
)
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Fixed asset class already exists for class_id {fa_class_data.class_id}"
|
||||
)
|
||||
|
||||
data_dict = fa_class_data.model_dump()
|
||||
|
||||
new_fa_class = QClasses(
|
||||
**data_dict,
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
)
|
||||
|
||||
db.add(new_fa_class)
|
||||
db.commit()
|
||||
db.refresh(new_fa_class)
|
||||
|
||||
logger.info(
|
||||
f"Created fixed asset class {new_fa_class.id} for class_id {new_fa_class.class_id}"
|
||||
)
|
||||
|
||||
return new_fa_class
|
||||
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"IntegrityError creating fixed asset class: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Database constraint violation: {str(e.orig)}"
|
||||
)
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error creating fixed asset class: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@staticmethod
|
||||
def update(
|
||||
db: Session,
|
||||
fa_class_id: int,
|
||||
tenant_id: int,
|
||||
fa_class_data: FAClassUpdateDTO,
|
||||
company_id: int,
|
||||
) -> QClasses:
|
||||
"""Actualizar una clase de activo fijo"""
|
||||
fa_class = FAClassService.get_by_id(db, fa_class_id, tenant_id, company_id)
|
||||
|
||||
if not fa_class:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Fixed asset class {fa_class_id} not found"
|
||||
)
|
||||
|
||||
try:
|
||||
update_data = fa_class_data.model_dump(exclude_unset=True)
|
||||
|
||||
for key, value in update_data.items():
|
||||
setattr(fa_class, key, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(fa_class)
|
||||
|
||||
logger.info(f"Updated fixed asset class {fa_class_id}")
|
||||
|
||||
return fa_class
|
||||
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"IntegrityError updating fixed asset class: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Database constraint violation: {str(e.orig)}"
|
||||
)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error updating fixed asset class: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@staticmethod
|
||||
def delete(
|
||||
db: Session, fa_class_id: int, tenant_id: int, company_id: int
|
||||
) -> None:
|
||||
"""Eliminar una clase de activo fijo"""
|
||||
fa_class = FAClassService.get_by_id(db, fa_class_id, tenant_id, company_id)
|
||||
|
||||
if not fa_class:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail=f"Fixed asset class {fa_class_id} not found"
|
||||
)
|
||||
|
||||
try:
|
||||
db.delete(fa_class)
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Deleted fixed asset class {fa_class_id}")
|
||||
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
logger.error(f"IntegrityError deleting fixed asset class: {str(e)}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="Cannot delete: Fixed asset class is referenced by other records"
|
||||
)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error deleting fixed asset class: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
Reference in New Issue
Block a user