diff --git a/backend/alembic/versions/506969b0a256_company_prosec_boolean_to_integer.py b/backend/alembic/versions/506969b0a256_company_prosec_boolean_to_integer.py deleted file mode 100644 index 61760536..00000000 --- a/backend/alembic/versions/506969b0a256_company_prosec_boolean_to_integer.py +++ /dev/null @@ -1,37 +0,0 @@ -"""company prosec boolean to integer - -Revision ID: 506969b0a256 -Revises: a7b8c9d0e1f2 -Create Date: 2026-06-02 00:00:00.000000 - -""" -from typing import Sequence, Union - -from alembic import op - -revision: str = "506969b0a256" -down_revision: Union[str, None] = "a7b8c9d0e1f2" -branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = None - - -def upgrade() -> None: - # Convierte prosec de boolean a integer para almacenar el ID del sector PROSEC. - # El default 'false' debe eliminarse antes de cambiar el tipo. - op.execute("ALTER TABLE a76.company ALTER COLUMN prosec DROP DEFAULT") - op.execute("ALTER TABLE a76.company ALTER COLUMN prosec DROP NOT NULL") - op.execute( - "ALTER TABLE a76.company ALTER COLUMN prosec TYPE integer USING prosec::int" - ) - - -def downgrade() -> None: - op.execute( - """ - ALTER TABLE a76.company - ALTER COLUMN prosec TYPE boolean - USING CASE WHEN prosec IS NULL OR prosec = 0 THEN false ELSE true END - """ - ) - op.execute("ALTER TABLE a76.company ALTER COLUMN prosec SET DEFAULT false") - op.execute("ALTER TABLE a76.company ALTER COLUMN prosec SET NOT NULL") diff --git a/backend/alembic/versions/b3c4d5e6f7a8_split_rfc_tax_id_clients_and_providers.py b/backend/alembic/versions/b3c4d5e6f7a8_split_rfc_tax_id_clients_and_providers.py index 8d63e5ef..22044ece 100644 --- a/backend/alembic/versions/b3c4d5e6f7a8_split_rfc_tax_id_clients_and_providers.py +++ b/backend/alembic/versions/b3c4d5e6f7a8_split_rfc_tax_id_clients_and_providers.py @@ -9,7 +9,7 @@ La migración mueve el valor de los extranjeros (type_nat_foreign='E') de `rfc` `tax_id` y deja `rfc` en NULL para esos registros. Los nacionales no se tocan. Revision ID: b3c4d5e6f7a8 -Revises: 506969b0a256 +Revises: a7b8c9d0e1f2 Create Date: 2026-06-02 10:00:00.000000 """ @@ -20,7 +20,7 @@ import sqlalchemy as sa from alembic import op revision: str = "b3c4d5e6f7a8" -down_revision: Union[str, None] = "506969b0a256" +down_revision: Union[str, None] = "a7b8c9d0e1f2" branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None diff --git a/backend/api/v1/modules/a76/general_catalogs/company/dto.py b/backend/api/v1/modules/a76/general_catalogs/company/dto.py index 7ff5f030..16485abf 100644 --- a/backend/api/v1/modules/a76/general_catalogs/company/dto.py +++ b/backend/api/v1/modules/a76/general_catalogs/company/dto.py @@ -26,9 +26,9 @@ class CompanyCreateDTO(BaseModel): program_number: Optional[str] = Field( None, max_length=40, description="Program number" ) - prosec: Optional[int] = Field(None, description="PROSEC") + prosec: Optional[bool] = Field(False, description="Empresa con programa PROSEC") prosec_authorization: Optional[str] = Field( - None, max_length=20, description="PROSEC authorization" + None, max_length=20, description="Clave del sector PROSEC" ) # Identifiers @@ -250,6 +250,22 @@ class CompanyCreateDTO(BaseModel): raise ValueError("Date must be in DD/MM/YYYY format") from exc raise ValueError("Date must be in DD/MM/YYYY format") + @field_validator("prosec", mode="before") + @classmethod + def coerce_prosec_to_bool(cls, value): + """Coacciona prosec a booleano. Tolera valores heredados cuando la columna + era entero (ID de sector): null/0/vacío → False, cualquier otro → True. + Misma regla que el downgrade de la migración previa.""" + if value in (None, ""): + return False + if isinstance(value, bool): + return value + if isinstance(value, (int, float)): + return value != 0 + if isinstance(value, str): + return value.strip().lower() not in {"0", "false", "no", "off", ""} + return bool(value) + @field_validator("annex30_modality", mode="before") @classmethod def validate_annex30_modality(cls, value): @@ -278,7 +294,7 @@ class CompanyResponseDTO(BaseModel): # Program information program: Optional[str] = None program_number: Optional[str] = None - prosec: Optional[int] = None + prosec: bool = False prosec_authorization: Optional[str] = None # Identifiers @@ -317,6 +333,21 @@ class CompanyResponseDTO(BaseModel): created_at: datetime updated_at: Optional[datetime] = None + @field_validator("prosec", mode="before") + @classmethod + def coerce_prosec_to_bool(cls, value): + """Tolera datos heredados cuando la columna prosec era entero (ID de sector): + null/0/vacío → False, cualquier otro → True.""" + if value in (None, ""): + return False + if isinstance(value, bool): + return value + if isinstance(value, (int, float)): + return value != 0 + if isinstance(value, str): + return value.strip().lower() not in {"0", "false", "no", "off", ""} + return bool(value) + # --- Flattened Fields for Response --- # Sectores sector1: Optional[str] = None diff --git a/backend/api/v1/modules/a76/general_catalogs/company/models.py b/backend/api/v1/modules/a76/general_catalogs/company/models.py index 68ec0597..7edb419b 100644 --- a/backend/api/v1/modules/a76/general_catalogs/company/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/company/models.py @@ -39,7 +39,11 @@ class Company(Base, TimestampMixin): # Programa program: Mapped[Optional[str]] = mapped_column(String(7)) program_number: Mapped[Optional[str]] = mapped_column(String(40)) - prosec: Mapped[Optional[int]] = mapped_column(Integer, nullable=True, default=None) + # prosec: indica si la empresa cuenta con programa PROSEC (sí/no) + prosec: Mapped[bool] = mapped_column( + Boolean, nullable=False, default=False, server_default="false" + ) + # prosec_authorization: clave del sector PROSEC (catálogo de sectores) prosec_authorization: Mapped[Optional[str]] = mapped_column(String(20)) # Sectores diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/company.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/company.ts index 4367ac53..bffc03c4 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/company.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/company.ts @@ -97,7 +97,7 @@ export interface Company { main_activity: string | null; program: string | null; program_number: string | null; - prosec: number | null; + prosec: boolean; prosec_authorization: string | null; manufacturer_id: string | null; broker_company: string | null; @@ -264,7 +264,7 @@ export interface CompanyCreate { main_activity?: string | null; program?: string | null; program_number?: string | null; - prosec?: number | null; + prosec?: boolean; prosec_authorization?: string | null; manufacturer_id?: string | null; broker_company?: string | null; diff --git a/frontend/src/lib/components/dashboard/general_catalogs/company/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/general_catalogs/company/create-edit-dialog.svelte index a347593d..14ac1469 100644 --- a/frontend/src/lib/components/dashboard/general_catalogs/company/create-edit-dialog.svelte +++ b/frontend/src/lib/components/dashboard/general_catalogs/company/create-edit-dialog.svelte @@ -4,9 +4,31 @@ import { Input } from "$lib/components/ui/input"; import { Label } from "$lib/components/ui/label"; import * as Tabs from "$lib/components/ui/tabs"; - import { Switch } from "$lib/components/ui/switch"; + import { Switch } from "$lib/components/ui/switch"; import { createCompany, updateCompany, type Company } from "$lib/api/dashboard/a76/general_catalogs/company"; + import { sectorsApi } from "$lib/api/dashboard/reference_data/sectors"; import { companyStore } from "$lib/stores/company.svelte"; + import SectorSelectorDialog from "$lib/components/dashboard/shared/modals/sector-selector-dialog.svelte"; + import { Factory } from "lucide-svelte"; + + // Selector del sector PROSEC (se almacena por clave en prosec_authorization) + let sectorModalOpen = $state(false); + let prosecDisplay = $state(''); + + function handleSectorSelect(sector: { id: number; key: string; description: string }) { + formData.prosec_authorization = sector.key; + prosecDisplay = `${sector.key} - ${sector.description}`; + } + + // Resuelve la descripción del sector a partir de su clave para mostrar "KEY - Descripción" + async function resolveProsecDisplay(key: string | null | undefined) { + const companyId = companyStore.activeCompany?.id; + if (!key || !companyId) return; + const sector = (await sectorsApi.getByKey(key, companyId)).data?.items?.find( + (s) => s.key === key + ); + if (sector) prosecDisplay = `${sector.key} - ${sector.description}`; + } let { open = $bindable(false), @@ -32,7 +54,7 @@ // Programas program: '', program_number: '', - prosec: 0, + prosec: false, prosec_authorization: '', // Responsable @@ -66,7 +88,7 @@ program: item.program || '', program_number: item.program_number || '', - prosec: item.prosec || 0, + prosec: Boolean(item.prosec), prosec_authorization: item.prosec_authorization || '', responsible_name: item.responsible_name || '', @@ -82,11 +104,15 @@ ctpat_svi: item.ctpat_svi || '', trusted_exporter_number: item.trusted_exporter_number || '' }; + // Mostrar el sector PROSEC almacenado por clave; enriquece con descripción async + prosecDisplay = item.prosec_authorization || ''; + void resolveProsecDisplay(item.prosec_authorization); } else { - // Reset manual si es nuevo + prosecDisplay = ''; + // Reset manual si es nuevo formData = { name: '', rfc: '', curp: '', main_activity: '', - program: '', program_number: '', prosec: 0, prosec_authorization: '', + program: '', program_number: '', prosec: false, prosec_authorization: '', responsible_name: '', responsible_last_name: '', responsible_mother_last_name: '', responsible_rfc: '', position: '', manufacturer_id: '', is_service_company: false, order_format_type: '', ctpat_svi: '', trusted_exporter_number: '' @@ -113,7 +139,7 @@ program: formData.program.trim() || null, program_number: formData.program_number.trim() || null, - prosec: formData.prosec || null, + prosec: Boolean(formData.prosec), prosec_authorization: formData.prosec_authorization.trim() || null, // Concatenar nombre completo del responsable si se desea guardar en 'responsible' también @@ -210,13 +236,40 @@
-
- - +
+ { + if (!checked) { + formData.prosec_authorization = ''; + prosecDisplay = ''; + } + }} + /> +
- +
+ + +
@@ -290,4 +343,10 @@ - \ No newline at end of file + + + { formData.prosec_authorization = ''; prosecDisplay = ''; }} +/> \ No newline at end of file diff --git a/frontend/src/routes/dashboard/general_catalogs/company_information/edit/[[id]]/+page.svelte b/frontend/src/routes/dashboard/general_catalogs/company_information/edit/[[id]]/+page.svelte index 07897dd5..b9c1ff32 100644 --- a/frontend/src/routes/dashboard/general_catalogs/company_information/edit/[[id]]/+page.svelte +++ b/frontend/src/routes/dashboard/general_catalogs/company_information/edit/[[id]]/+page.svelte @@ -92,7 +92,7 @@ main_activity: '', program: '', program_number: '', - prosec: 0, + prosec: false, prosec_authorization: '', responsible_name: '', responsible_last_name: '', @@ -267,7 +267,7 @@ main_activity: item.main_activity || '', program: item.program || '', program_number: item.program_number || '', - prosec: item.prosec || 0, + prosec: Boolean(item.prosec), prosec_authorization: item.prosec_authorization || '', responsible_name: item.responsible_name || '', responsible_last_name: item.responsible_last_name || '', @@ -413,17 +413,18 @@ cancel_key_exp: item.cancel_key_exp || 0 }; - // Resolver el sector PROSEC contra el catálogo para mostrar KEY - Descripción - if (item.prosec) { + // Resolver el sector PROSEC (almacenado por clave en prosec_authorization) + // contra el catálogo para mostrar KEY - Descripción + if (item.prosec_authorization) { const companyId = companyStore.activeCompany?.id; - if (companyId) { - const sectorResp = await sectorsApi.get(item.prosec, companyId); - prosecDisplay = sectorResp.data - ? `${sectorResp.data.key} - ${sectorResp.data.description}` - : String(item.prosec); // fallback al ID si el sector no se encontró - } else { - prosecDisplay = String(item.prosec); - } + const sector = companyId + ? (await sectorsApi.getByKey(item.prosec_authorization, companyId)).data?.items?.find( + (s) => s.key === item.prosec_authorization + ) + : undefined; + prosecDisplay = sector + ? `${sector.key} - ${sector.description}` + : item.prosec_authorization; // fallback a la clave si el sector no se encontró } else { prosecDisplay = ''; } @@ -468,7 +469,8 @@ } function handleSectorSelect(sector: { id: number; key: string; description: string }) { - formData.prosec = sector.id; + // El sector PROSEC se almacena por clave en prosec_authorization + formData.prosec_authorization = sector.key; prosecDisplay = `${sector.key} - ${sector.description}`; } @@ -624,7 +626,7 @@ main_activity: clean(formData.main_activity), program: clean(formData.program), program_number: clean(formData.program_number), - prosec: Number(formData.prosec) || 0, + prosec: Boolean(formData.prosec), prosec_authorization: clean(formData.prosec_authorization), responsible_name: clean(formData.responsible_name), responsible_last_name: clean(formData.responsible_last_name), @@ -926,31 +928,41 @@
+
+ { + if (!checked) { + formData.prosec_authorization = ''; + prosecDisplay = ''; + } + }} + /> + +
- +
-
- - -
@@ -2089,6 +2101,6 @@ { formData.prosec = 0; prosecDisplay = ''; }} + onClear={() => { formData.prosec_authorization = ''; prosecDisplay = ''; }} /> {/if}