diff --git a/backend/alembic/versions/f1a2b3c4d5e6_isolate_equivalency_items.py b/backend/alembic/versions/f1a2b3c4d5e6_isolate_equivalency_items.py new file mode 100644 index 00000000..f825826c --- /dev/null +++ b/backend/alembic/versions/f1a2b3c4d5e6_isolate_equivalency_items.py @@ -0,0 +1,75 @@ +"""isolate equivalency items + +Revision ID: f1a2b3c4d5e6 +Revises: e4f5a6b7c8d9 +Create Date: 2026-05-12 17:18:00.000000 + +""" +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +revision: str = "f1a2b3c4d5e6" +down_revision: Union[str, None] = "e4f5a6b7c8d9" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # 1. Add equivalency_id column + op.add_column( + 'equivalency_items', + sa.Column( + 'equivalency_id', + sa.Integer(), + sa.ForeignKey('a76.equivalencies.id', ondelete='CASCADE'), + nullable=True + ), + schema='a76' + ) + + # 2. Drop old unique constraint + # Note: In some DBs we might need to specify the type, but for Postgres 'unique' or 'foreignkey' works. + op.drop_constraint('uq_equivalency_item_fields', 'equivalency_items', schema='a76', type_='unique') + + # 3. Create new unique constraint including equivalency_id + op.create_unique_constraint( + 'uq_equivalency_item_fields', + 'equivalency_items', + ['equivalency_id', 'original_field', 'external_field', 'tenant_id', 'company_id'], + schema='a76' + ) + + # 4. Increase identifier length in equivalencies table + op.alter_column( + 'equivalencies', + 'identifier', + type_=sa.String(50), + existing_type=sa.String(10), + schema='a76' + ) + + +def downgrade() -> None: + # 1. Drop new unique constraint + op.drop_constraint('uq_equivalency_item_fields', 'equivalency_items', schema='a76', type_='unique') + + # 2. Re-create old unique constraint + op.create_unique_constraint( + 'uq_equivalency_item_fields', + 'equivalency_items', + ['original_field', 'external_field', 'tenant_id', 'company_id'], + schema='a76' + ) + + # 4. Revert identifier length in equivalencies table + op.alter_column( + 'equivalencies', + 'identifier', + type_=sa.String(10), + existing_type=sa.String(50), + schema='a76' + ) + # 5. Drop equivalency_id column + op.drop_column('equivalency_items', 'equivalency_id', schema='a76') diff --git a/backend/api/v1/modules/a76/general_catalogs/equivalencies/dto.py b/backend/api/v1/modules/a76/general_catalogs/equivalencies/dto.py index 4494351f..8dd1d318 100644 --- a/backend/api/v1/modules/a76/general_catalogs/equivalencies/dto.py +++ b/backend/api/v1/modules/a76/general_catalogs/equivalencies/dto.py @@ -10,6 +10,7 @@ class EquivalencyItemBase(BaseModel): original_field: str = Field(..., max_length=100, description="Campo original (from)") external_field: str = Field(..., max_length=100, description="Campo externo (to)") conversion_factor: Optional[Decimal] = Field(None, description="Factor de conversión") + equivalency_id: Optional[int] = Field(None, description="ID de la equivalencia padre") class EquivalencyItemCreate(EquivalencyItemBase): @@ -20,6 +21,7 @@ class EquivalencyItemUpdate(BaseModel): original_field: Optional[str] = Field(None, max_length=100) external_field: Optional[str] = Field(None, max_length=100) conversion_factor: Optional[Decimal] = None + equivalency_id: Optional[int] = None class EquivalencyItemResponse(EquivalencyItemBase): @@ -34,7 +36,7 @@ class EquivalencyItemResponse(EquivalencyItemBase): class EquivalencyBase(BaseModel): - identifier: str = Field(..., max_length=10, description="Identificador único") + identifier: str = Field(..., max_length=50, description="Identificador único") description: Optional[str] = Field(None, max_length=200, description="Descripción (opcional)") item_id: Optional[int] = Field(None, description="ID del EquivalencyItem seleccionado") @@ -44,7 +46,7 @@ class EquivalencyCreate(EquivalencyBase): class EquivalencyUpdate(BaseModel): - identifier: Optional[str] = Field(None, max_length=10) + identifier: Optional[str] = Field(None, max_length=50) description: Optional[str] = Field(None, max_length=200) item_id: Optional[int] = Field(None, description="Re-asociar a otro EquivalencyItem") diff --git a/backend/api/v1/modules/a76/general_catalogs/equivalencies/models.py b/backend/api/v1/modules/a76/general_catalogs/equivalencies/models.py index 211badd7..cfcd69c2 100644 --- a/backend/api/v1/modules/a76/general_catalogs/equivalencies/models.py +++ b/backend/api/v1/modules/a76/general_catalogs/equivalencies/models.py @@ -13,7 +13,7 @@ class EquivalencyItem(Base, TenantScopedMixin, TimestampMixin): __tablename__ = "equivalency_items" __table_args__ = ( UniqueConstraint( - "original_field", "external_field", "tenant_id", "company_id", + "equivalency_id", "original_field", "external_field", "tenant_id", "company_id", name="uq_equivalency_item_fields" ), {"schema": "a76"} @@ -27,6 +27,14 @@ class EquivalencyItem(Base, TenantScopedMixin, TimestampMixin): conversion_factor: Mapped[Optional[Decimal]] = mapped_column( Numeric(13, 6), nullable=True) + equivalency_id: Mapped[Optional[int]] = mapped_column( + Integer, + ForeignKey("a76.equivalencies.id", ondelete="CASCADE"), + nullable=True) + + equivalency: Mapped[Optional["Equivalency"]] = relationship( + back_populates="items", foreign_keys=[equivalency_id]) + equivalencies: Mapped[List["Equivalency"]] = relationship( back_populates="item", foreign_keys="[Equivalency.item_id]") @@ -44,7 +52,7 @@ class Equivalency(Base, TenantScopedMixin, TimestampMixin): id: Mapped[int] = mapped_column( Integer, primary_key=True, autoincrement=True) - identifier: Mapped[str] = mapped_column(String(10), nullable=False) + identifier: Mapped[str] = mapped_column(String(50), nullable=False) description: Mapped[Optional[str]] = mapped_column( String(200), nullable=True) @@ -55,3 +63,6 @@ class Equivalency(Base, TenantScopedMixin, TimestampMixin): item: Mapped[Optional["EquivalencyItem"]] = relationship( back_populates="equivalencies", foreign_keys=[item_id]) + + items: Mapped[List["EquivalencyItem"]] = relationship( + back_populates="equivalency", foreign_keys="[EquivalencyItem.equivalency_id]") diff --git a/backend/api/v1/modules/a76/general_catalogs/equivalencies/service.py b/backend/api/v1/modules/a76/general_catalogs/equivalencies/service.py index 2732877c..2713e0de 100644 --- a/backend/api/v1/modules/a76/general_catalogs/equivalencies/service.py +++ b/backend/api/v1/modules/a76/general_catalogs/equivalencies/service.py @@ -33,6 +33,10 @@ class EquivalencyItemService: query = query.filter( EquivalencyItem.external_field.ilike(f"%{filters['external_field']}%") ) + if "equivalency_id" in filters: + query = query.filter( + EquivalencyItem.equivalency_id == filters["equivalency_id"] + ) total = query.count() items = query.offset(skip).limit(limit).all() @@ -63,6 +67,7 @@ class EquivalencyItemService: original_field=data.original_field, external_field=data.external_field, conversion_factor=data.conversion_factor, + equivalency_id=data.equivalency_id, tenant_id=tenant_id, company_id=company_id, ) diff --git a/frontend/src/lib/api/dashboard/a76/general_catalogs/equivalencies.ts b/frontend/src/lib/api/dashboard/a76/general_catalogs/equivalencies.ts index b54abcfd..2190468a 100644 --- a/frontend/src/lib/api/dashboard/a76/general_catalogs/equivalencies.ts +++ b/frontend/src/lib/api/dashboard/a76/general_catalogs/equivalencies.ts @@ -10,6 +10,7 @@ export interface EquivalencyItem { original_field: string; external_field: string; conversion_factor: number | null; + equivalency_id: number | null; tenant_id: number; company_id: number; } @@ -18,12 +19,14 @@ export interface EquivalencyItemCreate { original_field: string; external_field: string; conversion_factor?: number | null; + equivalency_id?: number | null; } export interface EquivalencyItemUpdate { original_field?: string; external_field?: string; conversion_factor?: number | null; + equivalency_id?: number | null; } export interface EquivalencyItemListResponse { diff --git a/frontend/src/lib/components/dashboard/general_catalogs/equivalencies/create-edit-dialog.svelte b/frontend/src/lib/components/dashboard/general_catalogs/equivalencies/create-edit-dialog.svelte index f6628eee..556f0d5a 100644 --- a/frontend/src/lib/components/dashboard/general_catalogs/equivalencies/create-edit-dialog.svelte +++ b/frontend/src/lib/components/dashboard/general_catalogs/equivalencies/create-edit-dialog.svelte @@ -16,11 +16,13 @@ let { open = $bindable(false), item = null, + equivalency_id = null, defaultOriginalField = null, onSuccess }: { open: boolean; item?: EquivalencyItem | null; + equivalency_id?: number | null; defaultOriginalField?: string | null; onSuccess?: () => void; } = $props(); @@ -82,7 +84,8 @@ const dataToSend = { original_field: formData.original_field.trim(), - external_field: formData.external_field.trim() + external_field: formData.external_field.trim(), + equivalency_id: equivalency_id }; if (isEdit && item) { diff --git a/frontend/src/lib/components/dashboard/general_catalogs/equivalencies/data-equivalencies-dialog.svelte b/frontend/src/lib/components/dashboard/general_catalogs/equivalencies/data-equivalencies-dialog.svelte index f35ff469..e2b5f95e 100644 --- a/frontend/src/lib/components/dashboard/general_catalogs/equivalencies/data-equivalencies-dialog.svelte +++ b/frontend/src/lib/components/dashboard/general_catalogs/equivalencies/data-equivalencies-dialog.svelte @@ -7,6 +7,7 @@ import * as Table from '$lib/components/ui/table'; import { companyStore } from '$lib/stores/company.svelte'; import { toast } from 'svelte-sonner'; + import { friendlyApiErrorParts } from '$lib/api'; import { Check } from 'lucide-svelte'; import type { Equivalency, @@ -68,7 +69,17 @@ loading = true; error = null; try { - const resp = await getEquivalencyItems(1, 2000, companyId); + const eq = getEffectiveEquivalency(); + if (!eq && mode === 'create') { + items = []; + loading = false; + return; + } + + const filters: Record = {}; + if (eq) filters.equivalency_id = eq.id; + + const resp = await getEquivalencyItems(1, 2000, companyId, filters); if (resp.error) throw new Error(resp.error); items = resp.data.items || []; } catch (e) { @@ -144,7 +155,8 @@ companyId ); if (created.error || !created.data) { - toast.error(created.error || 'Error al guardar'); + const { title, description } = friendlyApiErrorParts(created); + toast.error(title, { description }); return; } savedEquivalency = created.data; @@ -248,7 +260,6 @@ function handleInputsSuccess() { inputsOpen = false; reloadItems(); - onSuccess?.(); } @@ -378,6 +389,7 @@