Merge pull request 'feauture-pedimentos' (#10) from feauture-pedimentos into development

Reviewed-on: ADUANASOFT/anexo76#10
This commit is contained in:
2025-11-07 16:11:10 +00:00
51 changed files with 2275 additions and 1182 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

@@ -1,11 +1,10 @@
"""
Modelos ORM para gestión de clientes y proveedores
"""
from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, SmallInteger, Numeric, ForeignKey
from sqlalchemy import Column, Integer, String, SmallInteger, Numeric, ForeignKey
from sqlalchemy.sql import func
from sqlalchemy.orm import relationship
from core.database import Base
import enum
class ClientProvider(Base):
@@ -17,7 +16,7 @@ class ClientProvider(Base):
# Primary key
client_id = Column(String(8), primary_key=True, nullable=False)
# Basic information
type_nat_foreign = Column(String(1), nullable=True) # TIPO NACIONAL/EXTRANJERO
name = Column(String(256), nullable=True)
@@ -37,15 +36,15 @@ class ClientProvider(Base):
tenant_id = Column(String, ForeignKey("a76.tenants.id"), nullable=False)
# Relationships
address = relationship("GClientProviderAddress", back_populates="client_provider", uselist=False, cascade="all, delete-orphan")
programs = relationship("GClientProviderPrograms", back_populates="client_provider", uselist=False, cascade="all, delete-orphan")
address = relationship("ClientProviderAddress", back_populates="client_provider", uselist=False, cascade="all, delete-orphan")
programs = relationship("ClientProviderPrograms", back_populates="client_provider", uselist=False, cascade="all, delete-orphan")
class GClientProviderAddress(Base):
class ClientProviderAddress(Base):
"""
Modelo para la tabla GClientesPro_Direccion - Dirección de clientes y proveedores
"""
__tablename__ = "gclient_provider_address"
__tablename__ = "client_provider_address"
__table_args__ = {"schema": "a76"}
# Primary key (foreign key)
@@ -71,11 +70,11 @@ class GClientProviderAddress(Base):
client_provider = relationship("ClientProvider", back_populates="address")
class GClientProviderPrograms(Base):
class ClientProviderPrograms(Base):
"""
Modelo para la tabla GClientesPro_Programas - Programas de clientes y proveedores
"""
__tablename__ = "gclient_provider_programs"
__tablename__ = "client_provider_programs"
__table_args__ = {"schema": "a76"}
# Primary key (foreign key)

View File

@@ -8,7 +8,7 @@ from fastapi import HTTPException
from typing import List, Optional
import logging
from .models import ClientProvider, GClientProviderAddress, GClientProviderPrograms
from .models import ClientProvider, ClientProviderAddress, ClientProviderPrograms
from .dto import (
ClientProviderCreateDTO,
ClientProviderUpdateDTO,
@@ -72,7 +72,7 @@ class ClientProviderService:
# Crear dirección si se proporciona
if client_data.address:
db_address = GClientProviderAddress(
db_address = ClientProviderAddress(
client_id=client_data.client_id,
**client_data.address.model_dump(exclude_unset=True)
)
@@ -80,7 +80,7 @@ class ClientProviderService:
# Crear programas si se proporciona
if client_data.programs:
db_programs = GClientProviderPrograms(
db_programs = ClientProviderPrograms(
client_id=client_data.client_id,
**client_data.programs.model_dump(exclude_unset=True)
)
@@ -207,7 +207,7 @@ class ClientProviderService:
# Actualizar dirección
if client_data.address:
address = self.db.query(GClientProviderAddress).filter(GClientProviderAddress.client_id == client_id).first()
address = self.db.query(ClientProviderAddress).filter(ClientProviderAddress.client_id == client_id).first()
if address:
# Actualizar dirección existente
address_data = client_data.address.model_dump(exclude_unset=True)
@@ -215,7 +215,7 @@ class ClientProviderService:
setattr(address, field, value)
else:
# Crear nueva dirección
address = GClientProviderAddress(
address = ClientProviderAddress(
client_id=client_id,
**client_data.address.model_dump(exclude_unset=True)
)
@@ -223,7 +223,7 @@ class ClientProviderService:
# Actualizar programas
if client_data.programs:
programs = self.db.query(GClientProviderPrograms).filter(GClientProviderPrograms.client_id == client_id).first()
programs = self.db.query(ClientProviderPrograms).filter(ClientProviderPrograms.client_id == client_id).first()
if programs:
# Actualizar programas existentes
programs_data = client_data.programs.model_dump(exclude_unset=True)
@@ -231,7 +231,7 @@ class ClientProviderService:
setattr(programs, field, value)
else:
# Crear nuevos programas
programs = GClientProviderPrograms(
programs = ClientProviderPrograms(
client_id=client_id,
**client_data.programs.model_dump(exclude_unset=True)
)

View File

@@ -7,11 +7,11 @@ from core.database import Base
import enum
class GCompany(Base):
class Company(Base):
"""
Modelo para la tabla GCompany - Información de la empresa
Modelo para la tabla Company - Información de la empresa
"""
__tablename__ = "gcompany"
__tablename__ = "company"
__table_args__ = {"schema": "a76"}
# Primary key

View File

@@ -7,7 +7,7 @@ from fastapi import HTTPException
from typing import List, Optional
import logging
from .models import GCompany
from .models import Company
from .dto import CompanyCreateDTO, CompanyUpdateDTO, CompanyResponseDTO
logger = logging.getLogger(__name__)
@@ -34,12 +34,12 @@ class CompanyService:
"""
try:
# Verificar que no exista ya una empresa (solo puede haber una por el consecutivo único)
existing = self.db.query(GCompany).filter(GCompany.consecutive == True).first()
existing = self.db.query(Company).filter(Company.consecutive == True).first()
if existing:
raise HTTPException(status_code=400, detail="A company is already registered in the system")
# Crear empresa
db_company = GCompany(
db_company = Company(
id=company_data.id,
consecutive=company_data.consecutive,
name=company_data.name,
@@ -98,7 +98,7 @@ class CompanyService:
Returns:
CompanyResponseDTO o None si no existe
"""
company = self.db.query(GCompany).filter(GCompany.consecutive == True).first()
company = self.db.query(Company).filter(Company.consecutive == True).first()
if not company:
return None
return CompanyResponseDTO.model_validate(company)
@@ -113,7 +113,7 @@ class CompanyService:
Returns:
CompanyResponseDTO o None si no existe
"""
company = self.db.query(GCompany).filter(GCompany.id == company_id).first()
company = self.db.query(Company).filter(Company.id == company_id).first()
if not company:
return None
return CompanyResponseDTO.model_validate(company)
@@ -129,7 +129,7 @@ class CompanyService:
Returns:
CompanyResponseDTO actualizada o None si no existe
"""
company = self.db.query(GCompany).filter(GCompany.id == company_id).first()
company = self.db.query(Company).filter(Company.id == company_id).first()
if not company:
return None
@@ -158,7 +158,7 @@ class CompanyService:
Returns:
True si se eliminó, False si no existe
"""
company = self.db.query(GCompany).filter(GCompany.id == company_id).first()
company = self.db.query(Company).filter(Company.id == company_id).first()
if not company:
return False
@@ -179,6 +179,6 @@ class CompanyService:
Returns:
True si existe una empresa, False en caso contrario
"""
return self.db.query(GCompany).filter(GCompany.consecutive == True).first() is not None
return self.db.query(Company).filter(Company.consecutive == True).first() is not None

View File

@@ -5,8 +5,6 @@ from datetime import datetime, time
class PedimentoDatesBase(BaseModel):
"""Base schema for Pedimento Dates"""
pedimento_id: int = Field(..., description="Pedimento ID")
tenant_id: int = Field(..., description="Tenant ID")
entry_date: Optional[datetime] = Field(None, description="Entry date")
pedimento_date: Optional[datetime] = Field(None, description="Pedimento date")
payment_date: Optional[datetime] = Field(None, description="Payment date")
@@ -45,6 +43,8 @@ class PedimentoDatesUpdate(BaseModel):
class PedimentoDatesResponse(PedimentoDatesBase):
"""Schema for Pedimento Dates response"""
id: int
pedimento_id: int = Field(..., description="Pedimento ID")
tenant_id: int = Field(..., description="Tenant ID")
created_at: datetime
model_config = ConfigDict(from_attributes=True)

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

@@ -1,6 +1,7 @@
"""
Routes for PedimentoDates CRUD operations
"""
import logging
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.orm import Session
from core.database import get_core_db
@@ -15,7 +16,7 @@ from ..dtos.pedimento_dates import (
router = APIRouter(prefix="/{pedimento_id}/dates")
logger = logging.getLogger(__name__)
@router.get("/", response_model=PedimentoDatesResponse)
async def get_dates(
@@ -36,8 +37,7 @@ async def get_dates(
@router.post("/", response_model=PedimentoDatesResponse, status_code=201)
async def create_dates(
pedimento_id: int,
async def create_dates(
data: PedimentoDatesCreate,
db: Session = Depends(get_core_db),
current_user: dict = Depends(get_current_user)
@@ -46,11 +46,7 @@ async def create_dates(
tenant_id = get_tenant_from_token(current_user)
if not tenant_id:
raise HTTPException(status_code=400, detail="Tenant ID not found in token")
# Ensure pedimento_id matches
if data.pedimento_id != pedimento_id:
raise HTTPException(status_code=400, detail="Pedimento ID mismatch")
dates = PedimentoDatesService.create(db, data, tenant_id)
return dates

View File

@@ -1,12 +1,14 @@
"""
Service layer for PedimentoDates CRUD operations
"""
import logging
from typing import Optional
from sqlalchemy.orm import Session
from ..models.pedimento_dates import PedimentoDates
from ..dtos.pedimento_dates import PedimentoDatesCreate, PedimentoDatesUpdate
logger = logging.getLogger(__name__)
class PedimentoDatesService:
"""Service class for PedimentoDates business logic"""

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

@@ -12,10 +12,10 @@ Se han actualizado todos los modelos en `api/v1/modules/a76/` para usar el schem
| Módulo | Tabla | Schema | Estado |
|--------|-------|---------|---------|
| **Company** | `gcompany` | `a76` | ✅ Actualizada |
| **Company** | `company` | `a76` | ✅ Actualizada |
| **Client & Provider** | `client_provider` | `a76` | ✅ Actualizada |
| **Client & Provider** | `gclient_provider_address` | `a76` | ✅ Actualizada |
| **Client & Provider** | `gclient_provider_programs` | `a76` | ✅ Actualizada |
| **Client & Provider** | `client_provider_address` | `a76` | ✅ Actualizada |
| **Client & Provider** | `client_provider_programs` | `a76` | ✅ Actualizada |
| **GParts** | `parts` | `a76` | ✅ Actualizada |
| **Class** | `classes` | `a76` | ✅ Actualizada |
| **Licenses** | `licenses` | `a76` | ✅ Ya estaba |
@@ -27,12 +27,12 @@ Se han actualizado todos los modelos en `api/v1/modules/a76/` para usar el schem
#### 1. Configuración de Schema
```python
# ANTES
class GCompany(Base):
__tablename__ = "gcompany"
class Company(Base):
__tablename__ = "company"
# DESPUÉS
class GCompany(Base):
__tablename__ = "gcompany"
class Company(Base):
__tablename__ = "company"
__table_args__ = {"schema": "a76"}
```
@@ -59,10 +59,10 @@ PostgreSQL Database
├── tenants
├── licenses
├── license_usage
├── gcompany
├── company
├── client_provider
├── gclient_provider_address
├── gclient_provider_programs
├── client_provider_address
├── client_provider_programs
├── parts
└── classes
```

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,67 @@
/**
* 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;
pedimento_date?: string | null;
payment_date?: string | null;
rectification_payment_date?: string | null;
extraction_date?: string | null;
submission_date?: string | null;
eucan_date?: string | null;
original_date?: string | null;
start_date?: string | null;
end_date?: string | null;
capture_date?: string | null;
capture_time?: string | null;
created_at: string;
}
export interface CreatePedimentoDatesData {
entry_date?: string | null;
pedimento_date?: string | null;
payment_date?: string | null;
rectification_payment_date?: string | null;
extraction_date?: string | null;
submission_date?: string | null;
eucan_date?: string | null;
original_date?: string | null;
start_date?: string | null;
end_date?: string | null;
capture_date?: string | null;
capture_time?: string | null;
}
export interface UpdatePedimentoDatesData {
entry_date?: string | null;
pedimento_date?: string | null;
payment_date?: string | null;
rectification_payment_date?: string | null;
extraction_date?: string | null;
submission_date?: string | null;
eucan_date?: string | null;
original_date?: string | null;
start_date?: string | null;
end_date?: string | null;
capture_date?: string | null;
capture_time?: 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,67 @@
/**
* API Client para Pagos de Pedimentos
*/
import { api } from '$lib/api';
export interface PedimentoPayments {
id: number;
pedimento_id: number;
tenant_id: number;
acknowledgment?: string | null;
operation_number?: string | null;
bank_code?: number | null;
cashier?: string | null;
date?: string | null;
time?: string | null;
shift?: string | null;
total_cash_paid?: number | null;
total_contributions?: number | null;
counter_payment?: number | null;
pece_code?: string | null;
payment_id?: number | null;
created_at: string;
}
export interface CreatePedimentoPaymentsData {
acknowledgment?: string | null;
operation_number?: string | null;
bank_code?: number | null;
cashier?: string | null;
date?: string | null;
time?: string | null;
shift?: string | null;
total_cash_paid?: number | null;
total_contributions?: number | null;
counter_payment?: number | null;
pece_code?: string | null;
payment_id?: number | null;
}
export interface UpdatePedimentoPaymentsData {
acknowledgment?: string | null;
operation_number?: string | null;
bank_code?: number | null;
cashier?: string | null;
date?: string | null;
time?: string | null;
shift?: string | null;
total_cash_paid?: number | null;
total_contributions?: number | null;
counter_payment?: number | null;
pece_code?: string | null;
payment_id?: number | 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,43 @@
/**
* API Client para Medios de Transporte de Pedimentos
*/
import { api } from '$lib/api';
export interface PedimentoTransportMeans {
id: number;
pedimento_id: number;
tenant_id: number;
destination?: number | null;
entry_exit?: string | null;
arrival?: string | null;
departure?: string | null;
created_at: string;
}
export interface CreatePedimentoTransportMeansData {
destination?: number | null;
entry_exit?: string | null;
arrival?: string | null;
departure?: string | null;
}
export interface UpdatePedimentoTransportMeansData {
destination?: number | null;
entry_exit?: string | null;
arrival?: string | null;
departure?: 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,55 @@
/**
* API Client para Validación de Pedimentos
*/
import { api } from '$lib/api';
export interface PedimentoValidation {
id: number;
pedimento_id: number;
tenant_id: number;
validator?: string | null;
validation_ack?: string | null;
pre_ack?: string | null;
line_signature?: string | null;
electronic_signature?: string | null;
certificate_number?: string | null;
validator_id?: number | null;
responsible_id?: number | null;
created_at: string;
}
export interface CreatePedimentoValidationData {
validator?: string | null;
validation_ack?: string | null;
pre_ack?: string | null;
line_signature?: string | null;
electronic_signature?: string | null;
certificate_number?: string | null;
validator_id?: number | null;
responsible_id?: number | null;
}
export interface UpdatePedimentoValidationData {
validator?: string | null;
validation_ack?: string | null;
pre_ack?: string | null;
line_signature?: string | null;
electronic_signature?: string | null;
certificate_number?: string | null;
validator_id?: number | null;
responsible_id?: number | 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;
@@ -67,15 +67,40 @@ function formatDate(date?: string | null): string {
function getStatusColor(status?: string | null): string {
if (!status) return 'bg-gray-100 text-gray-800';
const statusLower = status.toLowerCase();
if (statusLower.includes('activo') || statusLower.includes('completado')) {
const statusUpper = status.toUpperCase();
// Estados completados/exitosos - Verde
if (statusUpper === 'VALIDADO' || statusUpper === 'PAGADO' || statusUpper === 'CARTA CUPO') {
return 'bg-green-100 text-green-800';
} else if (statusLower.includes('pendiente') || statusLower.includes('proceso')) {
}
// Estados en espera/proceso - Amarillo
if (statusUpper.startsWith('ESPERA')) {
return 'bg-yellow-100 text-yellow-800';
} else if (statusLower.includes('cancelado') || statusLower.includes('rechazado')) {
}
// Estados con firma - Azul
if (statusUpper === 'CON FIRMA DE PREVIO') {
return 'bg-blue-100 text-blue-800';
}
// Estados modificables/editables - Índigo
if (statusUpper === 'MODIFICABLE') {
return 'bg-indigo-100 text-indigo-800';
}
// Estados de borrado - Naranja
if (statusUpper.includes('BORRADA')) {
return 'bg-orange-100 text-orange-800';
}
// Estados cancelados/desistidos - Rojo
if (statusUpper === 'DESISTIO') {
return 'bg-red-100 text-red-800';
}
return 'bg-blue-100 text-blue-800';
// Default - Gris
return 'bg-gray-100 text-gray-800';
}
export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
@@ -99,7 +124,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();
@@ -131,17 +156,20 @@ export function createColumns(onSuccess?: () => void): ColumnDef<Pedimento>[] {
cell: ({ row }) => {
const status = row.original.status;
const colorClass = getStatusColor(status);
const formattedStatus = status
? status.charAt(0).toUpperCase() + status.slice(1).toLowerCase()
: 'N/A';
const statusSnippet = createRawSnippet<[{ status?: string | null; colorClass: string }]>((getStatus) => {
const statusSnippet = createRawSnippet<[{ status: string; colorClass: string }]>((getStatus) => {
const { status, colorClass } = getStatus();
return {
render: () =>
`<span class="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ${colorClass}">
${status || 'N/A'}
${status}
</span>`
};
});
return renderSnippet(statusSnippet, { status, colorClass });
return renderSnippet(statusSnippet, { status: formattedStatus, colorClass });
}
},
{

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,248 @@
<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: '',
pedimento_date: '',
payment_date: '',
rectification_payment_date: '',
extraction_date: '',
submission_date: '',
eucan_date: '',
original_date: '',
start_date: '',
end_date: '',
capture_date: '',
capture_time: ''
};
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: '',
pedimento_date: '',
payment_date: '',
rectification_payment_date: '',
extraction_date: '',
submission_date: '',
eucan_date: '',
original_date: '',
start_date: '',
end_date: '',
capture_date: '',
capture_time: ''
};
} else if (response.data) {
exists = true;
formData = {
entry_date: response.data.entry_date ? response.data.entry_date.substring(0, 10) : '',
pedimento_date: response.data.pedimento_date ? response.data.pedimento_date.substring(0, 10) : '',
payment_date: response.data.payment_date ? response.data.payment_date.substring(0, 10) : '',
rectification_payment_date: response.data.rectification_payment_date ? response.data.rectification_payment_date.substring(0, 10) : '',
extraction_date: response.data.extraction_date ? response.data.extraction_date.substring(0, 10) : '',
submission_date: response.data.submission_date ? response.data.submission_date.substring(0, 10) : '',
eucan_date: response.data.eucan_date ? response.data.eucan_date.substring(0, 10) : '',
original_date: response.data.original_date ? response.data.original_date.substring(0, 10) : '',
start_date: response.data.start_date ? response.data.start_date.substring(0, 10) : '',
end_date: response.data.end_date ? response.data.end_date.substring(0, 10) : '',
capture_date: response.data.capture_date ? response.data.capture_date.substring(0, 10) : '',
capture_time: response.data.capture_time || ''
};
}
} catch (e) {
console.error('Error loading dates:', e);
exists = false;
formData = {
entry_date: '',
pedimento_date: '',
payment_date: '',
rectification_payment_date: '',
extraction_date: '',
submission_date: '',
eucan_date: '',
original_date: '',
start_date: '',
end_date: '',
capture_date: '',
capture_time: ''
};
} 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-2 lg: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="pedimento_date">Fecha de Presentación</Label>
<Input
id="pedimento_date"
type="date"
bind:value={formData.pedimento_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>
<!-- Fecha de Pago Rectificación -->
<div class="space-y-2">
<Label for="rectification_payment_date">Fecha de Pago Rectificación</Label>
<Input
id="rectification_payment_date"
type="date"
bind:value={formData.rectification_payment_date}
/>
</div>
<!-- Fecha de Extracción -->
<div class="space-y-2">
<Label for="extraction_date">Fecha de Extracción</Label>
<Input
id="extraction_date"
type="date"
bind:value={formData.extraction_date}
/>
</div>
<!-- Fecha de Presentación (Submission) -->
<div class="space-y-2">
<Label for="submission_date">Fecha de Envío</Label>
<Input
id="submission_date"
type="date"
bind:value={formData.submission_date}
/>
</div>
<!-- Fecha EUCAN -->
<div class="space-y-2">
<Label for="eucan_date">Fecha EUCAN</Label>
<Input
id="eucan_date"
type="date"
bind:value={formData.eucan_date}
/>
</div>
<!-- Fecha Original -->
<div class="space-y-2">
<Label for="original_date">Fecha Original</Label>
<Input
id="original_date"
type="date"
bind:value={formData.original_date}
/>
</div>
<!-- Fecha de Inicio -->
<div class="space-y-2">
<Label for="start_date">Fecha de Inicio</Label>
<Input
id="start_date"
type="date"
bind:value={formData.start_date}
/>
</div>
<!-- Fecha de Fin -->
<div class="space-y-2">
<Label for="end_date">Fecha de Fin</Label>
<Input
id="end_date"
type="date"
bind:value={formData.end_date}
/>
</div>
<!-- Fecha de Captura -->
<div class="space-y-2">
<Label for="capture_date">Fecha de Captura</Label>
<Input
id="capture_date"
type="date"
bind:value={formData.capture_date}
/>
</div>
<!-- Hora de Captura -->
<div class="space-y-2">
<Label for="capture_time">Hora de Captura</Label>
<Input
id="capture_time"
type="time"
bind:value={formData.capture_time}
/>
</div>
</div>
</div>
{/if}
</Card.Content>
</Card.Root>

View File

@@ -0,0 +1,242 @@
<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: 'MODIFICABLE', label: 'Modificable' },
{ value: 'ESPERA FIRMA PREVIO', label: 'Espera de firma previo' },
{ value: 'ESPERA VALIDACION', label: 'Espera validacion' },
{ value: 'VALIDADO', label: 'Validado' },
{ value: 'ESPERA BORRAR FIRMA PREVIO', label: 'Espera borrar firma previo' },
{ value: 'ESPERA BORRAR FIRMA VALIDACION', label: 'Espera borrar firma validacion ' },
{ value: 'ESPERA PAGO', label: 'Espera pago' },
{ value: 'CON FIRMA DE PREVIO', label: 'Con firma de previo' },
{ value: 'F DE PREVIO BORRADA', label: 'Forma de previo borrada' },
{ value: 'F VALIDACION BORRADA', label: 'Forma validacion borrada' },
{ value: 'PAGADO', label: 'Pagado' },
{ value: 'DESISTIO', label: 'Desistio' },
{ value: 'ESPERA CARTA CUPO', label: 'Espera carta cupo' },
{ value: 'CARTA CUPO', label: 'Carta cupo' },
{ value: 'ESPERA CANCELAR CARTA CUPO', label: 'Espera cancelar carta cupo' }
];
</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: IMD"
/>
</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,257 @@
<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 = {
acknowledgment: '',
operation_number: '',
bank_code: null,
cashier: '',
date: '',
time: '',
shift: '',
total_cash_paid: null,
total_contributions: null,
counter_payment: null,
pece_code: '',
payment_id: null
};
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 = {
acknowledgment: '',
operation_number: '',
bank_code: null,
cashier: '',
date: '',
time: '',
shift: '',
total_cash_paid: null,
total_contributions: null,
counter_payment: null,
pece_code: '',
payment_id: null
};
} else if (response.data) {
exists = true;
formData = {
acknowledgment: response.data.acknowledgment || '',
operation_number: response.data.operation_number || '',
bank_code: response.data.bank_code || null,
cashier: response.data.cashier || '',
date: response.data.date || '',
time: response.data.time || '',
shift: response.data.shift || '',
total_cash_paid: response.data.total_cash_paid || null,
total_contributions: response.data.total_contributions || null,
counter_payment: response.data.counter_payment || null,
pece_code: response.data.pece_code || '',
payment_id: response.data.payment_id || null
};
}
} catch (e) {
console.error('Error loading payments:', e);
exists = false;
formData = {
acknowledgment: '',
operation_number: '',
bank_code: null,
cashier: '',
date: '',
time: '',
shift: '',
total_cash_paid: null,
total_contributions: null,
counter_payment: null,
pece_code: '',
payment_id: null
};
} 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 lg:grid-cols-3 gap-4">
<!-- Acuse -->
<div class="space-y-2">
<Label for="acknowledgment">Acuse</Label>
<Input
id="acknowledgment"
bind:value={formData.acknowledgment}
placeholder="Máx. 20 caracteres"
maxlength={20}
/>
</div>
<!-- Número de Operación -->
<div class="space-y-2">
<Label for="operation_number">Número de Operación</Label>
<Input
id="operation_number"
bind:value={formData.operation_number}
placeholder="Máx. 14 caracteres"
maxlength={14}
/>
</div>
<!-- Código de Banco -->
<div class="space-y-2">
<Label for="bank_code">Código de Banco</Label>
<Input
id="bank_code"
type="number"
bind:value={formData.bank_code}
placeholder="Código numérico"
/>
</div>
<!-- Cajero -->
<div class="space-y-2">
<Label for="cashier">Cajero</Label>
<Input
id="cashier"
bind:value={formData.cashier}
placeholder="Máx. 2 caracteres"
maxlength={2}
/>
</div>
<!-- Fecha -->
<div class="space-y-2">
<Label for="date">Fecha</Label>
<Input
id="date"
type="date"
bind:value={formData.date}
/>
</div>
<!-- Hora -->
<div class="space-y-2">
<Label for="time">Hora</Label>
<Input
id="time"
type="time"
bind:value={formData.time}
/>
</div>
<!-- Turno -->
<div class="space-y-2">
<Label for="shift">Turno</Label>
<Input
id="shift"
bind:value={formData.shift}
placeholder="1 carácter"
maxlength={1}
/>
</div>
<!-- Total Efectivo Pagado -->
<div class="space-y-2">
<Label for="total_cash_paid">Total Efectivo Pagado</Label>
<Input
id="total_cash_paid"
type="number"
bind:value={formData.total_cash_paid}
placeholder="Monto en efectivo"
/>
</div>
<!-- Total Contribuciones -->
<div class="space-y-2">
<Label for="total_contributions">Total Contribuciones</Label>
<Input
id="total_contributions"
type="number"
bind:value={formData.total_contributions}
placeholder="Monto total"
/>
</div>
<!-- Pago en Ventanilla -->
<div class="space-y-2">
<Label for="counter_payment">Pago en Ventanilla</Label>
<Input
id="counter_payment"
type="number"
bind:value={formData.counter_payment}
placeholder="Monto"
/>
</div>
<!-- Código PECE -->
<div class="space-y-2">
<Label for="pece_code">Código PECE</Label>
<Input
id="pece_code"
bind:value={formData.pece_code}
placeholder="Máx. 5 caracteres"
maxlength={5}
/>
</div>
<!-- ID de Pago -->
<div class="space-y-2">
<Label for="payment_id">ID de Pago</Label>
<Input
id="payment_id"
type="number"
bind:value={formData.payment_id}
placeholder="Identificador"
/>
</div>
</div>
</div>
{/if}
</Card.Content>
</Card.Root>

View File

@@ -0,0 +1,141 @@
<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 = {
destination: null,
entry_exit: '',
arrival: '',
departure: ''
};
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 = {
destination: null,
entry_exit: '',
arrival: '',
departure: ''
};
} else if (response.data) {
exists = true;
formData = {
destination: response.data.destination || null,
entry_exit: response.data.entry_exit || '',
arrival: response.data.arrival || '',
departure: response.data.departure || ''
};
}
} catch (e) {
console.error('Error loading transport:', e);
exists = false;
formData = {
destination: null,
entry_exit: '',
arrival: '',
departure: ''
};
} 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">
<!-- Destino -->
<div class="space-y-2">
<Label for="destination">Destino</Label>
<Input
id="destination"
type="number"
bind:value={formData.destination}
placeholder="Código de destino"
/>
</div>
<!-- Entrada/Salida -->
<div class="space-y-2">
<Label for="entry_exit">Entrada/Salida</Label>
<Input
id="entry_exit"
bind:value={formData.entry_exit}
placeholder="Máx. 2 caracteres"
maxlength={2}
/>
</div>
<!-- Llegada -->
<div class="space-y-2">
<Label for="arrival">Llegada</Label>
<Input
id="arrival"
bind:value={formData.arrival}
placeholder="Máx. 2 caracteres"
maxlength={2}
/>
</div>
<!-- Salida -->
<div class="space-y-2">
<Label for="departure">Salida</Label>
<Input
id="departure"
bind:value={formData.departure}
placeholder="Máx. 2 caracteres"
maxlength={2}
/>
</div>
</div>
</div>
{/if}
</Card.Content>
</Card.Root>

View File

@@ -0,0 +1,207 @@
<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 { 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 = {
validator: '',
validation_ack: '',
pre_ack: '',
line_signature: '',
electronic_signature: '',
certificate_number: '',
validator_id: null,
responsible_id: null
};
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 = {
validator: '',
validation_ack: '',
pre_ack: '',
line_signature: '',
electronic_signature: '',
certificate_number: '',
validator_id: null,
responsible_id: null
};
} else if (response.data) {
exists = true;
formData = {
validator: response.data.validator || '',
validation_ack: response.data.validation_ack || '',
pre_ack: response.data.pre_ack || '',
line_signature: response.data.line_signature || '',
electronic_signature: response.data.electronic_signature || '',
certificate_number: response.data.certificate_number || '',
validator_id: response.data.validator_id || null,
responsible_id: response.data.responsible_id || null
};
}
} catch (e) {
console.error('Error loading validation:', e);
exists = false;
formData = {
validator: '',
validation_ack: '',
pre_ack: '',
line_signature: '',
electronic_signature: '',
certificate_number: '',
validator_id: null,
responsible_id: null
};
} 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-10 w-full" />
<Skeleton class="h-10 w-full" />
<Skeleton class="h-32 w-full" />
<Skeleton class="h-32 w-full" />
</div>
{:else}
<div class="space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
<!-- Validador -->
<div class="space-y-2">
<Label for="validator">Validador</Label>
<Input
id="validator"
bind:value={formData.validator}
placeholder="Máx. 3 caracteres"
maxlength={3}
/>
</div>
<!-- Acuse de Validación -->
<div class="space-y-2">
<Label for="validation_ack">Acuse de Validación</Label>
<Input
id="validation_ack"
bind:value={formData.validation_ack}
placeholder="Máx. 8 caracteres"
maxlength={8}
/>
</div>
<!-- Pre-acuse -->
<div class="space-y-2">
<Label for="pre_ack">Pre-acuse</Label>
<Input
id="pre_ack"
bind:value={formData.pre_ack}
placeholder="Máx. 8 caracteres"
maxlength={8}
/>
</div>
<!-- Número de Certificado -->
<div class="space-y-2">
<Label for="certificate_number">Número de Certificado</Label>
<Input
id="certificate_number"
bind:value={formData.certificate_number}
placeholder="Máx. 99 caracteres"
maxlength={99}
/>
</div>
<!-- ID de Validador -->
<div class="space-y-2">
<Label for="validator_id">ID de Validador</Label>
<Input
id="validator_id"
type="number"
bind:value={formData.validator_id}
placeholder="Identificador numérico"
/>
</div>
<!-- ID de Responsable -->
<div class="space-y-2">
<Label for="responsible_id">ID de Responsable</Label>
<Input
id="responsible_id"
type="number"
bind:value={formData.responsible_id}
placeholder="Identificador numérico"
/>
</div>
</div>
<!-- Firma de Línea -->
<div class="space-y-2">
<Label for="line_signature">Firma de Línea</Label>
<Input
id="line_signature"
bind:value={formData.line_signature}
placeholder="Máx. 50 caracteres"
maxlength={50}
/>
</div>
<!-- Firma Electrónica -->
<div class="space-y-2">
<Label for="electronic_signature">Firma Electrónica</Label>
<Textarea
id="electronic_signature"
bind:value={formData.electronic_signature}
placeholder="Ingresa la firma electrónica..."
rows={6}
class="resize-none font-mono text-sm"
maxlength={999}
/>
<p class="text-sm text-muted-foreground">
Firma electrónica del pedimento (máximo 999 caracteres).
</p>
</div>
</div>
{/if}
</Card.Content>
</Card.Root>

View File

@@ -26,7 +26,7 @@
<div
class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 items-center justify-center rounded-lg"
>
<activeTeam.logo class="size-4" />
<activeTeam.logo class="size-4 text-white" />
</div>
<div class="grid flex-1 text-left text-sm leading-tight">
<span class="truncate font-medium">

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

@@ -1,12 +1,45 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ cookies }) => {
export const load: PageServerLoad = async ({ cookies, fetch }) => {
const token = cookies.get('access_token');
// Si está autenticado, redirigir al dashboard
// Si hay token, validar que sea válido antes de redirigir
if (token) {
throw redirect(303, '/dashboard');
try {
// Configurar la URL de la API
let apiUrl = process.env.INTERNAL_API_URL;
if (!apiUrl) {
apiUrl = import.meta.env.VITE_API_URL;
apiUrl = apiUrl?.replace('localhost', 'backend').replace('127.0.0.1', 'backend');
}
const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`;
// Verificar si el token es válido
const response = await fetch(`${baseUrl}v1/auth/me`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
// Solo redirigir al dashboard si el token es válido
if (response.ok) {
throw redirect(303, '/dashboard');
} else {
// Token inválido, limpiar cookies y mostrar la página pública
cookies.delete('access_token', { path: '/' });
cookies.delete('refresh_token', { path: '/' });
}
} catch (error) {
// Si es un redirect, re-lanzarlo
if (error && typeof error === 'object' && 'status' in error && 'location' in error) {
throw error;
}
// Para otros errores, limpiar cookies y continuar
cookies.delete('access_token', { path: '/' });
cookies.delete('refresh_token', { path: '/' });
}
}
// Si no está autenticado, mostrar la página principal pública

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

@@ -21,6 +21,7 @@
<div class="flex items-center gap-2 px-4">
<Sidebar.Trigger class="-ml-1" />
<Separator orientation="vertical" class="mr-2 data-[orientation=vertical]:h-4" />
<!--
<Breadcrumb.Root>
<Breadcrumb.List>
<Breadcrumb.Item class="hidden md:block">
@@ -32,6 +33,7 @@
</Breadcrumb.Item>
</Breadcrumb.List>
</Breadcrumb.Root>
-->
</div>
</header>
<div class="flex flex-1 flex-col gap-4 p-4 pt-0">

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';
@@ -13,9 +12,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({
@@ -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() {
@@ -174,11 +171,21 @@
// Opciones de status para el filtro
const statusOptions = [
{ value: "", label: "Todos" },
{ value: "Activo", label: "Activo" },
{ value: "Pendiente", label: "Pendiente" },
{ value: "En Proceso", label: "En Proceso" },
{ value: "Completado", label: "Completado" },
{ value: "Cancelado", label: "Cancelado" }
{ value: 'MODIFICABLE', label: 'Modificable' },
{ value: 'ESPERA FIRMA PREVIO', label: 'Espera de firma previo' },
{ value: 'ESPERA VALIDACION', label: 'Espera validacion' },
{ value: 'VALIDADO', label: 'Validado' },
{ value: 'ESPERA BORRAR FIRMA PREVIO', label: 'Espera borrar firma previo' },
{ value: 'ESPERA BORRAR FIRMA VALIDACION', label: 'Espera borrar firma validacion ' },
{ value: 'ESPERA PAGO', label: 'Espera pago' },
{ value: 'CON FIRMA DE PREVIO', label: 'Con firma de previo' },
{ value: 'F DE PREVIO BORRADA', label: 'Forma de previo borrada' },
{ value: 'F VALIDACION BORRADA', label: 'Forma validacion borrada' },
{ value: 'PAGADO', label: 'Pagado' },
{ value: 'DESISTIO', label: 'Desistio' },
{ value: 'ESPERA CARTA CUPO', label: 'Espera carta cupo' },
{ value: 'CARTA CUPO', label: 'Carta cupo' },
{ value: 'ESPERA CANCELAR CARTA CUPO', label: 'Espera cancelar carta cupo' }
];
// Crear columnas con el callback onSuccess
@@ -347,6 +354,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.pedimento_date || datesFormData.payment_date)) {
const payload = {
entry_date: datesFormData.entry_date || null,
pedimento_date: datesFormData.pedimento_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

@@ -2,20 +2,15 @@ import { redirect, fail } from '@sveltejs/kit';
import type { Actions, PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ cookies, url }) => {
const token = cookies.get('access_token');
// Si hay un parámetro 'logout' en la URL, limpiar las cookies
if (url.searchParams.has('logout')) {
cookies.delete('access_token', { path: '/' });
cookies.delete('refresh_token', { path: '/' });
return {};
}
// Si está autenticado, redirigir al dashboard
if (token) {
throw redirect(303, '/dashboard');
}
// Si no está autenticado, permitir acceso al login
// Permitir acceso al login sin redirigir automáticamente
// Esto evita bucles de redirección cuando el token existe pero puede estar expirado
return {};
};

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"