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:
@@ -13,8 +13,10 @@ from sqlalchemy.orm import Session
|
||||
from .dto import (
|
||||
ClassBasicDTO,
|
||||
ClassCreateDTO,
|
||||
ClassCreateDTOFA,
|
||||
ClassListDTO,
|
||||
ClassResponseDTO,
|
||||
ClassResponseDTOFA,
|
||||
ClassSearchDTO,
|
||||
ClassUpdateDTO,
|
||||
)
|
||||
@@ -38,6 +40,7 @@ class ClassService:
|
||||
"""
|
||||
Get all classes for a tenant with pagination and filters
|
||||
"""
|
||||
logger.info(f"get_all called with tenant_id={tenant_id}, company_id={company_id}, skip={skip}, limit={limit}")
|
||||
query = db.query(Class).filter(
|
||||
Class.tenant_id == tenant_id, Class.company_id == company_id
|
||||
)
|
||||
@@ -70,7 +73,8 @@ class ClassService:
|
||||
|
||||
total = query.count()
|
||||
items = query.offset(skip).limit(limit).all()
|
||||
|
||||
|
||||
logger.info(f"get_all returning {len(items)} items out of {total} total")
|
||||
return items, total
|
||||
|
||||
@staticmethod
|
||||
@@ -109,18 +113,19 @@ class ClassService:
|
||||
if existing:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Class with code '{data_dict['class_code']}' already exists for this tenant and company"
|
||||
detail=f" El código de clase '{data_dict['class_code']}' ya existe. Por favor use un código diferente."
|
||||
)
|
||||
|
||||
# Validate material_key exists if provided
|
||||
if data_dict.get("material_key"):
|
||||
from api.v1.modules.public.reference_data.material_types.models import MaterialType
|
||||
material_exists = db.query(MaterialType).filter(
|
||||
MaterialType.key == data_dict["material_key"]
|
||||
).first()
|
||||
if not material_exists:
|
||||
# Set to None if material_key doesn't exist
|
||||
data_dict["material_key"] = None
|
||||
# Validate material_key exists (now required)
|
||||
from api.v1.modules.public.reference_data.material_types.models import MaterialType
|
||||
material_exists = db.query(MaterialType).filter(
|
||||
MaterialType.key == data_dict["material_key"]
|
||||
).first()
|
||||
if not material_exists:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Material type '{data_dict['material_key']}' does not exist"
|
||||
)
|
||||
|
||||
class_obj = Class(**data_dict)
|
||||
class_obj.tenant_id = tenant_id
|
||||
@@ -147,11 +152,16 @@ class ClassService:
|
||||
company_id: int,
|
||||
) -> Optional[Class]:
|
||||
"""Update a class"""
|
||||
logger.info(f"Update called for class_id={class_id}, tenant_id={tenant_id}, company_id={company_id}")
|
||||
logger.info(f"Update data received: {class_data.model_dump(exclude_unset=True)}")
|
||||
|
||||
class_obj = ClassService.get_by_id(db, class_id, tenant_id, company_id)
|
||||
if not class_obj:
|
||||
logger.warning(f"Class {class_id} not found for tenant {tenant_id}, company {company_id}")
|
||||
return None
|
||||
|
||||
update_data = class_data.model_dump(exclude_unset=True)
|
||||
logger.info(f"Update data after model_dump: {update_data}")
|
||||
|
||||
# Validate material_key exists if provided
|
||||
if "material_key" in update_data and update_data["material_key"]:
|
||||
@@ -163,24 +173,241 @@ class ClassService:
|
||||
# Set to None if material_key doesn't exist
|
||||
update_data["material_key"] = None
|
||||
|
||||
# Validate class_code is unique if being changed
|
||||
if "class_code" in update_data and update_data["class_code"]:
|
||||
new_code = update_data["class_code"]
|
||||
# Check if another class with this code exists (excluding current class)
|
||||
# The unique constraint is on (tenant_id, company_id, client_id, class_code)
|
||||
existing_class = db.query(Class).filter(
|
||||
Class.class_code == new_code,
|
||||
Class.tenant_id == tenant_id,
|
||||
Class.company_id == company_id,
|
||||
Class.client_id == class_obj.client_id, # Same client
|
||||
Class.id != class_id # Exclude current class
|
||||
).first()
|
||||
|
||||
logger.info(f"Checking for duplicate class_code '{new_code}' for client {class_obj.client_id}")
|
||||
if existing_class:
|
||||
logger.warning(f"Duplicate class_code found: {existing_class.id}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"El código '{new_code}' ya está en uso para este cliente. Por favor ingrese un código diferente."
|
||||
)
|
||||
|
||||
for field, value in update_data.items():
|
||||
setattr(class_obj, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(class_obj)
|
||||
return class_obj
|
||||
try:
|
||||
logger.info(f"Attempting to commit changes for class {class_id}")
|
||||
db.commit()
|
||||
db.refresh(class_obj)
|
||||
logger.info(f"Successfully updated class {class_id}")
|
||||
return class_obj
|
||||
except IntegrityError as e:
|
||||
db.rollback()
|
||||
error_msg = str(e.orig)
|
||||
logger.error(f"IntegrityError updating class {class_id}: {error_msg}")
|
||||
|
||||
# Check if it's a duplicate class_code error
|
||||
if "already exists" in error_msg.lower() or "duplicate" in error_msg.lower():
|
||||
# Extract the code from update_data if it was changed
|
||||
code = update_data.get("class_code", class_obj.class_code)
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"El código '{code}' ya está en uso. Por favor ingrese un código diferente."
|
||||
)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Error al actualizar la clase: {error_msg}"
|
||||
)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Unexpected error updating class {class_id}: {type(e).__name__}: {str(e)}")
|
||||
raise
|
||||
|
||||
@staticmethod
|
||||
def delete(db: Session, class_id: int, tenant_id: int, company_id: int) -> bool:
|
||||
"""Delete a class"""
|
||||
"""Delete a class (and its FA extension if exists)"""
|
||||
from api.v1.modules.a24.fa.fa_classes.models import QClasses
|
||||
|
||||
class_obj = ClassService.get_by_id(db, class_id, tenant_id, company_id)
|
||||
if not class_obj:
|
||||
return False
|
||||
|
||||
# Delete FA extension first (if exists) to avoid FK constraint violation
|
||||
fa_extension = db.query(QClasses).filter(
|
||||
QClasses.class_id == class_id,
|
||||
QClasses.tenant_id == tenant_id
|
||||
).first()
|
||||
|
||||
if fa_extension:
|
||||
db.delete(fa_extension)
|
||||
|
||||
# Now delete the base class
|
||||
db.delete(class_obj)
|
||||
db.commit()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def create_fa_class(
|
||||
db: Session, class_data: ClassCreateDTOFA, tenant_id: int, company_id: int
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Create a fixed asset class (both a76.classes and a24.fa_classes)
|
||||
Returns a dict with both records combined
|
||||
"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"create_fa_class called with data: {class_data.model_dump()}")
|
||||
|
||||
from api.v1.modules.a24.fa.fa_classes.models import QClasses
|
||||
|
||||
# Extract base class fields
|
||||
base_fields = {
|
||||
"client_id", "class_code", "description_es", "description_en",
|
||||
"material_key", "unit_of_measure", "fraction", "us_fraction",
|
||||
"sub_key", "physical_review", "iva_exempt_fraction"
|
||||
}
|
||||
base_data = {k: v for k, v in class_data.model_dump().items() if k in base_fields}
|
||||
|
||||
# Extract FA-specific fields
|
||||
fa_fields = {
|
||||
"import_tariff_code", "import_tariff_type", "export_tariff_code",
|
||||
"export_tariff_type", "depreciation_rate", "fda_code", "eccn_code",
|
||||
"class_enabled"
|
||||
}
|
||||
fa_data = {k: v for k, v in class_data.model_dump().items() if k in fa_fields}
|
||||
|
||||
try:
|
||||
# 1. Create base class
|
||||
base_dto = ClassCreateDTO(**base_data)
|
||||
base_class = ClassService.create(db, base_dto, tenant_id, company_id)
|
||||
|
||||
# 2. Create FA extension
|
||||
fa_obj = QClasses(**fa_data)
|
||||
fa_obj.class_id = base_class.id
|
||||
fa_obj.tenant_id = tenant_id
|
||||
fa_obj.company_id = company_id
|
||||
|
||||
db.add(fa_obj)
|
||||
db.commit()
|
||||
db.refresh(fa_obj)
|
||||
|
||||
# 3. Combine response - build dict manually to avoid SQLAlchemy internals
|
||||
combined_response = {
|
||||
# Base class fields
|
||||
"id": base_class.id,
|
||||
"tenant_id": base_class.tenant_id,
|
||||
"company_id": base_class.company_id,
|
||||
"client_id": base_class.client_id,
|
||||
"class_code": base_class.class_code,
|
||||
"description_es": base_class.description_es,
|
||||
"description_en": base_class.description_en,
|
||||
"material_key": base_class.material_key,
|
||||
"unit_of_measure": base_class.unit_of_measure,
|
||||
"fraction": base_class.fraction,
|
||||
"us_fraction": base_class.us_fraction,
|
||||
"sub_key": base_class.sub_key,
|
||||
"physical_review": base_class.physical_review,
|
||||
"iva_exempt_fraction": base_class.iva_exempt_fraction,
|
||||
"created_at": base_class.created_at,
|
||||
"updated_at": base_class.updated_at,
|
||||
# FA extension fields
|
||||
"fa_id": fa_obj.id,
|
||||
"import_tariff_code": fa_obj.import_tariff_code,
|
||||
"import_tariff_type": fa_obj.import_tariff_type,
|
||||
"export_tariff_code": fa_obj.export_tariff_code,
|
||||
"export_tariff_type": fa_obj.export_tariff_type,
|
||||
"depreciation_rate": fa_obj.depreciation_rate,
|
||||
"fda_code": fa_obj.fda_code,
|
||||
"eccn_code": fa_obj.eccn_code,
|
||||
"class_enabled": fa_obj.class_enabled,
|
||||
}
|
||||
|
||||
return combined_response
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
# If FA creation fails, rollback base class too
|
||||
if 'base_class' in locals():
|
||||
try:
|
||||
db.delete(base_class)
|
||||
db.commit()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Extract and improve error message
|
||||
error_msg = str(e)
|
||||
if "already exists" in error_msg.lower() or "duplicad" in error_msg.lower():
|
||||
# Extract code from error if possible
|
||||
code = class_data.class_code
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"El código '{code}' ya está en uso. Por favor ingrese un código diferente."
|
||||
)
|
||||
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"Error al crear clase de activo fijo: {error_msg}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def seed_initial_data(
|
||||
db: Session, tenant_id: int, company_id: int, client_id: int
|
||||
) -> int:
|
||||
"""
|
||||
Seed initial fixed asset class data
|
||||
Returns: number of records created
|
||||
"""
|
||||
from .seed import seed
|
||||
|
||||
created_count = 0
|
||||
for record in seed:
|
||||
(
|
||||
class_code,
|
||||
description_es,
|
||||
description_en,
|
||||
material_key,
|
||||
unit_of_measure,
|
||||
fraction,
|
||||
us_fraction,
|
||||
bom,
|
||||
) = record
|
||||
|
||||
# Check if already exists
|
||||
existing = (
|
||||
db.query(Class)
|
||||
.filter(
|
||||
Class.tenant_id == tenant_id,
|
||||
Class.company_id == company_id,
|
||||
Class.client_id == client_id,
|
||||
Class.class_code == class_code,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if not existing:
|
||||
class_obj = Class(
|
||||
tenant_id=tenant_id,
|
||||
company_id=company_id,
|
||||
client_id=client_id,
|
||||
class_code=class_code,
|
||||
description_es=description_es,
|
||||
description_en=description_en,
|
||||
material_key=material_key if material_key else None,
|
||||
unit_of_measure=unit_of_measure if unit_of_measure else None,
|
||||
fraction=fraction if fraction else None,
|
||||
us_fraction=us_fraction if us_fraction else None,
|
||||
)
|
||||
db.add(class_obj)
|
||||
created_count += 1
|
||||
|
||||
if created_count > 0:
|
||||
db.commit()
|
||||
|
||||
return created_count
|
||||
|
||||
def __init__(self, db: Session):
|
||||
self.db = db
|
||||
|
||||
|
||||
Reference in New Issue
Block a user