Merge pull request 'fixed/logo_empresa' (#70) from fixed/logo_empresa into development

Reviewed-on: ADUANASOFT/anexo76#70
This commit is contained in:
2026-01-22 23:30:06 +00:00
13 changed files with 404 additions and 355 deletions

View File

@@ -61,7 +61,7 @@ class Company(Base, TimestampMixin):
# Configuración básica
logo: Mapped[Optional[str]] = mapped_column(String(255))
has_express_line: Mapped[Optional[bool]] = mapped_column(Boolean, default=False)
has_express_line: Mapped[Optional[str]] = mapped_column(String(2), default="N")
order_format_type: Mapped[Optional[str]] = mapped_column(String(19))
is_service_company: Mapped[Optional[bool]] = mapped_column(Boolean, default=False)
client_name: Mapped[Optional[str]] = mapped_column(String(300))
@@ -78,6 +78,7 @@ class Company(Base, TimestampMixin):
parts_replacement: Mapped[Optional[int]] = mapped_column(SmallInteger)
activate_facmexame: Mapped[Optional[int]] = mapped_column(SmallInteger)
part_reference: Mapped[Optional[int]] = mapped_column(SmallInteger)
part_reference: Mapped[Optional[int]] = mapped_column(SmallInteger)
international_firm: Mapped[Optional[int]] = mapped_column(SmallInteger)
# Configuraciones simples

View File

@@ -50,7 +50,8 @@ async def create_company(
)
service = CompanyService(db)
return service.create_company_manually(data, tenant_id=tenant_id)
new_company = service.create_company_manually(data, tenant_id=tenant_id)
return CompanyResponseDTO.model_validate(service.flatten_company_dto(new_company))
@router.get(
@@ -94,7 +95,10 @@ async def list_companies(
total_pages = (total + page_size - 1) // page_size
return {
"items": [CompanyResponseDTO.model_validate(item) for item in items],
"items": [
CompanyResponseDTO.model_validate(service.flatten_company_dto(item))
for item in items
],
"total": total,
"page": page,
"page_size": page_size,
@@ -122,135 +126,12 @@ async def get_my_companies(
service = CompanyService(db)
companies = service.get_companies_by_tenant(tenant_id)
return [CompanyResponseDTO.model_validate(company) for company in companies]
@router.get(
"/status/exists",
response_model=dict,
summary="Check if company exists for tenant",
)
async def check_company_exists(
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Check if a company exists for the current tenant"""
tenant_id = current_user.get("tenant_id")
if not tenant_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Tenant ID not found in user data",
)
service = CompanyService(db)
exists = service.exists_company(tenant_id)
return {"exists": exists}
@router.get(
"/info/basic/{company_id}",
response_model=dict,
summary="Get basic company info",
)
async def get_basic_info(
company_id: int,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Get basic information about a company"""
tenant_id = current_user.get("tenant_id")
company_id_from_user = current_user.get("company_id")
# Validate access
validate_access_to_resource(
db, tenant_id, company_id_from_user, Company, company_id, "id"
)
company = CompanyService.get_by_id(db, company_id, tenant_id, company_id_from_user)
if not company:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Company not found",
)
return {
"id": company.id,
"name": company.name,
"rfc": company.rfc,
"program": company.program,
}
@router.get(
"/info/responsible/{company_id}",
response_model=dict,
summary="Get responsible person info",
)
async def get_responsible_info(
company_id: int,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Get responsible person information for a company"""
tenant_id = current_user.get("tenant_id")
company_id_from_user = current_user.get("company_id")
# Validate access
validate_access_to_resource(
db, tenant_id, company_id_from_user, Company, company_id, "id"
)
company = CompanyService.get_by_id(db, company_id, tenant_id, company_id_from_user)
if not company:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Company not found",
)
return {
"responsible": company.responsible,
"responsible_name": company.responsible_name,
"responsible_last_name": company.responsible_last_name,
"responsible_mother_last_name": company.responsible_mother_last_name,
"responsible_rfc": company.responsible_rfc,
"position": company.position,
}
@router.get(
"/info/program/{company_id}",
response_model=dict,
summary="Get program information",
)
async def get_program_info(
company_id: int,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Get program information for a company"""
tenant_id = current_user.get("tenant_id")
company_id_from_user = current_user.get("company_id")
# Validate access
validate_access_to_resource(
db, tenant_id, company_id_from_user, Company, company_id, "id"
)
company = CompanyService.get_by_id(db, company_id, tenant_id, company_id_from_user)
if not company:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Company not found",
)
return {
"program": company.program,
"program_number": company.program_number,
"prosec": company.prosec,
"prosec_authorization": company.prosec_authorization,
}
return [
CompanyResponseDTO.model_validate(service.flatten_company_dto(company))
for company in companies
]
# ... existing code ...
@router.get(
"/{company_id}",
@@ -270,6 +151,7 @@ async def get_company(
detail="Tenant ID not found in user data",
)
service = CompanyService(db)
company = CompanyService.get_by_id(db, company_id, tenant_id, 0)
if not company:
raise HTTPException(
@@ -277,7 +159,7 @@ async def get_company(
detail="Company not found",
)
return CompanyResponseDTO.model_validate(company)
return CompanyResponseDTO.model_validate(service.flatten_company_dto(company))
@router.put(
@@ -299,81 +181,28 @@ async def update_company(
detail="Tenant ID not found in user data",
)
updated_company = CompanyService.update(db, company_id, tenant_id, 0, data)
service = CompanyService(db)
updated_company = service.update(db, company_id, tenant_id, 0, data)
if not updated_company:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Company not found",
)
return CompanyResponseDTO.model_validate(updated_company)
return CompanyResponseDTO.model_validate(service.flatten_company_dto(updated_company))
return CompanyResponseDTO.model_validate(updated_company)
@router.post(
"/{company_id}/upload-logo",
response_model=dict,
summary="Upload company logo",
)
async def upload_company_logo(
company_id: int,
file: UploadFile = File(...),
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user),
):
"""Upload logo for a company"""
tenant_id = current_user.get("tenant_id")
if not tenant_id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Tenant ID not found in user data",
)
# 1. Verify company exists
company = CompanyService.get_by_id(db, company_id, tenant_id, 0)
if not company:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Company not found",
)
# 2. Define upload path
# Use a persistent path: 'app_data/logos/{company_id}'
upload_dir = Path(f"app_data/logos/{company_id}")
upload_dir.mkdir(parents=True, exist_ok=True)
# 3. Save file
# Preserve original filename
filename = file.filename or "logo.png"
file_path = upload_dir / filename
try:
# Check if file exists and remove it to avoid accumulation if needed,
# or just overwrite (shutil.copyfileobj overwrites)
with open(file_path, "wb") as buffer:
shutil.copyfileobj(file.file, buffer)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Could not save file: {e}",
)
# 4. Returns the absolute path keys
abs_path = str(file_path.absolute())
return {"path": abs_path}
@router.get(
"/{company_id}/logo/image",
summary="Get company logo image",
)
@router.get(
"/{company_id}/logo/image",
summary="Get company logo image",
)
async def get_company_logo_image(
company_id: int,
db: Session = Depends(get_core_db),
@@ -401,6 +230,13 @@ async def get_company_logo_image(
raise HTTPException(status_code=404, detail="Logo file not found on server")
return FileResponse(file_path)
@router.delete(
"/{company_id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Delete a company",
)
async def delete_company(
company_id: int,
db: Session = Depends(get_core_db),
@@ -497,7 +333,8 @@ async def upload_company_logo(
# Actualizar la empresa con la ruta del logo
update_data = CompanyUpdateDTO(logo=file_path)
updated_company = CompanyService.update(db, company_id, tenant_id, 0, update_data)
service = CompanyService(db)
updated_company = service.update(db, company_id, tenant_id, 0, update_data)
return {
"message": "Logo uploaded successfully",

View File

@@ -97,8 +97,139 @@ class CompanyService:
logger.error(f"Error creating company: {str(e)}")
raise HTTPException(status_code=500, detail="Error creating company")
@staticmethod
# ==================== HELPERS FOR FIELD MAPPING ====================
def _extract_company_fields(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""Extrae campos que pertenecen a la tabla Company principal"""
company_fields = [
"name", "rfc", "curp", "main_activity", "program", "program_number",
"prosec", "prosec_authorization", "sector1", "sector2", "sector3",
"manufacturer_id", "broker_company", "responsible", "responsible_name",
"responsible_last_name", "responsible_mother_last_name", "responsible_rfc",
"position", "logo", "has_express_line", "order_format_type",
"is_service_company", "client_name", "subassembly_mode", "previous_code",
"active_labels", "active_fractions", "activate_caat", "trans_interface",
"american_costs", "scaf_readonly", "parts_replacement", "activate_facmexame",
"part_reference", "international_firm", "ftp_key", "sifra_path",
"version_type", "sql_language", "balance_operation_mode", "inter_db_name"
]
return {k: v for k, v in data.items() if k in company_fields}
def _extract_certification_fields(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""Extrae campos que pertenecen a CompanyCertification"""
cert_fields = [
"is_certified_company", "certified_company_registration",
"certified_company_start_date", "certified_company_end_date",
"annex31_certification_date", "annex31_certification_number",
"annex31_modality", "annex31_company_type", "annex31_renewal_date",
"annex31_final_certification_date", "is_oea_company", "ctpat_svi",
"trusted_exporter_number", "neec_company"
]
return {k: v for k, v in data.items() if k in cert_fields}
def _extract_prevalidator_fields(self, data: Dict[str, Any]) -> Dict[str, Any]:
"""Extrae campos que pertenecen a CompanyPrevalidator"""
# Note: 'prevalidator_key' in DTO maps to 'key' in model
fields = {}
if "prevalidator_key" in data:
fields["key"] = data["prevalidator_key"]
# Add other fields if present in DTO in the future
return fields
def flatten_company_dto(self, company: Company) -> Dict[str, Any]:
"""Flattens Company and its submodels into a single dict for DTO validation"""
# 1. Base Company fields
result = {
k: getattr(company, k)
for k in company.__mapper__.c.keys()
}
# Explicitly ensure logo is present (defensive programming)
if hasattr(company, 'logo'):
result['logo'] = company.logo
# Convert has_express_line from String "S"/"N" to Boolean
if hasattr(company, 'has_express_line'):
val = getattr(company, 'has_express_line', "N")
result['has_express_line'] = (val == "S")
# 2. Certification fields
if company.certification:
cert_fields = [
"is_certified_company", "certified_company_registration",
"certified_company_start_date", "certified_company_end_date",
"annex31_certification_date", "annex31_certification_number",
"annex31_modality", "annex31_company_type", "annex31_renewal_date",
"annex31_final_certification_date", "is_oea_company", "ctpat_svi",
"trusted_exporter_number", "neec_company"
]
for field in cert_fields:
val = getattr(company.certification, field, None)
if val is not None:
result[field] = val
# 3. Prevalidator fields
if company.prevalidator:
if company.prevalidator.key:
result["prevalidator_key"] = company.prevalidator.key
return result
# ==================== CRUD METHODS ====================
def create_company_manually(self, data: CompanyCreateDTO, tenant_id: int) -> Company:
from .submodels.certification import CompanyCertification
from .submodels.prevalidator import CompanyPrevalidator
try:
# 1. Preparar datos
obj_data = data.model_dump(exclude_unset=True)
# Handle boolean flags for Company (Hybrid Approach)
# has_express_line is String(2), is_service_company is Boolean
if "has_express_line" in obj_data and isinstance(obj_data["has_express_line"], bool):
obj_data["has_express_line"] = "S" if obj_data["has_express_line"] else "N"
# 2. Extract fields for each model
company_data = self._extract_company_fields(obj_data)
cert_data = self._extract_certification_fields(obj_data)
preval_data = self._extract_prevalidator_fields(obj_data)
# 3. Create Company
db_company = Company(**company_data, tenant_id=tenant_id)
self.db.add(db_company)
self.db.flush() # Generate ID
# 4. Create Certification if data exists
if cert_data:
cert = CompanyCertification(**cert_data, company_id=db_company.id)
self.db.add(cert)
# 5. Create Prevalidator if data exists
if preval_data:
preval = CompanyPrevalidator(**preval_data, company_id=db_company.id)
self.db.add(preval)
# 6. Commit
self.db.commit()
self.db.refresh(db_company)
return db_company
except IntegrityError as e:
self.db.rollback()
logger.error(f"IntegrityError creating company manually: {str(e)}")
raise HTTPException(
status_code=400,
detail="Error de integridad: Es posible que esta empresa ya exista.",
)
except Exception as e:
self.db.rollback()
logger.error(f"Error creating company manually: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error creando empresa: {str(e)}")
def update(
self, # Changed to instance method to use self helper methods
db: Session,
company_id: int,
tenant_id: int,
@@ -106,23 +237,57 @@ class CompanyService:
company_data: CompanyUpdateDTO,
) -> Optional[Company]:
"""Update a company"""
company = CompanyService.get_by_id(db, company_id, tenant_id, company_id_unused)
from .submodels.certification import CompanyCertification
from .submodels.prevalidator import CompanyPrevalidator
# Use self.db if db is passed as None, or use passed db (legacy support)
session = db if db else self.db
company = self.get_by_id(session, company_id, tenant_id, company_id_unused)
if not company:
return None
# Update only provided fields
update_data = company_data.model_dump(exclude_unset=True)
for field, value in update_data.items():
# 1. Update Company fields
company_fields = self._extract_company_fields(update_data)
# Hybrid Approach: has_express_line is String, is_service_company is Boolean
if "has_express_line" in company_fields and isinstance(company_fields["has_express_line"], bool):
company_fields["has_express_line"] = "S" if company_fields["has_express_line"] else "N"
for field, value in company_fields.items():
setattr(company, field, value)
# 2. Update Certification
cert_fields = self._extract_certification_fields(update_data)
if cert_fields:
if company.certification:
for field, value in cert_fields.items():
setattr(company.certification, field, value)
else:
new_cert = CompanyCertification(**cert_fields, company_id=company.id)
session.add(new_cert)
# 3. Update Prevalidator
preval_fields = self._extract_prevalidator_fields(update_data)
if preval_fields:
if company.prevalidator:
for field, value in preval_fields.items():
setattr(company.prevalidator, field, value)
else:
new_preval = CompanyPrevalidator(**preval_fields, company_id=company.id)
session.add(new_preval)
try:
db.commit()
db.refresh(company)
session.commit()
session.refresh(company)
return company
except Exception as e:
db.rollback()
session.rollback()
logger.error(f"Error updating company {company_id}: {str(e)}")
raise HTTPException(status_code=500, detail="Error updating company")
raise HTTPException(status_code=500, detail=f"Error al actualizar la empresa: {str(e)}")
@staticmethod
def delete(
@@ -134,19 +299,56 @@ class CompanyService:
return False
try:
# Manual cascade delete for submodels to ensure order and avoid FK issues
# (Even though cascade="all, delete-orphan" is set, manual deletion is safer for strict DBs)
# 1. Delete Certification
if company.certification:
db.delete(company.certification)
# 2. Delete Prevalidator
if company.prevalidator:
db.delete(company.prevalidator)
# 3. Delete Electronic Agent
if company.electronic_agent:
db.delete(company.electronic_agent)
# 4. Delete VU
if company.ventanilla_unica:
db.delete(company.ventanilla_unica)
# 5. Delete CFDI
if company.cfdi:
db.delete(company.cfdi)
# 6. Delete Digital Certificates
for cert in company.digital_certificates:
db.delete(cert)
# 7. Delete Addresses
for addr in company.addresses:
db.delete(addr)
# Flush to execute submodel deletions first
db.flush()
db.delete(company)
db.commit()
return True
except IntegrityError as e:
db.rollback()
logger.error(f"IntegrityError deleting company {company_id}: {str(e)}")
# Check if it's a foreign key constraint
if "foreign key constraint" in str(e).lower():
raise HTTPException(
status_code=400,
detail="No se puede eliminar la empresa porque tiene registros relacionados (facturas, conceptos, etc.)"
)
raise HTTPException(status_code=400, detail="Error al eliminar la empresa")
# Try to get detailed error from psycopg2
detail = "No se puede eliminar la empresa porque tiene registros relacionados."
if hasattr(e, 'orig') and hasattr(e.orig, 'diag'):
if e.orig.diag.message_detail:
detail += f" Detalles: {e.orig.diag.message_detail}"
raise HTTPException(
status_code=400,
detail=detail
)
except Exception as e:
db.rollback()
logger.error(f"Error deleting company {company_id}: {str(e)}")
@@ -169,32 +371,4 @@ class CompanyService:
.filter(Company.tenant_id == tenant_id)
.first()
is not None
)
def create_company_manually(self, data: CompanyCreateDTO, tenant_id: int) -> Company:
try:
# 1. Preparar datos
obj_data = data.model_dump(exclude_unset=True)
# 2. Crear objeto SQLAlchemy
db_obj = Company(**obj_data, tenant_id=tenant_id)
# 3. Guardar
self.db.add(db_obj)
self.db.commit()
self.db.refresh(db_obj)
return db_obj
except IntegrityError as e:
self.db.rollback()
logger.error(f"IntegrityError creating company manually: {str(e)}")
raise HTTPException(
status_code=400,
detail="Error de integridad: Es posible que esta empresa ya exista.",
)
except Exception as e:
self.db.rollback()
logger.error(f"Error creating company manually: {str(e)}")
raise HTTPException(status_code=500, detail=f"Error creando empresa: {str(e)}")
)

View File

@@ -2,7 +2,7 @@
Modelo de certificaciones de empresa
"""
from typing import Optional, TYPE_CHECKING
from sqlalchemy import Integer, String, SmallInteger, ForeignKey
from sqlalchemy import Integer, String, SmallInteger, ForeignKey, Boolean
from sqlalchemy.orm import Mapped, mapped_column, relationship
from core.database import Base
from api.v1.common.base_models import TimestampMixin

View File

@@ -17,6 +17,7 @@ ace_router = TenantCRUDRoutes(
id_name="id",
enable_list=True,
enable_filters=True,
max_page_size=10000,
).router
router.include_router(ace_router)
@@ -32,6 +33,7 @@ oma_router = TenantCRUDRoutes(
id_name="id",
enable_list=True,
enable_filters=True,
max_page_size=10000,
).router
router.include_router(oma_router)
@@ -47,6 +49,7 @@ american_router = TenantCRUDRoutes(
id_name="id",
enable_list=True,
enable_filters=True,
max_page_size=10000,
).router
router.include_router(american_router)
@@ -62,6 +65,7 @@ customs_router = TenantCRUDRoutes(
id_name="id",
enable_list=True,
enable_filters=True,
max_page_size=10000,
).router
router.include_router(customs_router)
@@ -77,6 +81,7 @@ general_router = TenantCRUDRoutes(
id_name="id",
enable_list=True,
enable_filters=True,
max_page_size=10000,
).router
router.include_router(general_router)
@@ -93,5 +98,6 @@ main_router = TenantCRUDRoutes(
id_name="id",
enable_list=True,
enable_filters=True,
max_page_size=10000,
).router
router.include_router(main_router)