Merge pull request 'fixed/logo_empresa' (#70) from fixed/logo_empresa into development
Reviewed-on: ADUANASOFT/anexo76#70
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)}")
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
15
backend/debug_mapper.py
Normal file
15
backend/debug_mapper.py
Normal file
@@ -0,0 +1,15 @@
|
||||
from api.v1.modules.a76.general_catalogs.company.models import Company
|
||||
from api.v1.modules.a76.general_catalogs.company.service import CompanyService
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
print("Checking Company Mapper keys...")
|
||||
try:
|
||||
keys = Company.__mapper__.c.keys()
|
||||
print(f"Keys found: {keys}")
|
||||
if 'logo' in keys:
|
||||
print("SUCCESS: 'logo' is in keys")
|
||||
else:
|
||||
print("FAILURE: 'logo' is NOT in keys")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
20
backend/inspect_schema.py
Normal file
20
backend/inspect_schema.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from sqlalchemy import create_engine, text
|
||||
|
||||
# Connect to DB (adjust for localhost)
|
||||
DB_URL = "postgresql://postgres:postgres@localhost:5432/anexo76_core"
|
||||
engine = create_engine(DB_URL)
|
||||
|
||||
sql = """
|
||||
SELECT column_name, data_type, character_maximum_length
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'company' AND table_schema = 'a76'
|
||||
AND column_name IN ('has_express_line', 'is_service_company');
|
||||
"""
|
||||
|
||||
try:
|
||||
with engine.connect() as conn:
|
||||
result = conn.execute(text(sql))
|
||||
for row in result:
|
||||
print(f"Column: {row[0]}, Type: {row[1]}, Length: {row[2]}")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
32
backend/verify_logo_presence.py
Normal file
32
backend/verify_logo_presence.py
Normal file
@@ -0,0 +1,32 @@
|
||||
from api.v1.modules.a76.general_catalogs.company.models import Company
|
||||
from api.v1.modules.a76.general_catalogs.company.service import CompanyService
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
import os
|
||||
|
||||
# Connect to DB (adjust for localhost)
|
||||
DB_URL = "postgresql://postgres:postgres@localhost:5432/anexo76_core"
|
||||
engine = create_engine(DB_URL)
|
||||
SessionLocal = sessionmaker(bind=engine)
|
||||
session = SessionLocal()
|
||||
|
||||
try:
|
||||
print("Querying first company...")
|
||||
company = session.query(Company).first()
|
||||
if company:
|
||||
print(f"Company ID: {company.id}")
|
||||
print(f"Direct Logo Access: '{company.logo}'")
|
||||
|
||||
service = CompanyService(session)
|
||||
flattened = service.flatten_company_dto(company)
|
||||
|
||||
print(f"Flattened Logo: '{flattened.get('logo')}'")
|
||||
|
||||
has_logo = 'logo' in flattened
|
||||
print(f"Is 'logo' key in dict?: {has_logo}")
|
||||
else:
|
||||
print("No companies found in DB.")
|
||||
except Exception as e:
|
||||
print(f"Error: {e}")
|
||||
finally:
|
||||
session.close()
|
||||
@@ -295,7 +295,7 @@ export interface UnitOfMeasureListResponse {
|
||||
|
||||
export async function getUnitsOfMeasure(
|
||||
page: number = 1,
|
||||
pageSize: number = 50,
|
||||
pageSize: number = 1000,
|
||||
companyId: number,
|
||||
filters: Record<string, any> = {}
|
||||
): Promise<ApiResponse<UnitOfMeasureListResponse>> {
|
||||
|
||||
@@ -16,15 +16,27 @@
|
||||
} = $props();
|
||||
|
||||
let items = $state<UnitOfMeasure[]>([]);
|
||||
let allItems = $state<UnitOfMeasure[]>([]); // Store full dataset
|
||||
let loading = $state(false);
|
||||
let searchTerm = $state("");
|
||||
let page = $state(1);
|
||||
let totalPages = $state(1);
|
||||
let searchTimeout: NodeJS.Timeout;
|
||||
|
||||
// Derived state for filtering
|
||||
$effect(() => {
|
||||
if (!searchTerm) {
|
||||
items = allItems;
|
||||
} else {
|
||||
const lowerTerm = searchTerm.toLowerCase();
|
||||
items = allItems.filter(item =>
|
||||
item.code.toLowerCase().includes(lowerTerm) ||
|
||||
(item.description && item.description.toLowerCase().includes(lowerTerm)) ||
|
||||
(item.description_en && item.description_en.toLowerCase().includes(lowerTerm))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// Cargar datos al abrir
|
||||
$effect(() => {
|
||||
if (open && companyStore.activeCompany) {
|
||||
if (open && companyStore.activeCompany && allItems.length === 0) {
|
||||
loadData();
|
||||
}
|
||||
});
|
||||
@@ -34,17 +46,12 @@
|
||||
|
||||
loading = true;
|
||||
try {
|
||||
const filters = searchTerm ? { code: searchTerm } : {};
|
||||
// Nota: Si tu backend soporta búsqueda por descripción, úsalo aquí.
|
||||
// Por ahora asumo búsqueda por 'code' o 'description' según tu filtro backend.
|
||||
|
||||
const response = await getUnitsOfMeasure(page, 10, companyStore.activeCompany.id, {
|
||||
q: searchTerm // Asumiendo que tu backend tiene un filtro genérico 'q' o usa 'code'/'description'
|
||||
});
|
||||
// Fetch everything once using the high limit
|
||||
const response = await getUnitsOfMeasure(1, 1000, companyStore.activeCompany.id, {});
|
||||
|
||||
if (response.data) {
|
||||
items = response.data.items;
|
||||
totalPages = response.data.pages;
|
||||
allItems = response.data.items;
|
||||
items = allItems; // Initialize view
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error cargando unidades:", error);
|
||||
@@ -56,32 +63,13 @@
|
||||
function handleSearch(e: Event) {
|
||||
const value = (e.target as HTMLInputElement).value;
|
||||
searchTerm = value;
|
||||
page = 1;
|
||||
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
loadData();
|
||||
}, 500);
|
||||
// No network call needed, $effect handles filtering
|
||||
}
|
||||
|
||||
function handleSelect(item: UnitOfMeasure) {
|
||||
onSelect(item);
|
||||
open = false;
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
if (page < totalPages) {
|
||||
page++;
|
||||
loadData();
|
||||
}
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
if (page > 1) {
|
||||
page--;
|
||||
loadData();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<Dialog.Root bind:open>
|
||||
@@ -104,7 +92,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border h-[300px] overflow-auto relative">
|
||||
<div class="rounded-md border h-[400px] overflow-auto relative">
|
||||
{#if loading}
|
||||
<div class="absolute inset-0 bg-background/50 flex items-center justify-center z-10">
|
||||
<Loader2 class="h-6 w-6 animate-spin text-primary" />
|
||||
@@ -146,27 +134,9 @@
|
||||
</Table.Body>
|
||||
</Table.Root>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-muted-foreground">Página {page} de {totalPages}</span>
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={page === 1 || loading}
|
||||
onclick={prevPage}
|
||||
>
|
||||
Anterior
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={page >= totalPages || loading}
|
||||
onclick={nextPage}
|
||||
>
|
||||
Siguiente
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-muted-foreground text-center">
|
||||
Mostrando {items.length} registros
|
||||
</div>
|
||||
</div>
|
||||
</Dialog.Content>
|
||||
|
||||
@@ -10,10 +10,10 @@
|
||||
|
||||
const sidebar = useSidebar();
|
||||
|
||||
// Derivar la URL del logo
|
||||
// Derivar la URL del logo usando el endpoint específico
|
||||
let activeCompanyLogoUrl = $derived(
|
||||
companyStore.activeCompany?.logo
|
||||
? getBackendAssetUrl(companyStore.activeCompany.logo)
|
||||
? getBackendAssetUrl(`v1/a76/company/${companyStore.activeCompany.id}/logo/image?t=${new Date().getTime()}`)
|
||||
: null
|
||||
);
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
<div class="flex size-6 items-center justify-center rounded-md border overflow-hidden">
|
||||
{#if company.logo}
|
||||
<img
|
||||
src={getBackendAssetUrl(company.logo)}
|
||||
src={getBackendAssetUrl(`v1/a76/company/${company.id}/logo/image`)}
|
||||
alt={company.name}
|
||||
class="size-full rounded object-cover"
|
||||
/>
|
||||
|
||||
@@ -144,6 +144,30 @@ class CompanyStore {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Actualiza los datos de une empresa en el store localmente
|
||||
* Útil para reflejar cambios inmediatos (ej: cambio de logo) sin recargar
|
||||
*/
|
||||
updateCompany(id: number, data: Partial<Company>) {
|
||||
// 1. Actualizar en la lista
|
||||
const index = this._companies.findIndex(c => c.id === id);
|
||||
if (index !== -1) {
|
||||
this._companies[index] = { ...this._companies[index], ...data };
|
||||
|
||||
// 2. Si es la activa, actualizar también
|
||||
if (this._activeCompany?.id === id) {
|
||||
this._activeCompany = { ...this._activeCompany, ...data };
|
||||
// Actualizar persistencia si es necesario
|
||||
if (typeof window !== 'undefined') {
|
||||
// Disparar evento para notificar cambios a componentes que no usan el store reactivo directo (si los hay)
|
||||
window.dispatchEvent(new CustomEvent('companyChanged', {
|
||||
detail: { companyId: id }
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restaura la compañía activa desde localStorage
|
||||
*/
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
uploadCompanyLogo,
|
||||
type Company
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/company';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { getBackendAssetUrl } from '$lib/utils';
|
||||
import { ArrowLeft, LoaderCircle, Save, Upload, X, Building2, FileText, User, Settings } from 'lucide-svelte';
|
||||
|
||||
@@ -25,15 +26,18 @@
|
||||
let loading = $state(false);
|
||||
let uploading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let activeTab = $state('general');
|
||||
|
||||
let logoFile = $state<File | null>(null);
|
||||
let logoPreview = $state<string | null>(null);
|
||||
let currentLogo = $state<string | null>(null);
|
||||
let uploadingLogo = $state(false);
|
||||
let activeTab = $state('general');
|
||||
|
||||
|
||||
// URL completa del logo derivada
|
||||
let currentLogoUrl = $derived(
|
||||
logoPreview || getBackendAssetUrl(currentLogo) || ''
|
||||
logoPreview
|
||||
? logoPreview
|
||||
: (currentLogo ? getBackendAssetUrl(`v1/a76/company/${id}/logo/image?t=${new Date().getTime()}`) : '')
|
||||
);
|
||||
|
||||
// 2. Estado Inicial (Reset)
|
||||
@@ -104,7 +108,15 @@
|
||||
is_service_company: item.is_service_company || false,
|
||||
order_format_type: item.order_format_type || '',
|
||||
ctpat_svi: item.ctpat_svi || '',
|
||||
trusted_exporter_number: item.trusted_exporter_number || ''
|
||||
trusted_exporter_number: item.trusted_exporter_number || '',
|
||||
logo: item.logo || '',
|
||||
previous_code: item.previous_code || 0,
|
||||
client_name: item.client_name || '',
|
||||
subassembly_mode: item.subassembly_mode || '',
|
||||
broker_company: item.broker_company || '',
|
||||
inter_db_name: item.inter_db_name || '',
|
||||
prevalidator_key: item.prevalidator_key || '',
|
||||
seventh_amendment: item.seventh_amendment || false
|
||||
};
|
||||
// Guardar la URL del logo actual si existe
|
||||
if (item.logo) {
|
||||
@@ -118,6 +130,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const clean = (value: any) => (typeof value === 'string' ? value.trim() || null : value);
|
||||
|
||||
function handleLogoChange(event: Event) {
|
||||
const target = event.target as HTMLInputElement;
|
||||
const file = target.files?.[0];
|
||||
@@ -170,6 +185,9 @@
|
||||
currentLogo = response.data.logo_path;
|
||||
logoFile = null;
|
||||
logoPreview = null;
|
||||
|
||||
// Actualizar el store reactivamente
|
||||
companyStore.updateCompany(companyId, { logo: response.data.logo_path });
|
||||
}
|
||||
} catch (e: any) {
|
||||
error = `Error al subir el logo: ${e.message}`;
|
||||
@@ -178,33 +196,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
const clean = (value: any) => (typeof value === 'string' ? value.trim() || null : value);
|
||||
|
||||
async function handleFileSelect(e: Event) {
|
||||
const input = e.target as HTMLInputElement;
|
||||
if (!input.files || input.files.length === 0) return;
|
||||
|
||||
const file = input.files[0];
|
||||
if (!isEdit) {
|
||||
alert("Primero debes guardar la empresa antes de subir un logo.");
|
||||
return;
|
||||
}
|
||||
|
||||
uploading = true;
|
||||
try {
|
||||
const res = await uploadCompanyLogo(Number(id), file);
|
||||
if (res.data) {
|
||||
formData.logo = res.data.path;
|
||||
} else if (res.error) {
|
||||
alert("Error al subir imagen: " + res.error);
|
||||
}
|
||||
} catch (err) {
|
||||
alert("Error al intentar subir la imagen");
|
||||
} finally {
|
||||
uploading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
error = null;
|
||||
loading = true;
|
||||
@@ -222,6 +213,7 @@
|
||||
main_activity: clean(formData.main_activity),
|
||||
program: clean(formData.program),
|
||||
program_number: clean(formData.program_number),
|
||||
prosec: Number(formData.prosec) || 0,
|
||||
prosec_authorization: clean(formData.prosec_authorization),
|
||||
responsible_name: clean(formData.responsible_name),
|
||||
responsible_last_name: clean(formData.responsible_last_name),
|
||||
@@ -239,7 +231,10 @@
|
||||
broker_company: clean(formData.broker_company),
|
||||
inter_db_name: clean(formData.inter_db_name),
|
||||
prevalidator_key: clean(formData.prevalidator_key),
|
||||
seventh_amendment: formData.seventh_amendment
|
||||
seventh_amendment: formData.seventh_amendment,
|
||||
// Ensure optional booleans are passed correctly or default to false/null if needed
|
||||
has_express_line: formData.has_express_line,
|
||||
is_service_company: formData.is_service_company
|
||||
};
|
||||
|
||||
const response = isEdit
|
||||
@@ -285,6 +280,8 @@
|
||||
<Tabs.Root bind:value={activeTab} class="w-full">
|
||||
<div class="min-h-[400px]">
|
||||
<Tabs.Content value="general" class="space-y-4 pt-4">
|
||||
|
||||
|
||||
<!-- Logo Upload Section -->
|
||||
<div class="grid gap-4 p-4 border rounded-lg bg-muted/30">
|
||||
<Label>Logo de la Empresa</Label>
|
||||
@@ -344,36 +341,9 @@
|
||||
<Label for="main_activity">Actividad Principal</Label>
|
||||
<Input id="main_activity" bind:value={formData.main_activity} />
|
||||
</div>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<div class="grid gap-2">
|
||||
<Label for="logo">Ruta del Logo</Label>
|
||||
<div class="flex items-center gap-2">
|
||||
<Input id="logo" bind:value={formData.logo} placeholder="/path/to/logo.png" />
|
||||
{#if isEdit}
|
||||
<div class="relative">
|
||||
<Button variant="outline" size="icon" disabled={uploading}>
|
||||
{#if uploading}
|
||||
<LoaderCircle class="h-4 w-4 animate-spin" />
|
||||
{:else}
|
||||
<Upload class="h-4 w-4" />
|
||||
{/if}
|
||||
</Button>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
class="absolute inset-0 opacity-0 cursor-pointer"
|
||||
onchange={handleFileSelect}
|
||||
disabled={uploading}
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<p class="text-[0.8rem] text-muted-foreground">Sube una imagen para obtener su ruta local.</p>
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="client_name">Nombre Cliente (Maquila)</Label>
|
||||
<Input id="client_name" bind:value={formData.client_name} />
|
||||
</div>
|
||||
<div class="grid gap-2">
|
||||
<Label for="client_name">Nombre Cliente (Maquila)</Label>
|
||||
<Input id="client_name" bind:value={formData.client_name} />
|
||||
</div>
|
||||
</Tabs.Content>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user