fix/AS-prosec-revertir-boolean (#482)

prosec revertir boolean y uso de catalogo

Reviewed-on: ADUANASOFT/anexo76#482
Co-authored-by: hreyes <hreyes@aduanasoft.com.mx>
Co-committed-by: hreyes <hreyes@aduanasoft.com.mx>
This commit is contained in:
2026-06-02 21:06:23 +00:00
committed by acazares
parent 96b3beed4f
commit 26c5a97c04
7 changed files with 147 additions and 78 deletions

View File

@@ -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")

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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 @@
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="grid gap-2">
<Label for="prosec">Sector PROSEC (ID)</Label>
<Input id="prosec" type="number" bind:value={formData.prosec} />
<div class="flex items-center gap-3 rounded-lg border p-4">
<Switch
id="prosec"
bind:checked={formData.prosec}
onCheckedChange={(checked) => {
if (!checked) {
formData.prosec_authorization = '';
prosecDisplay = '';
}
}}
/>
<Label for="prosec">PROSEC</Label>
</div>
<div class="grid gap-2">
<Label for="prosec_auth">Autorización PROSEC</Label>
<Input id="prosec_auth" bind:value={formData.prosec_authorization} />
<div class="flex gap-2">
<Input
id="prosec_auth"
value={prosecDisplay}
placeholder="Seleccionar sector..."
readonly
disabled={!formData.prosec}
/>
<Button
type="button"
variant="outline"
size="icon"
onclick={() => (sectorModalOpen = true)}
disabled={!formData.prosec}
title="Buscar Sector"
>
<Factory class="h-4 w-4" />
</Button>
</div>
</div>
</div>
</Tabs.Content>
@@ -290,4 +343,10 @@
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>
</Dialog.Root>
<SectorSelectorDialog
bind:open={sectorModalOpen}
onSelect={handleSectorSelect}
onClear={() => { formData.prosec_authorization = ''; prosecDisplay = ''; }}
/>

View File

@@ -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 @@
</div>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="flex items-center gap-3 rounded-lg border p-4">
<Switch
id="prosec"
bind:checked={formData.prosec}
onCheckedChange={(checked) => {
if (!checked) {
formData.prosec_authorization = '';
prosecDisplay = '';
}
}}
/>
<Label for="prosec">PROSEC</Label>
</div>
<div class="grid gap-2">
<Label for="prosec">Sector PROSEC</Label>
<Label for="prosec_auth">Autorización PROSEC</Label>
<div class="flex gap-2">
<Input
id="prosec"
id="prosec_auth"
value={prosecDisplay}
placeholder="Seleccionar sector..."
readonly
disabled={!formData.prosec}
/>
<Button
type="button"
variant="outline"
size="icon"
onclick={() => (sectorModalOpen = true)}
disabled={loading}
disabled={loading || !formData.prosec}
title="Buscar Sector"
>
<Factory class="h-4 w-4" />
</Button>
</div>
</div>
<div class="grid gap-2">
<Label for="prosec_auth">Autorización PROSEC</Label>
<Input id="prosec_auth" bind:value={formData.prosec_authorization} />
</div>
</div>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="grid gap-2">
@@ -2089,6 +2101,6 @@
<SectorSelectorDialog
bind:open={sectorModalOpen}
onSelect={handleSectorSelect}
onClear={() => { formData.prosec = 0; prosecDisplay = ''; }}
onClear={() => { formData.prosec_authorization = ''; prosecDisplay = ''; }}
/>
{/if}