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:
@@ -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")
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = ''; }}
|
||||
/>
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user