feat: Add payments, transport, and validation tabs for pedimento editing

- Implemented PaymentsTabForm component for managing payment information.
- Implemented TransportTabForm component for managing transport details.
- Implemented ValidationTabForm component for managing validation documents.
- Enhanced the main edit page to include new tabs and handle data saving for each section.
- Added alert components for success and error messages during save operations.
- Updated server-side logic to handle fetching and saving of pedimento data.
This commit is contained in:
2025-11-07 00:02:35 -06:00
parent 1de76c6efb
commit e6d7d23328
39 changed files with 1661 additions and 1110 deletions

View File

@@ -1,12 +1,13 @@
# Anexo76
**Aplicación SaaS para gestión de comercio exterior conforme a Anexos 24, 31 y 22 del SAT**
**Aplicación SaaS para gestión de comercio exterior conforme a Anexos 24, 30 y 22 del SAT**
Anexo76 es una plataforma multi-tenant diseñada para maquilas, empresas IMMEX y agentes aduanales, que permite gestionar inventarios, pedimentos y facturas de importación/exportación con control de licencias y cumplimiento normativo.
## 🏗️ Arquitectura
### Backend
- **Framework**: FastAPI 0.110+
- **Autenticación**: Keycloak (OpenID Connect)
- **Base de Datos**: PostgreSQL con SQLAlchemy
@@ -20,11 +21,13 @@ Anexo76 es una plataforma multi-tenant diseñada para maquilas, empresas IMMEX y
- `routes.py`: Endpoints API
### Frontend
- **Framework**: SvelteKit
- **Autenticación**: keycloak-js
- **UI**: Dashboard moderno y responsivo
### Infraestructura
- **Containerización**: Docker / Docker Compose
- **Orquestación**: Kubernetes (futuro)
- **Monitoreo**: Prometheus + Grafana
@@ -64,6 +67,7 @@ anexo76/
## 🚀 Inicio Rápido
### Requisitos Previos
- Docker y Docker Compose
- Python 3.11+ (para desarrollo local)
- Node.js 18+ (para desarrollo frontend)
@@ -89,6 +93,7 @@ docker-compose up -d
```
Esto iniciará:
- **PostgreSQL** en `localhost:5432`
- **Keycloak** en `localhost:8080`
- **Backend API** en `localhost:8000`
@@ -150,17 +155,20 @@ npm run dev
## 📦 Módulos Principales
### 1. **Auth** (`/v1/auth`)
- Login con Keycloak
- Refresh token
- Logout
- Información de usuario
### 2. **Tenants** (`/v1/tenants`)
- Creación y gestión de tenants
- Upgrade de BD compartida a dedicada
- Gestión de realms de Keycloak
### 3. **Licenses** (`/v1/licenses`)
- Control de planes (Free, Basic, Professional, Enterprise)
- Validación de licencias activas
- Tracking de uso (usuarios, storage, operaciones)
@@ -188,11 +196,13 @@ npm run dev
### Modelo Híbrido
**BD Compartida** (tenants pequeños/medianos):
- Tabla única con `tenant_id` como foreign key
- Row-level security
- Más económico para clientes con bajo volumen
**BD Dedicada** (tenants enterprise):
- Base de datos PostgreSQL independiente
- Máximo aislamiento y performance
- Configuración almacenada en `tenants.db_config`
@@ -219,16 +229,18 @@ service.upgrade_to_dedicated(tenant_id=123, db_config=db_config)
### Planes Disponibles
| Plan | Usuarios | Storage | Operaciones/mes | Features |
|------|----------|---------|-----------------|----------|
| Free | 5 | 10 GB | 1,000 | API básica |
| Basic | 20 | 50 GB | 10,000 | + Reportes |
| Professional | 100 | 200 GB | 50,000 | + Integraciones |
| Enterprise | Ilimitado | Ilimitado | Ilimitado | + Soporte dedicado + BD dedicada |
| Plan | Usuarios | Storage | Operaciones/mes | Features |
| ------------ | --------- | --------- | --------------- | -------------------------------- |
| Free | 5 | 10 GB | 1,000 | API básica |
| Basic | 20 | 50 GB | 10,000 | + Reportes |
| Professional | 100 | 200 GB | 50,000 | + Integraciones |
| Enterprise | Ilimitado | Ilimitado | Ilimitado | + Soporte dedicado + BD dedicada |
### Middleware de Validación
El `LicenseValidationMiddleware` verifica en cada request:
- ✅ Licencia activa
- ✅ No expirada
- ✅ Límites no excedidos
@@ -254,6 +266,7 @@ npm test
### Prometheus Metrics
El backend expone métricas en `/metrics`:
- Request duration
- Request count por endpoint
- Error rate
@@ -262,6 +275,7 @@ El backend expone métricas en `/metrics`:
### Logging
Logs estructurados con nivel configurable:
- INFO: Operaciones normales
- WARNING: Validaciones fallidas
- ERROR: Errores de sistema
@@ -282,6 +296,7 @@ Este proyecto es privado y propietario.
## 📞 Soporte
Para soporte técnico o consultas:
- Email: soporte@anexo76.com
- Documentación: https://docs.anexo76.com

View File

@@ -1,424 +0,0 @@
"""Pedimentos
Revision ID: 03b786378f94
Revises: 7937209f9718
Create Date: 2025-11-06 17:07:16.536298
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '03b786378f94'
down_revision: Union[str, Sequence[str], None] = '54f2046774d0'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('pedimentos',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('tenant_id', sa.Integer(), nullable=False),
sa.Column('year', sa.String(length=2), nullable=True),
sa.Column('customs_office', sa.String(length=2), nullable=True),
sa.Column('license', sa.String(length=4), nullable=True),
sa.Column('pedimento_number', sa.String(length=7), nullable=True),
sa.Column('client_id', sa.Integer(), nullable=True),
sa.Column('operation_type', sa.Integer(), nullable=True),
sa.Column('pedimento_type', sa.Integer(), nullable=True),
sa.Column('pedimento_key', sa.String(length=2), nullable=True),
sa.Column('regime', sa.String(length=3), nullable=True),
sa.Column('status', sa.String(length=30), nullable=True),
sa.Column('usd_value', sa.Numeric(precision=17, scale=6), nullable=True),
sa.Column('paid_price', sa.Numeric(precision=17, scale=6), nullable=True),
sa.Column('gross_weight', sa.Numeric(precision=19, scale=3), nullable=True),
sa.Column('exchange_rate', sa.Numeric(precision=9, scale=5), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ),
sa.PrimaryKeyConstraint('id', name='pedimentos_pkey'),
schema='a76'
)
op.create_index('idx_pedimentos_client_id', 'pedimentos', ['client_id'], unique=False, schema='a76')
op.create_index('idx_pedimentos_created_at', 'pedimentos', ['created_at'], unique=False, schema='a76')
op.create_index('idx_pedimentos_status', 'pedimentos', ['status'], unique=False, schema='a76')
op.create_index(op.f('ix_a76_pedimentos_tenant_id'), 'pedimentos', ['tenant_id'], unique=False, schema='a76')
op.create_table('pedimento_config_additional',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('pedimento_id', sa.Integer(), nullable=False),
sa.Column('tenant_id', sa.Integer(), nullable=False),
sa.Column('add_po_identifier', sa.SmallInteger(), nullable=True),
sa.Column('do_not_exempt_norms_complement_x', sa.SmallInteger(), nullable=True),
sa.Column('manual_pedimento_year', sa.String(length=2), nullable=True),
sa.Column('enable_import_invoice_recipient', sa.SmallInteger(), nullable=True),
sa.Column('send_502_validation_file_for_consolidated', sa.SmallInteger(), nullable=True),
sa.Column('add_remove_norms', sa.SmallInteger(), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_config_additional', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ),
sa.PrimaryKeyConstraint('id', name='pedimento_config_additional_pkey'),
sa.UniqueConstraint('pedimento_id', name='pedimento_config_additional_pedimento_id_key'),
schema='a76'
)
op.create_index(op.f('ix_a76_pedimento_config_additional_tenant_id'), 'pedimento_config_additional', ['tenant_id'], unique=False, schema='a76')
op.create_table('pedimento_config_calculations',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('pedimento_id', sa.Integer(), nullable=False),
sa.Column('tenant_id', sa.Integer(), nullable=False),
sa.Column('dta_type', sa.String(length=1), nullable=True),
sa.Column('dta_operation', sa.SmallInteger(), nullable=True),
sa.Column('dta_vehicle_count', sa.SmallInteger(), nullable=True),
sa.Column('dta_mixed_rate_8permil', sa.SmallInteger(), nullable=True),
sa.Column('pays_vat', sa.SmallInteger(), nullable=True),
sa.Column('pays_prevalidation', sa.SmallInteger(), nullable=True),
sa.Column('include_sagar_certificate_fee', sa.SmallInteger(), nullable=True),
sa.Column('fixed_vehicle_dta_fee', sa.SmallInteger(), nullable=True),
sa.Column('additional_fixed_fee', sa.SmallInteger(), nullable=True),
sa.Column('additional_fixed_fee_payment_method', sa.SmallInteger(), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_config_calculations', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ),
sa.PrimaryKeyConstraint('id', name='pedimento_config_calculations_pkey'),
sa.UniqueConstraint('pedimento_id', name='pedimento_config_calculations_pedimento_id_key'),
schema='a76'
)
op.create_index(op.f('ix_a76_pedimento_config_calculations_tenant_id'), 'pedimento_config_calculations', ['tenant_id'], unique=False, schema='a76')
op.create_table('pedimento_config_parameters',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('pedimento_id', sa.Integer(), nullable=False),
sa.Column('tenant_id', sa.Integer(), nullable=False),
sa.Column('is_embassy', sa.SmallInteger(), nullable=True),
sa.Column('embassy_dta', sa.Numeric(precision=11, scale=2), nullable=True),
sa.Column('rule_3121_section_ii', sa.SmallInteger(), nullable=True),
sa.Column('use_previous_tariff', sa.SmallInteger(), nullable=True),
sa.Column('use_payment_date_fi', sa.SmallInteger(), nullable=True),
sa.Column('add_state_supplier_record_505', sa.SmallInteger(), nullable=True),
sa.Column('customs_value_calculation', sa.SmallInteger(), nullable=True),
sa.Column('two_decimals_unit_value', sa.SmallInteger(), nullable=True),
sa.Column('customs_value_per_item', sa.SmallInteger(), nullable=True),
sa.Column('is_national_supplier', sa.SmallInteger(), nullable=True),
sa.Column('is_consolidated', sa.SmallInteger(), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_config_parameters', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ),
sa.PrimaryKeyConstraint('id', name='pedimento_config_parameters_pkey'),
sa.UniqueConstraint('pedimento_id', name='pedimento_config_parameters_pedimento_id_key'),
schema='a76'
)
op.create_index(op.f('ix_a76_pedimento_config_parameters_tenant_id'), 'pedimento_config_parameters', ['tenant_id'], unique=False, schema='a76')
op.create_table('pedimento_config_surcharges',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('pedimento_id', sa.Integer(), nullable=False),
sa.Column('tenant_id', sa.Integer(), nullable=False),
sa.Column('surcharge_igi', sa.SmallInteger(), nullable=True),
sa.Column('surcharge_dta', sa.SmallInteger(), nullable=True),
sa.Column('surcharge_vat', sa.SmallInteger(), nullable=True),
sa.Column('surcharge_isan', sa.SmallInteger(), nullable=True),
sa.Column('surcharge_ieps', sa.SmallInteger(), nullable=True),
sa.Column('surcharge_cc', sa.SmallInteger(), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_config_surcharges', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ),
sa.PrimaryKeyConstraint('id', name='pedimento_config_surcharges_pkey'),
sa.UniqueConstraint('pedimento_id', name='pedimento_config_surcharges_pedimento_id_key'),
schema='a76'
)
op.create_index(op.f('ix_a76_pedimento_config_surcharges_tenant_id'), 'pedimento_config_surcharges', ['tenant_id'], unique=False, schema='a76')
op.create_table('pedimento_config_update_rectification',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('pedimento_id', sa.Integer(), nullable=False),
sa.Column('tenant_id', sa.Integer(), nullable=False),
sa.Column('update_vat', sa.SmallInteger(), nullable=True),
sa.Column('update_advalorem', sa.SmallInteger(), nullable=True),
sa.Column('update_cc', sa.SmallInteger(), nullable=True),
sa.Column('update_ieps', sa.SmallInteger(), nullable=True),
sa.Column('calculate_surcharge', sa.SmallInteger(), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_config_update_rectification', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ),
sa.PrimaryKeyConstraint('id', name='pedimento_config_update_rectification_pkey'),
sa.UniqueConstraint('pedimento_id', name='pedimento_config_update_rectification_pedimento_id_key'),
schema='a76'
)
op.create_index(op.f('ix_a76_pedimento_config_update_rectification_tenant_id'), 'pedimento_config_update_rectification', ['tenant_id'], unique=False, schema='a76')
op.create_table('pedimento_config_updates',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('pedimento_id', sa.Integer(), nullable=False),
sa.Column('tenant_id', sa.Integer(), nullable=False),
sa.Column('update_vat', sa.SmallInteger(), nullable=True),
sa.Column('update_advalorem', sa.SmallInteger(), nullable=True),
sa.Column('update_cc', sa.SmallInteger(), nullable=True),
sa.Column('update_ieps', sa.SmallInteger(), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_config_updates', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ),
sa.PrimaryKeyConstraint('id', name='pedimento_config_updates_pkey'),
sa.UniqueConstraint('pedimento_id', name='pedimento_config_updates_pedimento_id_key'),
schema='a76'
)
op.create_index(op.f('ix_a76_pedimento_config_updates_tenant_id'), 'pedimento_config_updates', ['tenant_id'], unique=False, schema='a76')
op.create_table('pedimento_customs_offices',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('pedimento_id', sa.Integer(), nullable=False),
sa.Column('tenant_id', sa.Integer(), nullable=False),
sa.Column('dispatch_customs', sa.String(length=3), nullable=True),
sa.Column('entry_exit_customs', sa.String(length=3), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_customs_offices', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ),
sa.PrimaryKeyConstraint('id', name='pedimento_customs_offices_pkey'),
sa.UniqueConstraint('pedimento_id', name='pedimento_customs_offices_pedimento_id_key'),
schema='a76'
)
op.create_index(op.f('ix_a76_pedimento_customs_offices_tenant_id'), 'pedimento_customs_offices', ['tenant_id'], unique=False, schema='a76')
op.create_table('pedimento_dates',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('pedimento_id', sa.Integer(), nullable=False),
sa.Column('tenant_id', sa.Integer(), nullable=False),
sa.Column('entry_date', sa.DateTime(), nullable=True),
sa.Column('pedimento_date', sa.DateTime(), nullable=True),
sa.Column('payment_date', sa.DateTime(), nullable=True),
sa.Column('rectification_payment_date', sa.DateTime(), nullable=True),
sa.Column('extraction_date', sa.DateTime(), nullable=True),
sa.Column('submission_date', sa.DateTime(), nullable=True),
sa.Column('eucan_date', sa.DateTime(), nullable=True),
sa.Column('original_date', sa.DateTime(), nullable=True),
sa.Column('start_date', sa.DateTime(), nullable=True),
sa.Column('end_date', sa.DateTime(), nullable=True),
sa.Column('capture_date', sa.DateTime(), nullable=True),
sa.Column('capture_time', sa.Time(), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_dates', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ),
sa.PrimaryKeyConstraint('id', name='pedimento_dates_pkey'),
sa.UniqueConstraint('pedimento_id', name='pedimento_dates_pedimento_id_key'),
schema='a76'
)
op.create_index('idx_pedimento_dates_pedimento_id', 'pedimento_dates', ['pedimento_id'], unique=False, schema='a76')
op.create_index(op.f('ix_a76_pedimento_dates_tenant_id'), 'pedimento_dates', ['tenant_id'], unique=False, schema='a76')
op.create_table('pedimento_decrementables',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('pedimento_id', sa.Integer(), nullable=False),
sa.Column('tenant_id', sa.Integer(), nullable=False),
sa.Column('freight', sa.Numeric(precision=13, scale=2), nullable=True),
sa.Column('insurance', sa.Numeric(precision=13, scale=2), nullable=True),
sa.Column('loading', sa.Numeric(precision=13, scale=2), nullable=True),
sa.Column('unloading', sa.Numeric(precision=13, scale=2), nullable=True),
sa.Column('others', sa.Numeric(precision=13, scale=2), nullable=True),
sa.Column('currency', sa.String(length=3), nullable=True),
sa.Column('currency_factor', sa.Numeric(precision=15, scale=8), nullable=True),
sa.Column('not_affect_usd_value', sa.SmallInteger(), nullable=True),
sa.Column('not_affect_customs_value', sa.SmallInteger(), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_decrementables', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ),
sa.PrimaryKeyConstraint('id', name='pedimento_decrementables_pkey'),
sa.UniqueConstraint('pedimento_id', name='pedimento_decrementables_pedimento_id_key'),
schema='a76'
)
op.create_index(op.f('ix_a76_pedimento_decrementables_tenant_id'), 'pedimento_decrementables', ['tenant_id'], unique=False, schema='a76')
op.create_table('pedimento_incrementables',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('pedimento_id', sa.Integer(), nullable=False),
sa.Column('tenant_id', sa.Integer(), nullable=False),
sa.Column('insured_value', sa.Numeric(precision=13, scale=2), nullable=True),
sa.Column('freight', sa.Numeric(precision=13, scale=2), nullable=True),
sa.Column('insurance', sa.Numeric(precision=13, scale=2), nullable=True),
sa.Column('packaging', sa.Numeric(precision=13, scale=2), nullable=True),
sa.Column('others', sa.Numeric(precision=13, scale=3), nullable=True),
sa.Column('deductibles', sa.Numeric(precision=13, scale=3), nullable=True),
sa.Column('currency', sa.String(length=3), nullable=True),
sa.Column('currency_factor', sa.Numeric(precision=15, scale=8), nullable=True),
sa.Column('not_affect_usd_value', sa.SmallInteger(), nullable=True),
sa.Column('not_affect_customs_value', sa.SmallInteger(), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_incrementables', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ),
sa.PrimaryKeyConstraint('id', name='pedimento_incrementables_pkey'),
sa.UniqueConstraint('pedimento_id', name='pedimento_incrementables_pedimento_id_key'),
schema='a76'
)
op.create_index(op.f('ix_a76_pedimento_incrementables_tenant_id'), 'pedimento_incrementables', ['tenant_id'], unique=False, schema='a76')
op.create_table('pedimento_indexes',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('pedimento_id', sa.Integer(), nullable=False),
sa.Column('tenant_id', sa.Integer(), nullable=False),
sa.Column('update_factor_type', sa.SmallInteger(), nullable=True),
sa.Column('update_factor', sa.Numeric(precision=7, scale=4), nullable=True),
sa.Column('manual_update_factor', sa.SmallInteger(), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_indexes', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ),
sa.PrimaryKeyConstraint('id', name='pedimento_indexes_pkey'),
sa.UniqueConstraint('pedimento_id', name='pedimento_indexes_pedimento_id_key'),
schema='a76'
)
op.create_index(op.f('ix_a76_pedimento_indexes_tenant_id'), 'pedimento_indexes', ['tenant_id'], unique=False, schema='a76')
op.create_table('pedimento_payments',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('pedimento_id', sa.Integer(), nullable=False),
sa.Column('tenant_id', sa.Integer(), nullable=False),
sa.Column('acknowledgment', sa.String(length=20), nullable=True),
sa.Column('operation_number', sa.String(length=14), nullable=True),
sa.Column('bank_code', sa.Integer(), nullable=True),
sa.Column('cashier', sa.String(length=2), nullable=True),
sa.Column('date', sa.Date(), nullable=True),
sa.Column('time', sa.Time(), nullable=True),
sa.Column('shift', sa.String(length=1), nullable=True),
sa.Column('total_cash_paid', sa.Integer(), nullable=True),
sa.Column('total_contributions', sa.Integer(), nullable=True),
sa.Column('counter_payment', sa.SmallInteger(), nullable=True),
sa.Column('pece_code', sa.String(length=5), nullable=True),
sa.Column('payment_id', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_payments', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ),
sa.PrimaryKeyConstraint('id', name='pedimento_payments_pkey'),
sa.UniqueConstraint('pedimento_id', name='pedimento_payments_pedimento_id_key'),
schema='a76'
)
op.create_index('idx_pedimento_payments_pedimento_id', 'pedimento_payments', ['pedimento_id'], unique=False, schema='a76')
op.create_index(op.f('ix_a76_pedimento_payments_tenant_id'), 'pedimento_payments', ['tenant_id'], unique=False, schema='a76')
op.create_table('pedimento_rectification_destination',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('pedimento_id', sa.Integer(), nullable=False),
sa.Column('tenant_id', sa.Integer(), nullable=False),
sa.Column('destination_pedimento_year', sa.String(length=2), nullable=True),
sa.Column('destination_customs_office', sa.String(length=3), nullable=True),
sa.Column('destination_license', sa.String(length=4), nullable=True),
sa.Column('destination_pedimento_number', sa.String(length=7), nullable=True),
sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_rectification_destination', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ),
sa.PrimaryKeyConstraint('id', name='pedimento_rectification_destination_pkey'),
sa.UniqueConstraint('pedimento_id', name='pedimento_rectification_destination_pedimento_id_key'),
schema='a76'
)
op.create_index(op.f('ix_a76_pedimento_rectification_destination_tenant_id'), 'pedimento_rectification_destination', ['tenant_id'], unique=False, schema='a76')
op.create_table('pedimento_rectification_origin',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('pedimento_id', sa.Integer(), nullable=False),
sa.Column('tenant_id', sa.Integer(), nullable=False),
sa.Column('original_pedimento_year', sa.String(length=2), nullable=True),
sa.Column('original_customs_office', sa.String(length=3), nullable=True),
sa.Column('original_license', sa.String(length=4), nullable=True),
sa.Column('original_pedimento_number', sa.String(length=7), nullable=True),
sa.Column('original_pedimento_key', sa.String(length=2), nullable=True),
sa.Column('original_payment_date', sa.DateTime(), nullable=True),
sa.Column('total_cash', sa.Integer(), nullable=True),
sa.Column('total_others', sa.Integer(), nullable=True),
sa.Column('reason', sa.String(length=255), nullable=True),
sa.Column('charge_to_client', sa.SmallInteger(), nullable=True),
sa.Column('use_original_payment_date_for_interest_calc', sa.SmallInteger(), nullable=True),
sa.Column('manual_calculation', sa.SmallInteger(), nullable=True),
sa.Column('original_pedimento_norms', sa.SmallInteger(), nullable=True),
sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_rectification_origin', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ),
sa.PrimaryKeyConstraint('id', name='pedimento_rectification_origin_pkey'),
sa.UniqueConstraint('pedimento_id', name='pedimento_rectification_origin_pedimento_id_key'),
schema='a76'
)
op.create_index(op.f('ix_a76_pedimento_rectification_origin_tenant_id'), 'pedimento_rectification_origin', ['tenant_id'], unique=False, schema='a76')
op.create_table('pedimento_transport_means',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('pedimento_id', sa.Integer(), nullable=False),
sa.Column('tenant_id', sa.Integer(), nullable=False),
sa.Column('destination', sa.SmallInteger(), nullable=True),
sa.Column('entry_exit', sa.String(length=2), nullable=True),
sa.Column('arrival', sa.String(length=2), nullable=True),
sa.Column('departure', sa.String(length=2), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_transport_means', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ),
sa.PrimaryKeyConstraint('id', name='pedimento_transport_means_pkey'),
sa.UniqueConstraint('pedimento_id', name='pedimento_transport_means_pedimento_id_key'),
schema='a76'
)
op.create_index(op.f('ix_a76_pedimento_transport_means_tenant_id'), 'pedimento_transport_means', ['tenant_id'], unique=False, schema='a76')
op.create_table('pedimento_validation',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('pedimento_id', sa.Integer(), nullable=False),
sa.Column('tenant_id', sa.Integer(), nullable=False),
sa.Column('validator', sa.String(length=3), nullable=True),
sa.Column('validation_ack', sa.String(length=8), nullable=True),
sa.Column('pre_ack', sa.String(length=8), nullable=True),
sa.Column('line_signature', sa.String(length=50), nullable=True),
sa.Column('electronic_signature', sa.String(length=999), nullable=True),
sa.Column('certificate_number', sa.String(length=99), nullable=True),
sa.Column('validator_id', sa.Integer(), nullable=True),
sa.Column('responsible_id', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=True),
sa.ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], name='fk_pedimento_validation', ondelete='CASCADE'),
sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ),
sa.PrimaryKeyConstraint('id', name='pedimento_validation_pkey'),
sa.UniqueConstraint('pedimento_id', name='pedimento_validation_pedimento_id_key'),
schema='a76'
)
op.create_index(op.f('ix_a76_pedimento_validation_tenant_id'), 'pedimento_validation', ['tenant_id'], unique=False, schema='a76')
op.drop_constraint(op.f('fk_regimenped'), 'code_pedimento_regimens', type_='foreignkey')
op.drop_constraint(op.f('fk_codeped'), 'code_pedimento_regimens', type_='foreignkey')
op.create_foreign_key('fk_codeped', 'code_pedimento_regimens', 'pedimento_codes', ['pedimento_code'], ['code'], source_schema='public', referent_schema='public')
op.create_foreign_key('fk_regimenped', 'code_pedimento_regimens', 'pedimento_regimens', ['regimen_code'], ['code'], source_schema='public', referent_schema='public')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('fk_regimenped', 'code_pedimento_regimens', schema='public', type_='foreignkey')
op.drop_constraint('fk_codeped', 'code_pedimento_regimens', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('fk_codeped'), 'code_pedimento_regimens', 'pedimento_codes', ['pedimento_code'], ['code'])
op.create_foreign_key(op.f('fk_regimenped'), 'code_pedimento_regimens', 'pedimento_regimens', ['regimen_code'], ['code'])
op.drop_index(op.f('ix_a76_pedimento_validation_tenant_id'), table_name='pedimento_validation', schema='a76')
op.drop_table('pedimento_validation', schema='a76')
op.drop_index(op.f('ix_a76_pedimento_transport_means_tenant_id'), table_name='pedimento_transport_means', schema='a76')
op.drop_table('pedimento_transport_means', schema='a76')
op.drop_index(op.f('ix_a76_pedimento_rectification_origin_tenant_id'), table_name='pedimento_rectification_origin', schema='a76')
op.drop_table('pedimento_rectification_origin', schema='a76')
op.drop_index(op.f('ix_a76_pedimento_rectification_destination_tenant_id'), table_name='pedimento_rectification_destination', schema='a76')
op.drop_table('pedimento_rectification_destination', schema='a76')
op.drop_index(op.f('ix_a76_pedimento_payments_tenant_id'), table_name='pedimento_payments', schema='a76')
op.drop_index('idx_pedimento_payments_pedimento_id', table_name='pedimento_payments', schema='a76')
op.drop_table('pedimento_payments', schema='a76')
op.drop_index(op.f('ix_a76_pedimento_indexes_tenant_id'), table_name='pedimento_indexes', schema='a76')
op.drop_table('pedimento_indexes', schema='a76')
op.drop_index(op.f('ix_a76_pedimento_incrementables_tenant_id'), table_name='pedimento_incrementables', schema='a76')
op.drop_table('pedimento_incrementables', schema='a76')
op.drop_index(op.f('ix_a76_pedimento_decrementables_tenant_id'), table_name='pedimento_decrementables', schema='a76')
op.drop_table('pedimento_decrementables', schema='a76')
op.drop_index(op.f('ix_a76_pedimento_dates_tenant_id'), table_name='pedimento_dates', schema='a76')
op.drop_index('idx_pedimento_dates_pedimento_id', table_name='pedimento_dates', schema='a76')
op.drop_table('pedimento_dates', schema='a76')
op.drop_index(op.f('ix_a76_pedimento_customs_offices_tenant_id'), table_name='pedimento_customs_offices', schema='a76')
op.drop_table('pedimento_customs_offices', schema='a76')
op.drop_index(op.f('ix_a76_pedimento_config_updates_tenant_id'), table_name='pedimento_config_updates', schema='a76')
op.drop_table('pedimento_config_updates', schema='a76')
op.drop_index(op.f('ix_a76_pedimento_config_update_rectification_tenant_id'), table_name='pedimento_config_update_rectification', schema='a76')
op.drop_table('pedimento_config_update_rectification', schema='a76')
op.drop_index(op.f('ix_a76_pedimento_config_surcharges_tenant_id'), table_name='pedimento_config_surcharges', schema='a76')
op.drop_table('pedimento_config_surcharges', schema='a76')
op.drop_index(op.f('ix_a76_pedimento_config_parameters_tenant_id'), table_name='pedimento_config_parameters', schema='a76')
op.drop_table('pedimento_config_parameters', schema='a76')
op.drop_index(op.f('ix_a76_pedimento_config_calculations_tenant_id'), table_name='pedimento_config_calculations', schema='a76')
op.drop_table('pedimento_config_calculations', schema='a76')
op.drop_index(op.f('ix_a76_pedimento_config_additional_tenant_id'), table_name='pedimento_config_additional', schema='a76')
op.drop_table('pedimento_config_additional', schema='a76')
op.drop_index(op.f('ix_a76_pedimentos_tenant_id'), table_name='pedimentos', schema='a76')
op.drop_index('idx_pedimentos_status', table_name='pedimentos', schema='a76')
op.drop_index('idx_pedimentos_created_at', table_name='pedimentos', schema='a76')
op.drop_index('idx_pedimentos_client_id', table_name='pedimentos', schema='a76')
op.drop_table('pedimentos', schema='a76')
op.drop_index(op.f('ix_a76_licenses_tenant_id'), table_name='licenses', schema='a76')
op.drop_index(op.f('ix_a76_licenses_id'), table_name='licenses', schema='a76')
op.drop_table('licenses', schema='a76')
op.drop_index(op.f('ix_a76_license_usage_tenant_id'), table_name='license_usage', schema='a76')
op.drop_index(op.f('ix_a76_license_usage_id'), table_name='license_usage', schema='a76')
op.drop_table('license_usage', schema='a76')
op.drop_index(op.f('ix_a76_tenants_slug'), table_name='tenants', schema='a76')
op.drop_index(op.f('ix_a76_tenants_name'), table_name='tenants', schema='a76')
op.drop_index(op.f('ix_a76_tenants_id'), table_name='tenants', schema='a76')
op.drop_table('tenants', schema='a76')
# ### end Alembic commands ###

View File

@@ -1,195 +0,0 @@
"""Create new A76 tables only - company, clients, parts, classes
Revision ID: 54f2046774d0
Revises: 7937209f9718
Create Date: 2025-11-06 03:38:27.848630
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '54f2046774d0'
down_revision: Union[str, Sequence[str], None] = '7937209f9718'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema - Create only new A76 tables."""
# Create new A76 tables only (skip existing tenants, licenses, license_usage)
op.create_table('client_provider',
sa.Column('client_id', sa.String(length=8), nullable=False),
sa.Column('type_nat_foreign', sa.String(length=1), nullable=True),
sa.Column('name', sa.String(length=256), nullable=True),
sa.Column('short_name', sa.String(length=10), nullable=True),
sa.Column('rfc', sa.String(length=30), nullable=True),
sa.Column('curp', sa.String(length=19), nullable=True),
sa.Column('client_or_provider', sa.String(length=1), nullable=True),
sa.Column('linking', sa.String(length=1), nullable=True),
sa.Column('transform_subassembly', sa.String(length=1), nullable=True),
sa.Column('extra_information', sa.String(length=399), nullable=True),
sa.Column('web_key', sa.String(length=40), nullable=True),
sa.Column('responsible', sa.String(length=80), nullable=True),
sa.Column('position', sa.String(length=30), nullable=True),
sa.Column('incoterm', sa.String(length=19), nullable=True),
sa.Column('is_national_provider', sa.String(length=2), nullable=True),
sa.Column('enabled_disabled', sa.SmallInteger(), nullable=True),
sa.PrimaryKeyConstraint('client_id'),
schema='a76'
)
op.create_table('gcompany',
sa.Column('id', sa.String(length=3), nullable=False),
sa.Column('consecutive', sa.Boolean(), nullable=False),
sa.Column('name', sa.String(length=255), nullable=True),
sa.Column('rfc', sa.String(length=30), nullable=True),
sa.Column('main_activity', sa.String(length=255), nullable=True),
sa.Column('program', sa.String(length=10), nullable=True),
sa.Column('program_number', sa.String(length=40), nullable=True),
sa.Column('prosec', sa.SmallInteger(), nullable=True),
sa.Column('prosec_authorization', sa.String(length=20), nullable=True),
sa.Column('manufacturer_id', sa.String(length=25), nullable=True),
sa.Column('broker_company', sa.String(length=10), nullable=True),
sa.Column('responsible', sa.String(length=80), nullable=True),
sa.Column('responsible_name', sa.String(length=20), nullable=True),
sa.Column('responsible_last_name', sa.String(length=20), nullable=True),
sa.Column('responsible_mother_last_name', sa.String(length=20), nullable=True),
sa.Column('responsible_rfc', sa.String(length=30), nullable=True),
sa.Column('position', sa.String(length=30), nullable=True),
sa.Column('logo', sa.String(length=255), nullable=True),
sa.Column('has_express_line', sa.Boolean(), nullable=True),
sa.Column('order_format_type', sa.String(length=19), nullable=True),
sa.Column('previous_code', sa.SmallInteger(), nullable=True),
sa.Column('is_service_company', sa.Boolean(), nullable=True),
sa.Column('client_name', sa.String(length=300), nullable=True),
sa.Column('subassembly_mode', sa.String(length=7), nullable=True),
sa.Column('curp', sa.String(length=19), nullable=True),
sa.Column('inter_db_name', sa.String(length=100), nullable=True),
sa.Column('ctpat_svi', sa.String(length=100), nullable=True),
sa.Column('trusted_exporter_number', sa.String(length=50), nullable=True),
sa.Column('prevalidator_key', sa.String(length=20), nullable=True),
sa.Column('seventh_amendment', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('consecutive'),
schema='a76'
)
op.create_table('classes',
sa.Column('client_key', sa.Integer(), nullable=False),
sa.Column('class_code', sa.String(length=8), nullable=False),
sa.Column('description_spanish', sa.String(length=500), nullable=True),
sa.Column('description_english', sa.String(length=500), nullable=True),
sa.Column('material_key', sa.String(length=10), nullable=True),
sa.Column('unit_of_measure', sa.String(length=5), nullable=True),
sa.Column('fraction', sa.String(length=10), nullable=True),
sa.Column('us_fraction', sa.String(length=16), nullable=True),
sa.Column('sub_key', sa.String(length=5), nullable=True),
sa.Column('physical_review', sa.SmallInteger(), nullable=True),
sa.Column('iva_exempt_fraction', sa.String(length=4), nullable=True),
sa.ForeignKeyConstraint(['material_key'], ['public.material_types.key'], ),
sa.PrimaryKeyConstraint('client_key', 'class_code'),
schema='a76'
)
op.create_table('parts',
sa.Column('client_key', sa.Integer(), nullable=False),
sa.Column('part_number', sa.String(length=49), nullable=False),
sa.Column('fraction', sa.String(length=10), nullable=True),
sa.Column('description_spanish', sa.String(length=500), nullable=True),
sa.Column('description_english', sa.String(length=500), nullable=True),
sa.Column('part_class', sa.String(length=8), nullable=True),
sa.Column('unit_of_measure', sa.String(length=5), nullable=True),
sa.Column('commercial_part_number', sa.String(length=70), nullable=True),
sa.Column('country_of_origin', sa.String(length=3), nullable=True),
sa.Column('unit_cost', sa.Numeric(precision=23, scale=8), nullable=True),
sa.Column('currency_type', sa.String(length=2), nullable=True),
sa.Column('currency_key', sa.String(length=3), nullable=True),
sa.Column('unit_weight', sa.Numeric(precision=19, scale=8), nullable=True),
sa.Column('weight_type', sa.String(length=6), nullable=True),
sa.Column('us_fraction', sa.String(length=16), nullable=True),
sa.Column('fda_key', sa.String(length=20), nullable=True),
sa.Column('fcc_key', sa.String(length=30), nullable=True),
sa.Column('license_code', sa.String(length=3), nullable=True),
sa.Column('eccn', sa.String(length=20), nullable=True),
sa.Column('export_code', sa.String(length=2), nullable=True),
sa.Column('exclusion_symbol', sa.String(length=19), nullable=True),
sa.Column('supplier', sa.String(length=14), nullable=True),
sa.Column('alternate_unit_measure', sa.String(length=14), nullable=True),
sa.Column('added_value', sa.Numeric(precision=23, scale=8), nullable=True),
sa.Column('enabled_disabled', sa.SmallInteger(), nullable=True),
sa.Column('creation_date', sa.Integer(), nullable=True),
sa.Column('modification_date', sa.Integer(), nullable=True),
sa.Column('modification_date_iso', sa.DateTime(timezone=True), nullable=True),
sa.Column('part_photo', sa.String(length=255), nullable=True),
sa.ForeignKeyConstraint(['country_of_origin'], ['public.countries.m3_key'], ),
sa.ForeignKeyConstraint(['currency_key'], ['public.currency_types.code'], ),
sa.PrimaryKeyConstraint('client_key', 'part_number'),
schema='a76'
)
# Create dependent tables after main tables
op.create_table('gclient_provider_address',
sa.Column('client_id', sa.String(length=8), nullable=False),
sa.Column('municipality', sa.String(length=150), nullable=True),
sa.Column('streets', sa.String(length=100), nullable=True),
sa.Column('neighborhood', sa.String(length=40), nullable=True),
sa.Column('interior_number', sa.String(length=20), nullable=True),
sa.Column('exterior_number', sa.String(length=20), nullable=True),
sa.Column('postal_code', sa.String(length=15), nullable=True),
sa.Column('city', sa.String(length=30), nullable=True),
sa.Column('state', sa.String(length=30), nullable=True),
sa.Column('country', sa.String(length=3), nullable=True),
sa.Column('phone', sa.String(length=30), nullable=True),
sa.Column('fax_number', sa.String(length=30), nullable=True),
sa.Column('email', sa.String(length=100), nullable=True),
sa.Column('contact', sa.String(length=50), nullable=True),
sa.Column('reference', sa.String(length=250), nullable=True),
sa.ForeignKeyConstraint(['client_id'], ['a76.client_provider.client_id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('client_id'),
schema='a76'
)
op.create_table('gclient_provider_programs',
sa.Column('client_id', sa.String(length=8), nullable=False),
sa.Column('program', sa.String(length=7), nullable=True),
sa.Column('program_number', sa.String(length=40), nullable=True),
sa.Column('prosec', sa.SmallInteger(), nullable=True),
sa.Column('prosec_authorization', sa.String(length=20), nullable=True),
sa.Column('secon_auth_date', sa.Integer(), nullable=True),
sa.Column('manufacturer_id', sa.String(length=25), nullable=True),
sa.Column('tax_id', sa.String(length=30), nullable=True),
sa.Column('broker', sa.String(length=6), nullable=True),
sa.Column('import_broker', sa.String(length=6), nullable=True),
sa.Column('transfer_key', sa.String(length=8), nullable=True),
sa.Column('secon_authorization', sa.String(length=20), nullable=True),
sa.Column('applied_proportion', sa.Numeric(precision=7, scale=2), nullable=True),
sa.Column('is_certified_company', sa.String(length=1), nullable=True),
sa.Column('certified_company_registry', sa.String(length=40), nullable=True),
sa.Column('donation_auth_number', sa.String(length=50), nullable=True),
sa.Column('ctpat_svi', sa.String(length=100), nullable=True),
sa.Column('tax_registry_number', sa.String(length=40), nullable=True),
sa.Column('subassembly_service', sa.SmallInteger(), nullable=True),
sa.Column('autse_dates', sa.Integer(), nullable=True),
sa.Column('autse_number', sa.String(length=300), nullable=True),
sa.ForeignKeyConstraint(['client_id'], ['a76.client_provider.client_id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('client_id'),
schema='a76'
)
def downgrade() -> None:
"""Downgrade schema - Drop only new A76 tables."""
# Drop tables in reverse dependency order
op.drop_table('gclient_provider_programs', schema='a76')
op.drop_table('gclient_provider_address', schema='a76')
op.drop_table('parts', schema='a76')
op.drop_table('classes', schema='a76')
op.drop_table('gcompany', schema='a76')
op.drop_table('client_provider', schema='a76')

View File

@@ -11,7 +11,7 @@ class PedimentoRectificationOriginBase(BaseModel):
original_customs_office: Optional[str] = Field(None, max_length=3, description="Original customs office")
original_license: Optional[str] = Field(None, max_length=4, description="Original license")
original_pedimento_number: Optional[str] = Field(None, max_length=7, description="Original pedimento number")
original_pedimento_key: Optional[str] = Field(None, max_length=2, description="Original pedimento key")
original_pedimento_code: Optional[str] = Field(None, max_length=2, description="Original pedimento key")
original_payment_date: Optional[datetime] = Field(None, description="Original payment date")
total_cash: Optional[int] = Field(None, description="Total cash")
total_others: Optional[int] = Field(None, description="Total others")
@@ -33,7 +33,7 @@ class PedimentoRectificationOriginUpdate(BaseModel):
original_customs_office: Optional[str] = Field(None, max_length=3)
original_license: Optional[str] = Field(None, max_length=4)
original_pedimento_number: Optional[str] = Field(None, max_length=7)
original_pedimento_key: Optional[str] = Field(None, max_length=2)
original_pedimento_code: Optional[str] = Field(None, max_length=2)
original_payment_date: Optional[datetime] = None
total_cash: Optional[int] = None
total_others: Optional[int] = None

View File

@@ -13,7 +13,7 @@ class PedimentosBase(BaseModel):
client_id: Optional[int] = Field(None, description="Client ID")
operation_type: Optional[int] = Field(None, description="Operation type")
pedimento_type: Optional[int] = Field(None, description="Pedimento type")
pedimento_key: Optional[str] = Field(None, max_length=2, description="Pedimento key")
pedimento_code: Optional[str] = Field(None, max_length=2, description="Pedimento key")
regime: Optional[str] = Field(None, max_length=3, description="Regime")
status: Optional[str] = Field(None, max_length=30, description="Status")
usd_value: Optional[Decimal] = Field(None, description="USD value")
@@ -36,7 +36,7 @@ class PedimentosUpdate(BaseModel):
client_id: Optional[int] = None
operation_type: Optional[int] = None
pedimento_type: Optional[int] = None
pedimento_key: Optional[str] = Field(None, max_length=2)
pedimento_code: Optional[str] = Field(None, max_length=2)
regime: Optional[str] = Field(None, max_length=3)
status: Optional[str] = Field(None, max_length=30)
usd_value: Optional[Decimal] = None

View File

@@ -20,7 +20,7 @@ class PedimentoRectificationOrigin(Base):
original_customs_office = mapped_column(String(3))
original_license = mapped_column(String(4))
original_pedimento_number = mapped_column(String(7))
original_pedimento_key = mapped_column(String(2))
original_pedimento_code = mapped_column(String(2))
original_payment_date = mapped_column(DateTime)
total_cash = mapped_column(Integer)
total_others = mapped_column(Integer)

View File

@@ -7,6 +7,8 @@ from core.database import Base
class Pedimentos(Base):
__tablename__ = 'pedimentos'
__table_args__ = (
ForeignKeyConstraint(['regime'], ['public.pedimento_regimens.code']),
ForeignKeyConstraint(['pedimento_code'], ['public.pedimento_codes.code']),
ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']),
PrimaryKeyConstraint('id', name='pedimentos_pkey'),
Index('idx_pedimentos_client_id', 'client_id'),
@@ -24,7 +26,7 @@ class Pedimentos(Base):
client_id = mapped_column(Integer)
operation_type = mapped_column(Integer)
pedimento_type = mapped_column(Integer)
pedimento_key = mapped_column(String(2))
pedimento_code = mapped_column(String(2))
regime = mapped_column(String(3))
status = mapped_column(String(30))
usd_value = mapped_column(Numeric(17, 6))

View File

@@ -12,6 +12,7 @@ from core.middleware import (
LicenseValidationMiddleware,
RequestLoggingMiddleware
)
from core.database import init_db
from api.v1.router import router as api_v1_router
# Configurar logging
@@ -26,12 +27,20 @@ logger = logging.getLogger(__name__)
app = FastAPI(
title="Anexo76 API",
version=settings.APP_VERSION,
description="Aplicación SaaS para gestión de comercio exterior conforme a Anexos 24, 31 y 22 del SAT",
description="Aplicación SaaS para gestión de comercio exterior conforme a Anexos 24, 30 y 22 del SAT",
docs_url="/api/docs" if settings.DEBUG else None,
redoc_url="/api/redoc" if settings.DEBUG else None,
openapi_url="/api/openapi.json" if settings.DEBUG else None,
)
# Inicializar la base de datos
@app.on_event("startup")
async def on_startup():
"""Evento de inicio de la aplicación"""
logger.info("Iniciando la aplicación Anexo76...")
init_db()
logger.info("Base de datos inicializada correctamente.")
# Configurar CORS
app.add_middleware(
CORSMiddleware,
@@ -41,6 +50,8 @@ app.add_middleware(
allow_headers=["*"],
)
logger.info(f"CORS configurado para orígenes: {settings.cors_origins_list}")
# Agregar middlewares personalizados
app.add_middleware(RequestLoggingMiddleware)
app.add_middleware(LicenseValidationMiddleware)

View File

@@ -162,7 +162,7 @@ services:
- KEYCLOAK_REALM=${KEYCLOAK_REALM:-master}
- KEYCLOAK_CLIENT_ID=${KEYCLOAK_CLIENT_ID:-anexo76-backend}
- KEYCLOAK_CLIENT_SECRET=${KEYCLOAK_CLIENT_SECRET:-dev-secret}
- CORS_ORIGINS=http://localhost:5180,http://localhost:3000
- CORS_ORIGINS=${CORS_ORIGINS:-http://localhost:5180,http://localhost:3000}
ports:
- "8000:8000"
depends_on:

View File

@@ -1,7 +1,7 @@
{
"context": {
"project_name": "Anexo76",
"description": "Aplicación SaaS para gestión de comercio exterior conforme a Anexos 24, 31 y 22 del SAT.",
"description": "Aplicación SaaS para gestión de comercio exterior conforme a Anexos 24, 30 y 22 del SAT.",
"business_goal": "Ofrecer una plataforma multi-tenant para maquilas, IMMEX y agentes aduanales que permita manejar inventarios, pedimentos y facturas de importación/exportación con control de licencias y cumplimiento normativo."
},
"architecture": {

View File

@@ -4,7 +4,7 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Anexo76 - Gestión de Comercio Exterior</title>
<meta name="description" content="Plataforma SaaS para gestión de comercio exterior conforme a Anexos 24, 31 y 22 del SAT" />
<meta name="description" content="Plataforma SaaS para gestión de comercio exterior conforme a Anexos 24, 30 y 22 del SAT" />
<script>
// Cargar el tema antes de renderizar para evitar flash
(function() {

View File

@@ -0,0 +1,41 @@
/**
* API Client para Fechas de Pedimentos
*/
import { api } from '$lib/api';
export interface PedimentoDates {
id: number;
pedimento_id: number;
tenant_id: number;
entry_date?: string | null;
presentation_date?: string | null;
payment_date?: string | null;
created_at: string;
updated_at: string;
}
export interface CreatePedimentoDatesData {
entry_date?: string | null;
presentation_date?: string | null;
payment_date?: string | null;
}
export interface UpdatePedimentoDatesData {
entry_date?: string | null;
presentation_date?: string | null;
payment_date?: string | null;
}
export const pedimentoDatesApi = {
get: (pedimentoId: number) =>
api.get<PedimentoDates>(`/v1/a76/pedimentos/${pedimentoId}/dates`),
create: (pedimentoId: number, data: CreatePedimentoDatesData) =>
api.post<PedimentoDates>(`/v1/a76/pedimentos/${pedimentoId}/dates`, data),
update: (pedimentoId: number, data: UpdatePedimentoDatesData) =>
api.put<PedimentoDates>(`/v1/a76/pedimentos/${pedimentoId}/dates`, data),
delete: (pedimentoId: number) =>
api.delete(`/v1/a76/pedimentos/${pedimentoId}/dates`)
};

View File

@@ -0,0 +1,38 @@
/**
* API Client para Pagos de Pedimentos
*/
import { api } from '$lib/api';
export interface PedimentoPayments {
id: number;
pedimento_id: number;
tenant_id: number;
payment_form?: string | null;
bank_identifier?: string | null;
created_at: string;
updated_at: string;
}
export interface CreatePedimentoPaymentsData {
payment_form?: string | null;
bank_identifier?: string | null;
}
export interface UpdatePedimentoPaymentsData {
payment_form?: string | null;
bank_identifier?: string | null;
}
export const pedimentoPaymentsApi = {
get: (pedimentoId: number) =>
api.get<PedimentoPayments>(`/v1/a76/pedimentos/${pedimentoId}/payments`),
create: (pedimentoId: number, data: CreatePedimentoPaymentsData) =>
api.post<PedimentoPayments>(`/v1/a76/pedimentos/${pedimentoId}/payments`, data),
update: (pedimentoId: number, data: UpdatePedimentoPaymentsData) =>
api.put<PedimentoPayments>(`/v1/a76/pedimentos/${pedimentoId}/payments`, data),
delete: (pedimentoId: number) =>
api.delete(`/v1/a76/pedimentos/${pedimentoId}/payments`)
};

View File

@@ -0,0 +1,44 @@
/**
* API Client para Medios de Transporte de Pedimentos
*/
import { api } from '$lib/api';
export interface PedimentoTransportMeans {
id: number;
pedimento_id: number;
tenant_id: number;
arrival_key?: string | null;
arrival_data?: string | null;
departure_key?: string | null;
departure_data?: string | null;
created_at: string;
updated_at: string;
}
export interface CreatePedimentoTransportMeansData {
arrival_key?: string | null;
arrival_data?: string | null;
departure_key?: string | null;
departure_data?: string | null;
}
export interface UpdatePedimentoTransportMeansData {
arrival_key?: string | null;
arrival_data?: string | null;
departure_key?: string | null;
departure_data?: string | null;
}
export const pedimentoTransportApi = {
get: (pedimentoId: number) =>
api.get<PedimentoTransportMeans>(`/v1/a76/pedimentos/${pedimentoId}/transport-means`),
create: (pedimentoId: number, data: CreatePedimentoTransportMeansData) =>
api.post<PedimentoTransportMeans>(`/v1/a76/pedimentos/${pedimentoId}/transport-means`, data),
update: (pedimentoId: number, data: UpdatePedimentoTransportMeansData) =>
api.put<PedimentoTransportMeans>(`/v1/a76/pedimentos/${pedimentoId}/transport-means`, data),
delete: (pedimentoId: number) =>
api.delete(`/v1/a76/pedimentos/${pedimentoId}/transport-means`)
};

View File

@@ -0,0 +1,35 @@
/**
* API Client para Validación de Pedimentos
*/
import { api } from '$lib/api';
export interface PedimentoValidation {
id: number;
pedimento_id: number;
tenant_id: number;
document?: string | null;
created_at: string;
updated_at: string;
}
export interface CreatePedimentoValidationData {
document?: string | null;
}
export interface UpdatePedimentoValidationData {
document?: string | null;
}
export const pedimentoValidationApi = {
get: (pedimentoId: number) =>
api.get<PedimentoValidation>(`/v1/a76/pedimentos/${pedimentoId}/validation`),
create: (pedimentoId: number, data: CreatePedimentoValidationData) =>
api.post<PedimentoValidation>(`/v1/a76/pedimentos/${pedimentoId}/validation`, data),
update: (pedimentoId: number, data: UpdatePedimentoValidationData) =>
api.put<PedimentoValidation>(`/v1/a76/pedimentos/${pedimentoId}/validation`, data),
delete: (pedimentoId: number) =>
api.delete(`/v1/a76/pedimentos/${pedimentoId}/validation`)
};

View File

@@ -14,7 +14,7 @@ export interface Pedimento {
client_id?: number | null;
operation_type?: number | null;
pedimento_type?: number | null;
pedimento_key?: string | null;
pedimento_code?: string | null;
regime?: string | null;
status?: string | null;
usd_value?: number | null;
@@ -39,7 +39,7 @@ export interface CreatePedimentoData {
client_id?: number | null;
operation_type?: number | null;
pedimento_type?: number | null;
pedimento_key?: string | null;
pedimento_code?: string | null;
regime?: string | null;
status?: string | null;
usd_value?: number | null;
@@ -56,7 +56,7 @@ export interface UpdatePedimentoData {
client_id?: number | null;
operation_type?: number | null;
pedimento_type?: number | null;
pedimento_key?: string | null;
pedimento_code?: string | null;
regime?: string | null;
status?: string | null;
usd_value?: number | null;

View File

@@ -13,7 +13,7 @@ export type Pedimento = {
client_id?: number | null;
operation_type?: number | null;
pedimento_type?: number | null;
pedimento_key?: string | null;
pedimento_code?: string | null;
regime?: string | null;
status?: string | null;
usd_value?: number | null;
@@ -99,7 +99,7 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
header: "Número de Pedimento",
cell: ({ row }) => {
const pedimento = row.original;
const fullNumber = `${pedimento.year || ''}${pedimento.customs_office || ''}${pedimento.license || ''}${pedimento.pedimento_number || ''}`;
const fullNumber = `${pedimento.year || ''}-${pedimento.customs_office || ''}-${pedimento.license || ''}-${pedimento.pedimento_number || ''}`;
const numberSnippet = createRawSnippet<[{ number: string }]>((getNumber) => {
const { number } = getNumber();

View File

@@ -1,425 +0,0 @@
<script lang="ts">
import { Button } from "$lib/components/ui/button";
import * as Dialog from "$lib/components/ui/dialog";
import { Input } from "$lib/components/ui/input";
import { Label } from "$lib/components/ui/label";
import { pedimentosApi, type Pedimento, type CreatePedimentoData, type UpdatePedimentoData } from "$lib/api/dashboard/a76/pedimentos";
let {
open = $bindable(false),
item = $bindable<Pedimento | null>(null),
onSuccess
}: {
open: boolean;
item?: Pedimento | null;
onSuccess?: () => void;
} = $props();
let formData = $state({
year: "",
customs_office: "",
license: "",
pedimento_number: "",
client_id: null as number | null,
operation_type: null as number | null,
pedimento_type: null as number | null,
pedimento_key: "",
regime: "",
status: "",
usd_value: null as number | null,
paid_price: null as number | null,
gross_weight: null as number | null,
exchange_rate: null as number | null
});
let loading = $state(false);
let error = $state<string | null>(null);
// Actualizar formData cuando item cambia
$effect(() => {
if (item) {
formData = {
year: item.year || "",
customs_office: item.customs_office || "",
license: item.license || "",
pedimento_number: item.pedimento_number || "",
client_id: item.client_id ?? null,
operation_type: item.operation_type ?? null,
pedimento_type: item.pedimento_type ?? null,
pedimento_key: item.pedimento_key || "",
regime: item.regime || "",
status: item.status || "",
usd_value: item.usd_value ?? null,
paid_price: item.paid_price ?? null,
gross_weight: item.gross_weight ?? null,
exchange_rate: item.exchange_rate ?? null
};
} else {
formData = {
year: "",
customs_office: "",
license: "",
pedimento_number: "",
client_id: null,
operation_type: null,
pedimento_type: null,
pedimento_key: "",
regime: "",
status: "",
usd_value: null,
paid_price: null,
gross_weight: null,
exchange_rate: null
};
}
});
const isEditing = $derived(!!item);
// Estados comunes
const statusOptions = [
{ value: "Activo", label: "Activo" },
{ value: "Pendiente", label: "Pendiente" },
{ value: "En Proceso", label: "En Proceso" },
{ value: "Completado", label: "Completado" },
{ value: "Cancelado", label: "Cancelado" }
];
async function handleSubmit(e: Event) {
e.preventDefault();
loading = true;
error = null;
try {
let response;
if (isEditing && item) {
const payload: UpdatePedimentoData = {
year: formData.year || null,
customs_office: formData.customs_office || null,
license: formData.license || null,
pedimento_number: formData.pedimento_number || null,
client_id: formData.client_id,
operation_type: formData.operation_type,
pedimento_type: formData.pedimento_type,
pedimento_key: formData.pedimento_key || null,
regime: formData.regime || null,
status: formData.status || null,
usd_value: formData.usd_value,
paid_price: formData.paid_price,
gross_weight: formData.gross_weight,
exchange_rate: formData.exchange_rate
};
response = await pedimentosApi.update(item.id, payload);
} else {
const payload: CreatePedimentoData = {
year: formData.year || null,
customs_office: formData.customs_office || null,
license: formData.license || null,
pedimento_number: formData.pedimento_number || null,
client_id: formData.client_id,
operation_type: formData.operation_type,
pedimento_type: formData.pedimento_type,
pedimento_key: formData.pedimento_key || null,
regime: formData.regime || null,
status: formData.status || null,
usd_value: formData.usd_value,
paid_price: formData.paid_price,
gross_weight: formData.gross_weight,
exchange_rate: formData.exchange_rate
};
response = await pedimentosApi.create(payload);
}
if (response.error) {
if (response.status === 401) {
error = 'Sesión expirada. Recargando página...';
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
error = response.error;
}
return;
}
// Éxito
open = false;
if (onSuccess) {
onSuccess();
}
} catch (e) {
error = e instanceof Error ? e.message : "Error al guardar";
console.error("Error saving:", e);
} finally {
loading = false;
}
}
function handleOpenChange(newOpen: boolean) {
if (!newOpen) {
// Limpiar form al cerrar
formData = {
year: "",
customs_office: "",
license: "",
pedimento_number: "",
client_id: null,
operation_type: null,
pedimento_type: null,
pedimento_key: "",
regime: "",
status: "",
usd_value: null,
paid_price: null,
gross_weight: null,
exchange_rate: null
};
error = null;
}
open = newOpen;
}
</script>
<Dialog.Root bind:open onOpenChange={handleOpenChange}>
<Dialog.Content class="sm:max-w-[700px] max-h-[90vh] overflow-y-auto">
<Dialog.Header>
<Dialog.Title>
{isEditing ? "Editar" : "Nuevo"} Pedimento
</Dialog.Title>
<Dialog.Description>
{isEditing
? "Modifica los datos del pedimento."
: "Completa los datos para crear un nuevo pedimento."}
</Dialog.Description>
</Dialog.Header>
<form onsubmit={handleSubmit} class="space-y-4">
{#if error}
<div class="rounded-lg border border-destructive bg-destructive/10 p-3 text-sm text-destructive">
{error}
</div>
{/if}
<!-- Información del Pedimento -->
<div class="space-y-4">
<h3 class="text-sm font-medium">Información del Pedimento</h3>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="year">Año</Label>
<Input
id="year"
bind:value={formData.year}
placeholder="22"
maxlength={2}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="customs_office">Aduana</Label>
<Input
id="customs_office"
bind:value={formData.customs_office}
placeholder="01"
maxlength={2}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="license">Patente</Label>
<Input
id="license"
bind:value={formData.license}
placeholder="3001"
maxlength={4}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="pedimento_number">Número de Pedimento</Label>
<Input
id="pedimento_number"
bind:value={formData.pedimento_number}
placeholder="0001234"
maxlength={7}
disabled={loading}
/>
</div>
</div>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="pedimento_key">Clave de Pedimento</Label>
<Input
id="pedimento_key"
bind:value={formData.pedimento_key}
placeholder="A1"
maxlength={2}
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="regime">Régimen</Label>
<Input
id="regime"
bind:value={formData.regime}
placeholder="IMD"
maxlength={3}
disabled={loading}
/>
</div>
</div>
</div>
<!-- Información del Cliente y Operación -->
<div class="space-y-4">
<h3 class="text-sm font-medium">Cliente y Operación</h3>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="client_id">ID del Cliente</Label>
<Input
id="client_id"
type="number"
bind:value={formData.client_id}
placeholder="123"
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="status">Estado</Label>
<select
id="status"
bind:value={formData.status}
disabled={loading}
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
>
<option value="">Seleccionar estado</option>
{#each statusOptions as option}
<option value={option.value}>{option.label}</option>
{/each}
</select>
</div>
<div class="space-y-2">
<Label for="operation_type">Tipo de Operación</Label>
<Input
id="operation_type"
type="number"
bind:value={formData.operation_type}
placeholder="1"
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="pedimento_type">Tipo de Pedimento</Label>
<Input
id="pedimento_type"
type="number"
bind:value={formData.pedimento_type}
placeholder="1"
disabled={loading}
/>
</div>
</div>
</div>
<!-- Información Financiera -->
<div class="space-y-4">
<h3 class="text-sm font-medium">Información Financiera</h3>
<div class="grid grid-cols-2 gap-4">
<div class="space-y-2">
<Label for="usd_value">Valor en USD</Label>
<Input
id="usd_value"
type="number"
step="0.01"
bind:value={formData.usd_value}
placeholder="1000.00"
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="paid_price">Precio Pagado</Label>
<Input
id="paid_price"
type="number"
step="0.01"
bind:value={formData.paid_price}
placeholder="1000.00"
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="exchange_rate">Tipo de Cambio</Label>
<Input
id="exchange_rate"
type="number"
step="0.00001"
bind:value={formData.exchange_rate}
placeholder="19.50000"
disabled={loading}
/>
</div>
<div class="space-y-2">
<Label for="gross_weight">Peso Bruto (kg)</Label>
<Input
id="gross_weight"
type="number"
step="0.001"
bind:value={formData.gross_weight}
placeholder="100.000"
disabled={loading}
/>
</div>
</div>
</div>
<Dialog.Footer>
<Button
type="button"
variant="outline"
onclick={() => (open = false)}
disabled={loading}
>
Cancelar
</Button>
<Button type="submit" disabled={loading}>
{#if loading}
<svg
class="mr-2 h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
{/if}
{isEditing ? "Guardar cambios" : "Crear pedimento"}
</Button>
</Dialog.Footer>
</form>
</Dialog.Content>
</Dialog.Root>

View File

@@ -11,8 +11,6 @@
onSuccess?: () => void;
} = $props();
let showEditDialog = $state(false);
let showDeleteDialog = $state(false);
let loading = $state(false);
let error = $state<string | null>(null);
@@ -54,12 +52,8 @@
}
function handleEdit() {
showEditDialog = true;
}
function handleView() {
// Navegar a la vista de detalles
window.location.href = `/dashboard/pedimentos/${item.id}`;
// Navegar a la página de edición
window.location.href = `/dashboard/pedimentos/edit/${item.id}`;
}
</script>
@@ -89,24 +83,6 @@
<DropdownMenu.Content align="end" class="w-[160px]">
<DropdownMenu.Label>Acciones</DropdownMenu.Label>
<DropdownMenu.Separator />
<DropdownMenu.Item onclick={handleView}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z" />
<circle cx="12" cy="12" r="3" />
</svg>
Ver detalles
</DropdownMenu.Item>
<DropdownMenu.Item onclick={handleEdit}>
<svg
xmlns="http://www.w3.org/2000/svg"
@@ -170,10 +146,3 @@
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
{#if showEditDialog}
<!-- Importar dinámicamente el componente de edición cuando se necesite -->
{#await import('./create-edit-dialog.svelte') then { default: CreateEditDialog }}
<CreateEditDialog bind:open={showEditDialog} bind:item onSuccess={onSuccess} />
{/await}
{/if}

View File

@@ -0,0 +1,122 @@
<script lang="ts">
import { onMount } from 'svelte';
import * as Card from '$lib/components/ui/card';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Skeleton } from '$lib/components/ui/skeleton';
import { pedimentoDatesApi, type PedimentoDates } from '$lib/api/dashboard/a76/pedimento-dates';
let {
pedimentoId,
formData = $bindable(),
exists = $bindable()
}: {
pedimentoId: number | null;
formData?: any;
exists?: boolean;
} = $props();
let loading = $state(true);
onMount(async () => {
await loadDates();
});
async function loadDates() {
// Si no hay pedimentoId (modo creación), inicializar vacío
if (!pedimentoId) {
formData = {
entry_date: '',
presentation_date: '',
payment_date: ''
};
exists = false;
loading = false;
return;
}
loading = true;
try {
const response = await pedimentoDatesApi.get(pedimentoId);
if (response.error) {
// No existe o hay error - inicializar vacío
exists = false;
formData = {
entry_date: '',
presentation_date: '',
payment_date: ''
};
} else if (response.data) {
exists = true;
formData = {
entry_date: response.data.entry_date ? response.data.entry_date.substring(0, 10) : '',
presentation_date: response.data.presentation_date ? response.data.presentation_date.substring(0, 10) : '',
payment_date: response.data.payment_date ? response.data.payment_date.substring(0, 10) : ''
};
}
} catch (e) {
console.error('Error loading dates:', e);
exists = false;
formData = {
entry_date: '',
presentation_date: '',
payment_date: ''
};
} finally {
loading = false;
}
}
</script>
<Card.Root>
<Card.Header>
<Card.Title>Fechas del Pedimento</Card.Title>
<Card.Description>
Gestiona las fechas importantes del pedimento
</Card.Description>
</Card.Header>
<Card.Content>
{#if loading}
<div class="space-y-4">
<Skeleton class="h-10 w-full" />
<Skeleton class="h-10 w-full" />
<Skeleton class="h-10 w-full" />
</div>
{:else}
<div class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<!-- Fecha de Entrada -->
<div class="space-y-2">
<Label for="entry_date">Fecha de Entrada</Label>
<Input
id="entry_date"
type="date"
bind:value={formData.entry_date}
/>
</div>
<!-- Fecha de Presentación -->
<div class="space-y-2">
<Label for="presentation_date">Fecha de Presentación</Label>
<Input
id="presentation_date"
type="date"
bind:value={formData.presentation_date}
/>
</div>
<!-- Fecha de Pago -->
<div class="space-y-2">
<Label for="payment_date">Fecha de Pago</Label>
<Input
id="payment_date"
type="date"
bind:value={formData.payment_date}
/>
</div>
</div>
</div>
{/if}
</Card.Content>
</Card.Root>

View File

@@ -0,0 +1,232 @@
<script lang="ts">
import * as Card from '$lib/components/ui/card';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import type { Pedimento } from '$lib/api/dashboard/a76/pedimentos';
let {
pedimento,
formData = $bindable()
}: {
pedimento: Pedimento | null;
formData?: any;
} = $props();
// Inicializar formData con los valores del pedimento (o vacío si es null)
if (!formData) {
formData = {
year: pedimento?.year || '',
customs_office: pedimento?.customs_office || '',
license: pedimento?.license || '',
pedimento_number: pedimento?.pedimento_number || '',
client_id: pedimento?.client_id ?? null,
operation_type: pedimento?.operation_type ?? null,
pedimento_type: pedimento?.pedimento_type ?? null,
pedimento_code: pedimento?.pedimento_code || '',
regime: pedimento?.regime || '',
status: pedimento?.status || '',
usd_value: pedimento?.usd_value ?? null,
paid_price: pedimento?.paid_price ?? null,
gross_weight: pedimento?.gross_weight ?? null,
exchange_rate: pedimento?.exchange_rate ?? null
};
}
const statusOptions = [
{ value: 'Activo', label: 'Activo' },
{ value: 'Pendiente', label: 'Pendiente' },
{ value: 'En Proceso', label: 'En Proceso' },
{ value: 'Completado', label: 'Completado' },
{ value: 'Cancelado', label: 'Cancelado' }
];
</script>
<Card.Root>
<Card.Header>
<Card.Title>Información General</Card.Title>
<Card.Description>
Edita los datos principales del pedimento
</Card.Description>
</Card.Header>
<Card.Content>
<div class="space-y-6">
<!-- Fila 1: Año (2), Aduana (2), Patente (4), Número de Pedimento (7) -->
<div class="flex items-end gap-2">
<!-- Año -->
<div class="space-y-2 w-16">
<Label for="year">Año</Label>
<Input
id="year"
bind:value={formData.year}
placeholder="23"
maxlength={2}
class="text-center"
/>
</div>
<!-- Separador -->
<div class="pb-2 text-2xl font-semibold text-muted-foreground">-</div>
<!-- Aduana -->
<div class="space-y-2 w-16">
<Label for="customs_office">Aduana</Label>
<Input
id="customs_office"
bind:value={formData.customs_office}
placeholder="01"
maxlength={2}
class="text-center"
/>
</div>
<!-- Separador -->
<div class="pb-2 text-2xl font-semibold text-muted-foreground">-</div>
<!-- Patente -->
<div class="space-y-2 w-24">
<Label for="license">Patente</Label>
<Input
id="license"
bind:value={formData.license}
placeholder="1234"
maxlength={4}
class="text-center"
/>
</div>
<!-- Separador -->
<div class="pb-2 text-2xl font-semibold text-muted-foreground">-</div>
<!-- Número de Pedimento -->
<div class="space-y-2 flex-1">
<Label for="pedimento_number">Número de Pedimento</Label>
<Input
id="pedimento_number"
bind:value={formData.pedimento_number}
placeholder="0000001"
maxlength={7}
class="text-left"
/>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- ID del Cliente -->
<div class="space-y-2">
<Label for="client_id">ID del Cliente</Label>
<Input
id="client_id"
type="number"
bind:value={formData.client_id}
placeholder="Ej: 123"
/>
</div>
<!-- Tipo de Operación -->
<div class="space-y-2">
<Label for="operation_type">Tipo de Operación</Label>
<Input
id="operation_type"
type="number"
bind:value={formData.operation_type}
placeholder="Ej: 1"
/>
</div>
<!-- Tipo de Pedimento -->
<div class="space-y-2">
<Label for="pedimento_type">Tipo de Pedimento</Label>
<Input
id="pedimento_type"
type="number"
bind:value={formData.pedimento_type}
placeholder="Ej: 1"
/>
</div>
<!-- Clave del Pedimento -->
<div class="space-y-2">
<Label for="pedimento_code">Clave del Pedimento</Label>
<Input
id="pedimento_code"
bind:value={formData.pedimento_code}
placeholder="Ej: A1"
/>
</div>
<!-- Régimen -->
<div class="space-y-2">
<Label for="regime">Régimen</Label>
<Input
id="regime"
bind:value={formData.regime}
placeholder="Ej: IMP"
/>
</div>
<!-- Estado -->
<div class="space-y-2">
<Label for="status">Estado</Label>
<select
id="status"
bind:value={formData.status}
class="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
>
<option value="">Seleccionar...</option>
{#each statusOptions as option}
<option value={option.value}>{option.label}</option>
{/each}
</select>
</div>
<!-- Valor USD -->
<div class="space-y-2">
<Label for="usd_value">Valor USD</Label>
<Input
id="usd_value"
type="number"
step="0.01"
bind:value={formData.usd_value}
placeholder="Ej: 1000.00"
/>
</div>
<!-- Precio Pagado -->
<div class="space-y-2">
<Label for="paid_price">Precio Pagado</Label>
<Input
id="paid_price"
type="number"
step="0.01"
bind:value={formData.paid_price}
placeholder="Ej: 1000.00"
/>
</div>
<!-- Peso Bruto -->
<div class="space-y-2">
<Label for="gross_weight">Peso Bruto (Kg)</Label>
<Input
id="gross_weight"
type="number"
step="0.01"
bind:value={formData.gross_weight}
placeholder="Ej: 100.00"
/>
</div>
<!-- Tipo de Cambio -->
<div class="space-y-2">
<Label for="exchange_rate">Tipo de Cambio</Label>
<Input
id="exchange_rate"
type="number"
step="0.0001"
bind:value={formData.exchange_rate}
placeholder="Ej: 17.5000"
/>
</div>
</div>
</div>
</Card.Content>
</Card.Root>

View File

@@ -0,0 +1,107 @@
<script lang="ts">
import { onMount } from 'svelte';
import * as Card from '$lib/components/ui/card';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Skeleton } from '$lib/components/ui/skeleton';
import { pedimentoPaymentsApi } from '$lib/api/dashboard/a76/pedimento-payments';
let {
pedimentoId,
formData = $bindable(),
exists = $bindable()
}: {
pedimentoId: number | null;
formData?: any;
exists?: boolean;
} = $props();
let loading = $state(true);
onMount(async () => {
await loadPayments();
});
async function loadPayments() {
// Si no hay pedimentoId (modo creación), inicializar vacío
if (!pedimentoId) {
formData = {
payment_form: '',
bank_identifier: ''
};
exists = false;
loading = false;
return;
}
loading = true;
try {
const response = await pedimentoPaymentsApi.get(pedimentoId);
if (response.error) {
// No existe o hay error - inicializar vacío
exists = false;
formData = {
payment_form: '',
bank_identifier: ''
};
} else if (response.data) {
exists = true;
formData = {
payment_form: response.data.payment_form || '',
bank_identifier: response.data.bank_identifier || ''
};
}
} catch (e) {
console.error('Error loading payments:', e);
exists = false;
formData = {
payment_form: '',
bank_identifier: ''
};
} finally {
loading = false;
}
}
</script>
<Card.Root>
<Card.Header>
<Card.Title>Información de Pagos</Card.Title>
<Card.Description>
Gestiona la información de pagos del pedimento
</Card.Description>
</Card.Header>
<Card.Content>
{#if loading}
<div class="space-y-4">
<Skeleton class="h-10 w-full" />
<Skeleton class="h-10 w-full" />
</div>
{:else}
<div class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Forma de Pago -->
<div class="space-y-2">
<Label for="payment_form">Forma de Pago</Label>
<Input
id="payment_form"
bind:value={formData.payment_form}
placeholder="Ej: Efectivo, Transferencia"
/>
</div>
<!-- Identificador Bancario -->
<div class="space-y-2">
<Label for="bank_identifier">Identificador Bancario</Label>
<Input
id="bank_identifier"
bind:value={formData.bank_identifier}
placeholder="Ej: 012345678901234567"
/>
</div>
</div>
</div>
{/if}
</Card.Content>
</Card.Root>

View File

@@ -0,0 +1,137 @@
<script lang="ts">
import { onMount } from 'svelte';
import * as Card from '$lib/components/ui/card';
import { Input } from '$lib/components/ui/input';
import { Label } from '$lib/components/ui/label';
import { Skeleton } from '$lib/components/ui/skeleton';
import { pedimentoTransportApi } from '$lib/api/dashboard/a76/pedimento-transport';
let {
pedimentoId,
formData = $bindable(),
exists = $bindable()
}: {
pedimentoId: number | null;
formData?: any;
exists?: boolean;
} = $props();
let loading = $state(true);
onMount(async () => {
await loadTransport();
});
async function loadTransport() {
// Si no hay pedimentoId (modo creación), inicializar vacío
if (!pedimentoId) {
formData = {
arrival_key: '',
arrival_data: '',
departure_key: '',
departure_data: ''
};
exists = false;
loading = false;
return;
}
loading = true;
try {
const response = await pedimentoTransportApi.get(pedimentoId);
if (response.error) {
// No existe o hay error - inicializar vacío
exists = false;
formData = {
arrival_key: '',
arrival_data: '',
departure_key: '',
departure_data: ''
};
} else if (response.data) {
exists = true;
formData = {
arrival_key: response.data.arrival_key || '',
arrival_data: response.data.arrival_data || '',
departure_key: response.data.departure_key || '',
departure_data: response.data.departure_data || ''
};
}
} catch (e) {
console.error('Error loading transport:', e);
exists = false;
formData = {
arrival_key: '',
arrival_data: '',
departure_key: '',
departure_data: ''
};
} finally {
loading = false;
}
}
</script>
<Card.Root>
<Card.Header>
<Card.Title>Medios de Transporte</Card.Title>
<Card.Description>
Gestiona la información de transporte del pedimento
</Card.Description>
</Card.Header>
<Card.Content>
{#if loading}
<div class="space-y-4">
<Skeleton class="h-10 w-full" />
<Skeleton class="h-10 w-full" />
<Skeleton class="h-10 w-full" />
<Skeleton class="h-10 w-full" />
</div>
{:else}
<div class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<!-- Clave de Llegada -->
<div class="space-y-2">
<Label for="arrival_key">Clave de Llegada</Label>
<Input
id="arrival_key"
bind:value={formData.arrival_key}
placeholder="Ej: 01"
/>
</div>
<!-- Datos de Llegada -->
<div class="space-y-2">
<Label for="arrival_data">Datos de Llegada</Label>
<Input
id="arrival_data"
bind:value={formData.arrival_data}
placeholder="Ej: Información adicional"
/>
</div>
<!-- Clave de Salida -->
<div class="space-y-2">
<Label for="departure_key">Clave de Salida</Label>
<Input
id="departure_key"
bind:value={formData.departure_key}
placeholder="Ej: 02"
/>
</div>
<!-- Datos de Salida -->
<div class="space-y-2">
<Label for="departure_data">Datos de Salida</Label>
<Input
id="departure_data"
bind:value={formData.departure_data}
placeholder="Ej: Información adicional"
/>
</div>
</div>
</div>
{/if}
</Card.Content>
</Card.Root>

View File

@@ -0,0 +1,94 @@
<script lang="ts">
import { onMount } from 'svelte';
import * as Card from '$lib/components/ui/card';
import { Label } from '$lib/components/ui/label';
import { Textarea } from '$lib/components/ui/textarea';
import { Skeleton } from '$lib/components/ui/skeleton';
import { pedimentoValidationApi } from '$lib/api/dashboard/a76/pedimento-validation';
let {
pedimentoId,
formData = $bindable(),
exists = $bindable()
}: {
pedimentoId: number | null;
formData?: any;
exists?: boolean;
} = $props();
let loading = $state(true);
onMount(async () => {
await loadValidation();
});
async function loadValidation() {
// Si no hay pedimentoId (modo creación), inicializar vacío
if (!pedimentoId) {
formData = {
document: ''
};
exists = false;
loading = false;
return;
}
loading = true;
try {
const response = await pedimentoValidationApi.get(pedimentoId);
if (response.error) {
// No existe o hay error - inicializar vacío
exists = false;
formData = {
document: ''
};
} else if (response.data) {
exists = true;
formData = {
document: response.data.document || ''
};
}
} catch (e) {
console.error('Error loading validation:', e);
exists = false;
formData = {
document: ''
};
} finally {
loading = false;
}
}
</script>
<Card.Root>
<Card.Header>
<Card.Title>Validación del Pedimento</Card.Title>
<Card.Description>
Gestiona la documentación de validación del pedimento
</Card.Description>
</Card.Header>
<Card.Content>
{#if loading}
<div class="space-y-4">
<Skeleton class="h-32 w-full" />
</div>
{:else}
<div class="space-y-6">
<div class="space-y-2">
<Label for="document">Documento de Validación</Label>
<Textarea
id="document"
bind:value={formData.document}
placeholder="Ingresa el documento o referencia de validación..."
rows={6}
class="resize-none"
/>
<p class="text-sm text-muted-foreground">
Ingresa el número de documento, referencia o cualquier información relevante para la validación del pedimento.
</p>
</div>
</div>
{/if}
</Card.Content>
</Card.Root>

View File

@@ -0,0 +1,23 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-description"
class={cn(
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
className
)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="alert-title"
class={cn("col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight", className)}
{...restProps}
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,44 @@
<script lang="ts" module>
import { type VariantProps, tv } from "tailwind-variants";
export const alertVariants = tv({
base: "relative grid w-full grid-cols-[0_1fr] items-start gap-y-0.5 rounded-lg border px-4 py-3 text-sm has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] has-[>svg]:gap-x-3 [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
variants: {
variant: {
default: "bg-card text-card-foreground",
destructive:
"text-destructive bg-card *:data-[slot=alert-description]:text-destructive/90 [&>svg]:text-current",
},
},
defaultVariants: {
variant: "default",
},
});
export type AlertVariant = VariantProps<typeof alertVariants>["variant"];
</script>
<script lang="ts">
import type { HTMLAttributes } from "svelte/elements";
import { cn, type WithElementRef } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
variant = "default",
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
variant?: AlertVariant;
} = $props();
</script>
<div
bind:this={ref}
data-slot="alert"
class={cn(alertVariants({ variant }), className)}
{...restProps}
role="alert"
>
{@render children?.()}
</div>

View File

@@ -0,0 +1,14 @@
import Root from "./alert.svelte";
import Description from "./alert-description.svelte";
import Title from "./alert-title.svelte";
export { alertVariants, type AlertVariant } from "./alert.svelte";
export {
Root,
Description,
Title,
//
Root as Alert,
Description as AlertDescription,
Title as AlertTitle,
};

View File

@@ -0,0 +1,16 @@
import Root from "./tabs.svelte";
import Content from "./tabs-content.svelte";
import List from "./tabs-list.svelte";
import Trigger from "./tabs-trigger.svelte";
export {
Root,
Content,
List,
Trigger,
//
Root as Tabs,
Content as TabsContent,
List as TabsList,
Trigger as TabsTrigger,
};

View File

@@ -0,0 +1,17 @@
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: TabsPrimitive.ContentProps = $props();
</script>
<TabsPrimitive.Content
bind:ref
data-slot="tabs-content"
class={cn("flex-1 outline-none", className)}
{...restProps}
/>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: TabsPrimitive.ListProps = $props();
</script>
<TabsPrimitive.List
bind:ref
data-slot="tabs-list"
class={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
className
)}
{...restProps}
/>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: TabsPrimitive.TriggerProps = $props();
</script>
<TabsPrimitive.Trigger
bind:ref
data-slot="tabs-trigger"
class={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 whitespace-nowrap rounded-md border border-transparent px-2 py-1 text-sm font-medium transition-[color,box-shadow] focus-visible:outline-1 focus-visible:ring-[3px] disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg:not([class*='size-'])]:size-4 [&_svg]:pointer-events-none [&_svg]:shrink-0",
className
)}
{...restProps}
/>

View File

@@ -0,0 +1,19 @@
<script lang="ts">
import { Tabs as TabsPrimitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
value = $bindable(""),
class: className,
...restProps
}: TabsPrimitive.RootProps = $props();
</script>
<TabsPrimitive.Root
bind:ref
bind:value
data-slot="tabs"
class={cn("flex flex-col gap-2", className)}
{...restProps}
/>

View File

@@ -77,7 +77,7 @@
Bienvenido a Anexo76
</h2>
<p class="mt-6 text-lg leading-8 text-gray-600">
Plataforma SaaS para gestión de comercio exterior conforme a Anexos 24, 31 y 22 del SAT.
Plataforma SaaS para gestión de comercio exterior conforme a Anexos 24, 30 y 22 del SAT.
Ideal para maquilas, empresas IMMEX y agentes aduanales.
</p>
<div class="mt-10 flex items-center justify-center gap-x-6">

View File

@@ -7,7 +7,7 @@
<div class="flex flex-col gap-2">
<h1 class="text-3xl font-bold tracking-tight">Bienvenido al Dashboard</h1>
<p class="text-muted-foreground">
Sistema de gestión de comercio exterior conforme a Anexos 24, 31 y 22 del SAT
Sistema de gestión de comercio exterior conforme a Anexos 24, 30 y 22 del SAT
</p>
</div>

View File

@@ -3,7 +3,6 @@
import { pedimentosApi, type Pedimento } from '$lib/api/dashboard/a76/pedimentos';
import DataTable from '$lib/components/dashboard/pedimentos/data-table.svelte';
import { createColumns } from '$lib/components/dashboard/pedimentos/columns.js';
import CreateEditDialog from '$lib/components/dashboard/pedimentos/create-edit-dialog.svelte';
import * as Card from '$lib/components/ui/card';
import { Button } from '$lib/components/ui/button';
import { Input } from '$lib/components/ui/input';
@@ -14,9 +13,6 @@
// Los datos iniciales vienen del servidor
let { data }: { data: PageData } = $props();
// Estado para el diálogo de crear
let showCreateDialog = $state(false);
// Estado para filtros
let filters = $state({
status: '',
@@ -163,7 +159,8 @@
}
function handleCreateClick() {
showCreateDialog = true;
// Redirigir a la página de creación (reusa la página de edición con ID "new")
window.location.href = '/dashboard/pedimentos/edit/new';
}
function handleSuccess() {
@@ -347,6 +344,3 @@
</Card.Content>
</Card.Root>
</div>
<!-- Diálogo de crear/editar -->
<CreateEditDialog bind:open={showCreateDialog} onSuccess={handleSuccess} />

View File

@@ -0,0 +1,72 @@
import type { PageServerLoad } from './$types';
import { error, redirect } from '@sveltejs/kit';
export const load: PageServerLoad = async ({ params, cookies }) => {
const accessToken = cookies.get('access_token');
if (!accessToken) {
throw redirect(302, '/login');
}
// Si el ID es "new", es una creación
if (params.id === 'new') {
return {
pedimento: null,
pedimentoId: null,
isCreate: true
};
}
const pedimentoId = parseInt(params.id);
if (isNaN(pedimentoId)) {
throw error(400, 'ID de pedimento inválido');
}
try {
// Configurar la URL de la API para SSR
let apiUrl = process.env.INTERNAL_API_URL;
if (!apiUrl) {
apiUrl = import.meta.env.VITE_API_URL;
// Reemplazar 'localhost' con 'backend' para llamadas desde el servidor (SSR)
apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend');
}
// Normalizar la URL
const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`;
// Cargar el pedimento desde el backend
const response = await fetch(
`${baseUrl}v1/a76/pedimentos/${pedimentoId}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json'
}
}
);
if (!response.ok) {
if (response.status === 404) {
throw error(404, 'Pedimento no encontrado');
}
if (response.status === 401) {
throw redirect(302, '/login');
}
throw error(response.status, 'Error al cargar el pedimento');
}
const pedimento = await response.json();
return {
pedimento,
pedimentoId,
isCreate: false
};
} catch (e) {
console.error('Error loading pedimento:', e);
if (e instanceof Error && 'status' in e) {
throw e;
}
throw error(500, 'Error al cargar el pedimento');
}
};

View File

@@ -0,0 +1,488 @@
<script lang="ts">
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import * as Card from '$lib/components/ui/card';
import * as Tabs from '$lib/components/ui/tabs';
import * as Alert from '$lib/components/ui/alert';
import { Button } from '$lib/components/ui/button';
import { Badge } from '$lib/components/ui/badge';
import { Separator } from '$lib/components/ui/separator';
import type { PageData } from './$types';
// Importar los componentes de cada pestaña (ahora sin botones de guardar propios)
import GeneralTabForm from '$lib/components/dashboard/pedimentos/edit/general-tab-form.svelte';
import DatesTabForm from '$lib/components/dashboard/pedimentos/edit/dates-tab-form.svelte';
import PaymentsTabForm from '$lib/components/dashboard/pedimentos/edit/payments-tab-form.svelte';
import TransportTabForm from '$lib/components/dashboard/pedimentos/edit/transport-tab-form.svelte';
import ValidationTabForm from '$lib/components/dashboard/pedimentos/edit/validation-tab-form.svelte';
// Importar las APIs
import { pedimentosApi, type CreatePedimentoData, type UpdatePedimentoData } from '$lib/api/dashboard/a76/pedimentos';
import { pedimentoDatesApi, type UpdatePedimentoDatesData, type CreatePedimentoDatesData } from '$lib/api/dashboard/a76/pedimento-dates';
import { pedimentoPaymentsApi, type UpdatePedimentoPaymentsData, type CreatePedimentoPaymentsData } from '$lib/api/dashboard/a76/pedimento-payments';
import { pedimentoTransportApi, type UpdatePedimentoTransportMeansData, type CreatePedimentoTransportMeansData } from '$lib/api/dashboard/a76/pedimento-transport';
import { pedimentoValidationApi, type UpdatePedimentoValidationData, type CreatePedimentoValidationData } from '$lib/api/dashboard/a76/pedimento-validation';
let { data }: { data: PageData } = $props();
let activeTab = $state('general');
let saving = $state(false);
let error = $state<string | null>(null);
let success = $state(false);
// ID del pedimento - se usa para crear sub-recursos después de crear el pedimento
let pedimentoId = $state<number | null>(data.pedimentoId);
// Referencias a los componentes de formulario para obtener sus datos
let generalFormData = $state<any>(null);
let datesFormData = $state<any>(null);
let paymentsFormData = $state<any>(null);
let transportFormData = $state<any>(null);
let validationFormData = $state<any>(null);
// Estados para saber si existen datos previos
let datesExists = $state(false);
let paymentsExists = $state(false);
let transportExists = $state(false);
let validationExists = $state(false);
function handleBack() {
goto('/dashboard/pedimentos');
}
function getStatusColor(status?: string | null): 'default' | 'destructive' | 'outline' | 'secondary' {
if (!status) return 'secondary';
const statusLower = status.toLowerCase();
if (statusLower.includes('activo') || statusLower.includes('completado')) {
return 'default';
} else if (statusLower.includes('pendiente') || statusLower.includes('proceso')) {
return 'secondary';
} else if (statusLower.includes('cancelado')) {
return 'destructive';
}
return 'outline';
}
async function handleSaveAll() {
saving = true;
error = null;
success = false;
try {
let pedimentoId = data.pedimentoId;
// 1. Crear o actualizar datos generales
if (generalFormData) {
const payload = {
year: generalFormData.year || null,
customs_office: generalFormData.customs_office || null,
license: generalFormData.license || null,
pedimento_number: generalFormData.pedimento_number || null,
client_id: generalFormData.client_id,
operation_type: generalFormData.operation_type,
pedimento_type: generalFormData.pedimento_type,
pedimento_code: generalFormData.pedimento_code || null,
regime: generalFormData.regime || null,
status: generalFormData.status || null,
usd_value: generalFormData.usd_value,
paid_price: generalFormData.paid_price,
gross_weight: generalFormData.gross_weight,
exchange_rate: generalFormData.exchange_rate
};
if (data.isCreate) {
// Crear nuevo pedimento
const response = await pedimentosApi.create(payload);
if (response.error) throw new Error(response.error);
if (!response.data?.id) throw new Error('No se recibió el ID del pedimento creado');
pedimentoId = response.data.id;
} else {
// Actualizar pedimento existente
const response = await pedimentosApi.update(pedimentoId!, payload);
if (response.error) throw new Error(response.error);
}
}
// 2. Guardar fechas
if (datesFormData && (datesFormData.entry_date || datesFormData.presentation_date || datesFormData.payment_date)) {
const payload = {
entry_date: datesFormData.entry_date || null,
presentation_date: datesFormData.presentation_date || null,
payment_date: datesFormData.payment_date || null
};
if (datesExists && !data.isCreate) {
const response = await pedimentoDatesApi.update(pedimentoId!, payload as UpdatePedimentoDatesData);
if (response.error && response.status !== 404) throw new Error(response.error);
} else {
const response = await pedimentoDatesApi.create(pedimentoId!, payload as CreatePedimentoDatesData);
if (response.error) throw new Error(response.error);
datesExists = true;
}
}
// 3. Guardar pagos
if (paymentsFormData && (paymentsFormData.payment_form || paymentsFormData.bank_identifier)) {
const payload = {
payment_form: paymentsFormData.payment_form || null,
bank_identifier: paymentsFormData.bank_identifier || null
};
if (paymentsExists && !data.isCreate) {
const response = await pedimentoPaymentsApi.update(pedimentoId!, payload as UpdatePedimentoPaymentsData);
if (response.error && response.status !== 404) throw new Error(response.error);
} else {
const response = await pedimentoPaymentsApi.create(pedimentoId!, payload as CreatePedimentoPaymentsData);
if (response.error) throw new Error(response.error);
paymentsExists = true;
}
}
// 4. Guardar transporte
if (transportFormData && (transportFormData.arrival_key || transportFormData.arrival_data || transportFormData.departure_key || transportFormData.departure_data)) {
const payload = {
arrival_key: transportFormData.arrival_key || null,
arrival_data: transportFormData.arrival_data || null,
departure_key: transportFormData.departure_key || null,
departure_data: transportFormData.departure_data || null
};
if (transportExists && !data.isCreate) {
const response = await pedimentoTransportApi.update(pedimentoId!, payload as UpdatePedimentoTransportMeansData);
if (response.error && response.status !== 404) throw new Error(response.error);
} else {
const response = await pedimentoTransportApi.create(pedimentoId!, payload as CreatePedimentoTransportMeansData);
if (response.error) throw new Error(response.error);
transportExists = true;
}
}
// 5. Guardar validación
if (validationFormData && validationFormData.document) {
const payload = {
document: validationFormData.document || null
};
if (validationExists && !data.isCreate) {
const response = await pedimentoValidationApi.update(pedimentoId!, payload as UpdatePedimentoValidationData);
if (response.error && response.status !== 404) throw new Error(response.error);
} else {
const response = await pedimentoValidationApi.create(pedimentoId!, payload as CreatePedimentoValidationData);
if (response.error) throw new Error(response.error);
validationExists = true;
}
}
// Si fue una creación, redirigir a la página de edición
if (data.isCreate && pedimentoId) {
await goto(`/dashboard/pedimentos/edit/${pedimentoId}`);
return;
}
success = true;
setTimeout(() => {
success = false;
}, 3000);
} catch (e) {
if (e instanceof Error && e.message.includes('401')) {
error = 'Sesión expirada. Recargando página...';
setTimeout(() => {
window.location.reload();
}, 1500);
} else {
error = e instanceof Error ? e.message : 'Error al guardar los cambios';
}
console.error('Error saving all:', e);
} finally {
saving = false;
}
}
</script>
<div class="space-y-6">
<!-- Header -->
<div class="flex items-center justify-between">
<div class="space-y-1">
<div class="flex items-center gap-3">
<Button variant="ghost" size="icon" onclick={handleBack}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m12 19-7-7 7-7" />
<path d="M19 12H5" />
</svg>
</Button>
<h1 class="text-3xl font-bold tracking-tight">
{#if data.isCreate}
Nuevo Pedimento
{:else}
Pedimento #{data.pedimento.id}
{/if}
</h1>
{#if data.isCreate}
<Badge variant="default">Nuevo</Badge>
{:else}
<Badge variant={getStatusColor(data.pedimento.status)}>
{data.pedimento.status || 'Sin estado'}
</Badge>
{/if}
</div>
<p class="text-muted-foreground">
{#if !data.isCreate && data.pedimento.pedimento_number}
Número: {data.pedimento.year}-{data.pedimento.customs_office}-{data.pedimento.license}-{data.pedimento.pedimento_number}
{:else}
Edita los detalles del pedimento
{/if}
</p>
</div>
</div>
<Separator />
<!-- Alertas globales -->
{#if error}
<Alert.Root variant="destructive">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="12" x2="12" y1="8" y2="12" />
<line x1="12" x2="12.01" y1="16" y2="16" />
</svg>
<Alert.Title>Error</Alert.Title>
<Alert.Description>{error}</Alert.Description>
</Alert.Root>
{/if}
{#if success}
<Alert.Root>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 22c5.523 0 10-4.477 10-10S17.523 2 12 2 2 6.477 2 12s4.477 10 10 10z" />
<path d="m9 12 2 2 4-4" />
</svg>
<Alert.Title>Éxito</Alert.Title>
<Alert.Description>Todos los cambios se guardaron correctamente</Alert.Description>
</Alert.Root>
{/if}
<!-- Tabs Navigation -->
<Tabs.Root bind:value={activeTab} class="space-y-4">
<Tabs.List class="grid w-full grid-cols-5">
<Tabs.Trigger value="general" disabled={false}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
<polyline points="14 2 14 8 20 8" />
</svg>
General
</Tabs.Trigger>
<Tabs.Trigger value="dates" disabled={false}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<rect width="18" height="18" x="3" y="4" rx="2" ry="2" />
<line x1="16" x2="16" y1="2" y2="6" />
<line x1="8" x2="8" y1="2" y2="6" />
<line x1="3" x2="21" y1="10" y2="10" />
</svg>
Fechas
</Tabs.Trigger>
<Tabs.Trigger value="payments" disabled={false}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<rect width="20" height="14" x="2" y="5" rx="2" />
<line x1="2" x2="22" y1="10" y2="10" />
</svg>
Pagos
</Tabs.Trigger>
<Tabs.Trigger value="transport" disabled={false}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M14 18V6a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v11a1 1 0 0 0 1 1h2" />
<path d="M15 18H9" />
<path d="M19 18h2a1 1 0 0 0 1-1v-3.65a1 1 0 0 0-.22-.624l-3.48-4.35A1 1 0 0 0 17.52 8H14" />
<circle cx="17" cy="18" r="2" />
<circle cx="7" cy="18" r="2" />
</svg>
Transporte
</Tabs.Trigger>
<Tabs.Trigger value="validation" disabled={false}>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10" />
<path d="m9 12 2 2 4-4" />
</svg>
Validación
</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="general">
<GeneralTabForm
pedimento={data.pedimento}
bind:formData={generalFormData}
/>
</Tabs.Content>
<Tabs.Content value="dates">
<DatesTabForm
pedimentoId={pedimentoId}
bind:formData={datesFormData}
bind:exists={datesExists}
/>
</Tabs.Content>
<Tabs.Content value="payments">
<PaymentsTabForm
pedimentoId={pedimentoId}
bind:formData={paymentsFormData}
bind:exists={paymentsExists}
/>
</Tabs.Content>
<Tabs.Content value="transport">
<TransportTabForm
pedimentoId={pedimentoId}
bind:formData={transportFormData}
bind:exists={transportExists}
/>
</Tabs.Content>
<Tabs.Content value="validation">
<ValidationTabForm
pedimentoId={pedimentoId}
bind:formData={validationFormData}
bind:exists={validationExists}
/>
</Tabs.Content>
</Tabs.Root>
<!-- Botón de guardar global -->
<Card.Root>
<Card.Content class="pt-6">
<div class="flex justify-end gap-3">
<Button type="button" variant="outline" onclick={handleBack} disabled={saving}>
Cancelar
</Button>
<Button onclick={handleSaveAll} disabled={saving}>
{#if saving}
<svg
class="mr-2 h-4 w-4 animate-spin"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
/>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
/>
</svg>
Guardando todos los cambios...
{:else}
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="mr-2"
>
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z" />
<polyline points="17 21 17 13 7 13 7 21" />
<polyline points="7 3 7 8 15 8" />
</svg>
Guardar Todos los Cambios
{/if}
</Button>
</div>
</Card.Content>
</Card.Root>
</div>

View File

@@ -21,7 +21,8 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-E
CREATE SCHEMA IF NOT EXISTS a76;
CREATE SCHEMA IF NOT EXISTS a22;
CREATE SCHEMA IF NOT EXISTS a24;
CREATE SCHEMA IF NOT EXISTS a31;
CREATE SCHEMA IF NOT EXISTS a30;
DROP SCHEMA IF EXISTS a31;
EOSQL
echo "✓ Extensiones y esquemas creados correctamente"