diff --git a/backend/api/v1/modules/a24/fa/fa_item_lines/models.py b/backend/api/v1/modules/a24/fa/fa_item_lines/models.py index 75496b34..a0187a75 100644 --- a/backend/api/v1/modules/a24/fa/fa_item_lines/models.py +++ b/backend/api/v1/modules/a24/fa/fa_item_lines/models.py @@ -75,7 +75,9 @@ class FaLineItem(Base, TenantScopedMixin, TimestampMixin): omit_annex31: Mapped[Optional[bool]] = mapped_column(Boolean) # OMITITENANEXO31 # --- RELACIÓN --- - master_info: Mapped["LineItem"] = relationship("LineItem", back_populates="fa_data") + master_info: Mapped["LineItem"] = relationship( + "api.v1.modules.a76.items.models.LineItem", back_populates="fa_data" + ) def __repr__(self) -> str: return f"" diff --git a/backend/api/v1/modules/a76/audit_log/events.py b/backend/api/v1/modules/a76/audit_log/events.py index 478ef95c..53a72a0b 100644 --- a/backend/api/v1/modules/a76/audit_log/events.py +++ b/backend/api/v1/modules/a76/audit_log/events.py @@ -1,11 +1,14 @@ """ Audit Log Events """ + from sqlalchemy import event, inspect from sqlalchemy.orm import Session from .services.service import AuditService +from .utils.serialization import serialize_for_json from core.context import get_user_context + def register_audit_listeners(models_to_audit): """ Register SQLAlchemy listeners for given models @@ -15,16 +18,23 @@ def register_audit_listeners(models_to_audit): event.listen(model, "after_update", after_update_listener) event.listen(model, "after_delete", after_delete_listener) + def _get_current_username(): try: context = get_user_context() if context: # Token usually has 'preferred_username' or 'name' or 'sub' - return context.get("preferred_username") or context.get("email") or context.get("sub") or "System" + return ( + context.get("preferred_username") + or context.get("email") + or context.get("sub") + or "System" + ) except: pass return "System" + def after_insert_listener(mapper, connection, target): """ Listener for INSERT operations @@ -33,7 +43,8 @@ def after_insert_listener(mapper, connection, target): record_data = {c.name: getattr(target, c.name) for c in mapper.columns} username = _get_current_username() company_id = getattr(target, "company_id", None) - + tenant_id = getattr(target, "tenant_id", None) + # Create a session bound to the connection session = Session(bind=connection) try: @@ -44,24 +55,26 @@ def after_insert_listener(mapper, connection, target): record_data=record_data, username=username, record_id=str(getattr(target, "id", "")), - company_id=company_id + company_id=company_id, + tenant_id=tenant_id, ) except Exception as e: print(f"Error logging insert: {e}") finally: session.close() + def after_update_listener(mapper, connection, target): """ Listener for UPDATE operations """ table_name = target.__tablename__ - + state = inspect(target) changes = {} old_values = {} new_values = {} - + for attr in state.attrs: hist = attr.history if hist.has_changes(): @@ -74,6 +87,8 @@ def after_update_listener(mapper, connection, target): record_data = {c.name: getattr(target, c.name) for c in mapper.columns} username = _get_current_username() + company_id = getattr(target, "company_id", None) + tenant_id = getattr(target, "tenant_id", None) session = Session(bind=connection) try: @@ -84,13 +99,16 @@ def after_update_listener(mapper, connection, target): record_data=record_data, username=username, record_id=str(getattr(target, "id", "")), - old_values=old_values, - new_values=new_values + old_values=serialize_for_json(old_values), + new_values=serialize_for_json(new_values), + company_id=company_id, + tenant_id=tenant_id, ) except Exception as e: - print(f"Error logging update: {e}") + print(f"Error logging update: {e}") finally: - session.close() + session.close() + def after_delete_listener(mapper, connection, target): """ @@ -99,7 +117,9 @@ def after_delete_listener(mapper, connection, target): table_name = target.__tablename__ record_data = {c.name: getattr(target, c.name) for c in mapper.columns} username = _get_current_username() - + company_id = getattr(target, "company_id", None) + tenant_id = getattr(target, "tenant_id", None) + session = Session(bind=connection) try: AuditService.log_crud_operation( @@ -108,9 +128,11 @@ def after_delete_listener(mapper, connection, target): operation_type="DELETE", record_data=record_data, username=username, - record_id=str(getattr(target, "id", "")) + record_id=str(getattr(target, "id", "")), + company_id=company_id, + tenant_id=tenant_id, ) except Exception as e: - print(f"Error logging delete: {e}") + print(f"Error logging delete: {e}") finally: - session.close() + session.close() diff --git a/backend/api/v1/modules/a76/audit_log/models.py b/backend/api/v1/modules/a76/audit_log/models.py index c47900f5..499ea453 100644 --- a/backend/api/v1/modules/a76/audit_log/models.py +++ b/backend/api/v1/modules/a76/audit_log/models.py @@ -4,10 +4,18 @@ Audit Log Models from sqlalchemy import Column, Integer, String, Date, Time, DateTime, Text, Index, func from sqlalchemy.dialects.postgresql import JSONB, ARRAY +from api.v1.common.base_models import TenantScopedMixin, TimestampMixin from core.database import Base -class AuditLog(Base): +class AuditLog(Base, TenantScopedMixin, TimestampMixin): __tablename__ = "audit_logs" + __table_args__ = ( + Index('idx_audit_username_date', 'username', 'date'), + Index('idx_audit_procedure_date', 'procedure', 'date'), + Index('idx_audit_system_timestamp', 'system', 'timestamp'), + Index('idx_audit_table_record', 'table_name', 'record_id'), + {"schema": "a76"} # Use the a76 schema for audit logs + ) # Primary Key spec_id = Column(Integer, primary_key=True, autoincrement=True) @@ -23,8 +31,6 @@ class AuditLog(Base): # Technical Columns timestamp = Column(DateTime(timezone=True), nullable=False, index=True) # Combined for queries system = Column(String(20), nullable=False, index=True, default="SCAF") - company_id = Column(Integer, nullable=True, index=True) - tenant_id = Column(Integer, nullable=True, index=True) # Traceability table_name = Column(String(100), nullable=True, index=True) @@ -42,15 +48,4 @@ class AuditLog(Base): endpoint = Column(String(500), nullable=True) request_method = Column(String(10), nullable=True) session_id = Column(String(50), nullable=True, index=True) - execution_time_ms = Column(Integer, nullable=True) - - # Metadata - created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) - - # Composite Indexes for common filters - __table_args__ = ( - Index('idx_audit_username_date', 'username', 'date'), - Index('idx_audit_procedure_date', 'procedure', 'date'), - Index('idx_audit_system_timestamp', 'system', 'timestamp'), - Index('idx_audit_table_record', 'table_name', 'record_id'), - ) + execution_time_ms = Column(Integer, nullable=True) diff --git a/backend/api/v1/modules/a76/audit_log/services/service.py b/backend/api/v1/modules/a76/audit_log/services/service.py index 9fdf4a6b..dd1b18c1 100644 --- a/backend/api/v1/modules/a76/audit_log/services/service.py +++ b/backend/api/v1/modules/a76/audit_log/services/service.py @@ -86,7 +86,8 @@ class AuditService: # Context ip_address: Optional[str] = None, user_agent: Optional[str] = None, - company_id: Optional[int] = None + company_id: Optional[int] = None, + tenant_id: Optional[int] = None ): """ High-level wrapper to log CRUD operations automatically mapping to Legacy format @@ -149,7 +150,8 @@ class AuditService: changed_fields=changed_fields, ip_address=ip_address, user_agent=user_agent, - company_id=company_id + company_id=company_id, + tenant_id=tenant_id ) @staticmethod diff --git a/backend/api/v1/modules/a76/audit_log/utils/__init__.py b/backend/api/v1/modules/a76/audit_log/utils/__init__.py new file mode 100644 index 00000000..57f53044 --- /dev/null +++ b/backend/api/v1/modules/a76/audit_log/utils/__init__.py @@ -0,0 +1,7 @@ +""" +Audit Log Utilities +""" + +from .serialization import serialize_for_json + +__all__ = ["serialize_for_json"] diff --git a/backend/api/v1/modules/a76/audit_log/utils/serialization.py b/backend/api/v1/modules/a76/audit_log/utils/serialization.py new file mode 100644 index 00000000..604de7b0 --- /dev/null +++ b/backend/api/v1/modules/a76/audit_log/utils/serialization.py @@ -0,0 +1,44 @@ +""" +Serialization utilities for audit logs +""" + +from datetime import date, datetime, time +from decimal import Decimal +from uuid import UUID +from typing import Any, Dict + + +def serialize_value(value: Any) -> Any: + """ + Convert a Python value to a JSON-serializable type + """ + if value is None: + return None + elif isinstance(value, (date, datetime)): + return value.isoformat() + elif isinstance(value, time): + return value.isoformat() + elif isinstance(value, Decimal): + return float(value) + elif isinstance(value, UUID): + return str(value) + elif isinstance(value, bytes): + return value.decode("utf-8", errors="replace") + elif isinstance(value, (list, tuple)): + return [serialize_value(item) for item in value] + elif isinstance(value, dict): + return {key: serialize_value(val) for key, val in value.items()} + else: + # For any other type, try to return as-is (str, int, float, bool, None) + # If it fails JSON serialization later, at least we tried + return value + + +def serialize_for_json(data: Dict[str, Any]) -> Dict[str, Any]: + """ + Recursively serialize a dictionary for JSON storage + """ + if not data: + return data + + return {key: serialize_value(value) for key, value in data.items()} diff --git a/backend/api/v1/modules/a76/items/models.py b/backend/api/v1/modules/a76/items/models.py index ebc92a2e..5eaa1180 100644 --- a/backend/api/v1/modules/a76/items/models.py +++ b/backend/api/v1/modules/a76/items/models.py @@ -6,14 +6,7 @@ SQLAlchemy v2 - Annex 24 Compliance from typing import Optional, TYPE_CHECKING from core.database import Base from decimal import Decimal -from sqlalchemy import ( - Boolean, - String, - Integer, - Numeric, - SmallInteger, - ForeignKey -) +from sqlalchemy import Boolean, String, Integer, Numeric, SmallInteger, ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship from api.v1.common.base_models import TenantScopedMixin, TimestampMixin from core.database import Base @@ -21,7 +14,7 @@ from core.database import Base from api.v1.modules.a76.classes.models import Class from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasure -if TYPE_CHECKING: +if TYPE_CHECKING: from .line_financials.models import LineFinancial from .line_quantities.models import LineQuantity from .line_customs.models import LineCustom @@ -41,17 +34,20 @@ class LineItem(Base, TenantScopedMixin, TimestampMixin): Unified item header table for all import/export operations Consolidates headers from both SCAF and SCAII systems """ + __tablename__ = "item_lines" __table_args__ = { "schema": "a76", } id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) - invoice_id: Mapped[int] = mapped_column(ForeignKey("a76.invoice_header.id")) # CONSECUTIVO + invoice_id: Mapped[int] = mapped_column( + ForeignKey("a76.invoice_header.id") + ) # CONSECUTIVO line_number: Mapped[int] = mapped_column(Integer) # LINEAIMPO/LINEAEXPO/LINEA - # Part identification + # Part identification part_number_id: Mapped[Optional[int]] = mapped_column( Integer, ForeignKey("a76.parts.id") ) # NUMPARTE @@ -170,30 +166,28 @@ class LineItem(Base, TenantScopedMixin, TimestampMixin): pallet2: Mapped[Optional[int]] = mapped_column(SmallInteger) # PALLET2 # Wildcard field - wildcard_field: Mapped[Optional[str]] = mapped_column(String(100)) # CAMPOCOMODIN + wildcard_field: Mapped[Optional[str]] = mapped_column(String(100)) # CAMPOCOMODIN # Item references - reference_number: Mapped[Optional[str]] = mapped_column( - String(20)) # NUMREFERENCIA - order: Mapped[Optional[str]] = mapped_column( - String(50)) # ORDENCOMPRA / ORDENVENTA + reference_number: Mapped[Optional[str]] = mapped_column(String(20)) # NUMREFERENCIA + order: Mapped[Optional[str]] = mapped_column(String(50)) # ORDENCOMPRA / ORDENVENTA guide_number: Mapped[Optional[str]] = mapped_column( - String(50)) # NUMEROGUIA/NUMERODEGUIA + String(50) + ) # NUMEROGUIA/NUMERODEGUIA # Dates depreciation_date: Mapped[Optional[int]] = mapped_column( - Integer) # FECHADEPRECIACION + Integer + ) # FECHADEPRECIACION # Administrative fields - rectification: Mapped[Optional[bool]] = mapped_column( - Boolean) # RECTIFICACION + rectification: Mapped[Optional[bool]] = mapped_column(Boolean) # RECTIFICACION warehouse: Mapped[Optional[str]] = mapped_column(String(30)) # BODEGA - location: Mapped[Optional[str]] = mapped_column( - String(200)) # LOCALIZACION - + location: Mapped[Optional[str]] = mapped_column(String(200)) # LOCALIZACION + invoice: Mapped["InvoiceHeader"] = relationship("InvoiceHeader") - - # Relationships + + # Relationships financial: Mapped[Optional["LineFinancial"]] = relationship( back_populates="line", cascade="all, delete-orphan", uselist=False ) @@ -220,7 +214,7 @@ class LineItem(Base, TenantScopedMixin, TimestampMixin): viewonly=True, ) fa_data: Mapped[Optional["FaLineItem"]] = relationship( - "FaLineItem", + "api.v1.modules.a24.fa.fa_item_lines.models.FaLineItem", back_populates="master_info", cascade="all, delete-orphan", uselist=False, @@ -231,6 +225,7 @@ class LineItem(Base, TenantScopedMixin, TimestampMixin): viewonly=True, ) + # ============================================================================ # SUPPORTING TABLES # ============================================================================ @@ -241,6 +236,7 @@ class PackingList(Base, TenantScopedMixin, TimestampMixin): Packing list items From: SPartidasPackingList """ + __tablename__ = "packing_lists" __table_args__ = { "schema": "a76", @@ -249,7 +245,8 @@ class PackingList(Base, TenantScopedMixin, TimestampMixin): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) item_line_id: Mapped[int] = mapped_column(Integer) # LINEA packing_list_number: Mapped[Optional[str]] = mapped_column( - String(100)) # NUMPACKINGLIST + String(100) + ) # NUMPACKINGLIST class CTMReceipt(Base, TenantScopedMixin, TimestampMixin): @@ -257,6 +254,7 @@ class CTMReceipt(Base, TenantScopedMixin, TimestampMixin): CTM Receipt lines (temporary manufacturing) From: SPartidasReciboCTM """ + __tablename__ = "ctm_receipts" __table_args__ = { "schema": "a76", @@ -264,11 +262,11 @@ class CTMReceipt(Base, TenantScopedMixin, TimestampMixin): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) receipt_line: Mapped[int] = mapped_column( - ForeignKey("a76.item_lines.id")) # LINEARECIBO + ForeignKey("a76.item_lines.id") + ) # LINEARECIBO option: Mapped[Optional[str]] = mapped_column(String(3)) # OPCION - exit_invoice: Mapped[Optional[str]] = mapped_column( - String(19)) # FACTURASALIDA + exit_invoice: Mapped[Optional[str]] = mapped_column(String(19)) # FACTURASALIDA class SubassemblyEntry(Base, TenantScopedMixin, TimestampMixin): @@ -276,6 +274,7 @@ class SubassemblyEntry(Base, TenantScopedMixin, TimestampMixin): Subassembly/Submanufacturing Entry lines From: SPartidasEntradaSM """ + __tablename__ = "subassembly_entries" __table_args__ = { "schema": "a76", @@ -283,10 +282,10 @@ class SubassemblyEntry(Base, TenantScopedMixin, TimestampMixin): id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) remission_line: Mapped[int] = mapped_column(Integer) # LINEAREMISION - exit_invoice: Mapped[Optional[str]] = mapped_column( -String(15)) # FACTURASALIDA + exit_invoice: Mapped[Optional[str]] = mapped_column(String(15)) # FACTURASALIDA exit_line: Mapped[Optional[int]] = mapped_column(Integer) # LINEASALIDA + # ============================================================================ # INDEXES AND CONSTRAINTS # ============================================================================ diff --git a/scripts/init_first_time.sh b/scripts/init_first_time.sh index e9416f4a..1137af42 100755 --- a/scripts/init_first_time.sh +++ b/scripts/init_first_time.sh @@ -12,6 +12,11 @@ # 6. Relación usuario-tenant en tabla user_tenants # 7. Actualización del tenant_id del usuario con el valor real # 8. Licencia Enterprise para el tenant (ilimitada, 1 año de vigencia) +# 9. [OPCIONAL] Datos iniciales de ejemplo si se pasa --seed-data +# +# Uso: +# ./init_first_time.sh # Solo configuración básica +# ./init_first_time.sh --seed-data # Configuración + datos de ejemplo # # Requisitos: # - Keycloak corriendo en http://localhost:8080 @@ -28,6 +33,22 @@ set -euo pipefail # Modo strict: exit on error, undefined vars, pipe failures # Trap para cleanup en caso de error trap 'echo -e "\n${RED}✗ Error en línea $LINENO. Script abortado.${NC}" >&2' ERR +# Parsear argumentos +SEED_DATA=false +while [[ $# -gt 0 ]]; do + case $1 in + --seed-data) + SEED_DATA=true + shift + ;; + *) + echo "Uso: $0 [--seed-data]" + echo " --seed-data: Carga datos de ejemplo en las tablas" + exit 1 + ;; + esac +done + # Colores para output RED='\033[0;31m' GREEN='\033[0;32m' @@ -112,6 +133,171 @@ create_tenant_mapper() { fi } +############################################################################### +# Funciones de seed data +############################################################################### + +# Insertar datos de ejemplo para customs_brokers +seed_customs_brokers() { + echo " → Insertando customs brokers..." + exec_pg_sql " + INSERT INTO a76.customs_brokers (tenant_id, company_id, type, broker_key, name, address, postal_code, city, state, phone, email, country, tax_id, license, company, contact, created_at, updated_at) + VALUES + (${TENANT_ID}, ${COMPANY_ID}, 'persona', 'CB001', 'Agente Aduanal García', 'Av. Reforma 123', '01000', 'Ciudad de México', 'CDMX', '5555555555', 'garcia@aduanas.com', 'MEX', 'GAAR800101ABC', '1234', 'García y Asociados', 'Juan García', now(), now()), + (${TENANT_ID}, ${COMPANY_ID}, 'persona', 'CB002', 'Agente Aduanal López', 'Blvd. Díaz Ordaz 456', '22000', 'Tijuana', 'BC', '6641234567', 'lopez@customs.com', 'MEX', 'LOPL750505XYZ', '2345', 'López Customs', 'María López', now(), now()), + (${TENANT_ID}, ${COMPANY_ID}, 'persona', 'CB003', 'Agente Aduanal Martínez', 'Calle Industria 789', '45000', 'Guadalajara', 'JAL', '3339876543', 'martinez@broker.com', 'MEX', 'MARM850315DEF', '3456', 'Martínez Brokerage', 'Pedro Martínez', now(), now()) + ON CONFLICT (broker_key, tenant_id, company_id) DO NOTHING; + " >/dev/null 2>&1 +} + +# Insertar datos de ejemplo para clients_and_providers +seed_clients_and_providers() { + echo " → Insertando clientes y proveedores..." + exec_pg_sql " + INSERT INTO a76.clients_and_providers (tenant_id, company_id, type_nat_foreign, name, short_name, rfc, client_or_provider, web_key, is_active, created_at, updated_at) + VALUES + (${TENANT_ID}, ${COMPANY_ID}, 'N', 'Proveedor Tecnológico SA de CV', 'PROVTECH', 'PTE901201ABC', 'BOTH', 'PROV001', true, now(), now()), + (${TENANT_ID}, ${COMPANY_ID}, 'N', 'Cliente Industrial del Norte SA', 'CINORTE', 'CIN850615XYZ', 'BOTH', 'CLI001', true, now(), now()), + (${TENANT_ID}, ${COMPANY_ID}, 'E', 'Global Supplies Inc', 'GLOBSUP', 'GSI123456789', 'BOTH', 'BOTH001', true, now(), now()), + (${TENANT_ID}, ${COMPANY_ID}, 'N', 'Manufacturas del Bajío SA', 'MANBAJIO', 'MDB920310DEF', 'BOTH', 'CLI002', true, now(), now()) + RETURNING id; + " >/dev/null 2>&1 +} + +# Insertar datos de ejemplo para packages +seed_packages() { + echo " → Insertando tipos de paquete..." + exec_pg_sql " + INSERT INTO a76.packages (tenant_id, company_id, key, description_es, description_en, weight_unit, plurals, plural_in, code_ace, code_aamex, created_at, updated_at) + VALUES + (${TENANT_ID}, ${COMPANY_ID}, 'PK01', 'Caja de Cartón', 'Cardboard Box', 0.5, 'CAJS', 'BOXS', 'CB01', 'CAJA001', now(), now()), + (${TENANT_ID}, ${COMPANY_ID}, 'PK02', 'Pallet de Madera', 'Wooden Pallet', 15.0, 'PLTS', 'PLTS', 'WP01', 'PALL001', now(), now()), + (${TENANT_ID}, ${COMPANY_ID}, 'PK03', 'Tambor Metálico', 'Metal Drum', 10.0, 'TMBS', 'DRMS', 'MD01', 'TAMB001', now(), now()), + (${TENANT_ID}, ${COMPANY_ID}, 'PK04', 'Contenedor', 'Container', 2000.0, 'CONT', 'CONT', 'CT01', 'CONT001', now(), now()) + ON CONFLICT (tenant_id, company_id, key) DO NOTHING; + " >/dev/null 2>&1 +} + +# Insertar datos de ejemplo para classes +seed_classes() { + echo " → Insertando clases..." + exec_pg_sql "INSERT INTO a76.classes (tenant_id, company_id, class_code, description_es, description_en, material_key, unit_of_measure, fraction, us_fraction, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, 'CLS001', 'Componentes Electrónicos', 'Electronic Components', 'MP', 'PZA', '8542.31.01', '8542.31.0000', now(), now()), (${TENANT_ID}, ${COMPANY_ID}, 'CLS002', 'Partes Automotrices', 'Automotive Parts', 'MP', 'KGS', '8708.29.99', '8708.29.9900', now(), now()), (${TENANT_ID}, ${COMPANY_ID}, 'CLS003', 'Textiles y Telas', 'Textiles and Fabrics', 'MP', 'MT', '5407.20.01', '5407.20.0100', now(), now()), (${TENANT_ID}, ${COMPANY_ID}, 'CLS004', 'Equipo de Computación', 'Computer Equipment', 'MP', 'PZA', '8471.30.01', '8471.30.0100', now(), now()) ON CONFLICT (tenant_id, company_id, class_code) DO NOTHING;" >/dev/null 2>&1 +} + +# Insertar datos de ejemplo para parts +seed_parts() { + echo " → Insertando partes/componentes..." + + # Obtener un client_id para asociar las partes + local client_id + client_id=$(exec_pg_sql "SELECT id FROM a76.clients_and_providers WHERE tenant_id = ${TENANT_ID} AND company_id = ${COMPANY_ID} LIMIT 1;" | xargs) + + if [ -n "$client_id" ]; then + exec_pg_sql "INSERT INTO a76.parts (tenant_id, company_id, client_id, part_number, description_spanish, description_english, part_class, currency_key, unit_of_measure, unit_cost, fraction, us_fraction, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, ${client_id}, 'PART-001', 'Microcontrolador ARM Cortex-M4', 'ARM Cortex-M4 Microcontroller', 'CLS001', 'USD', 'PZA', 15.50, '8542.31.01', '8542.31.0000', now(), now()), (${TENANT_ID}, ${COMPANY_ID}, ${client_id}, 'PART-002', 'Filtro de Aceite Automotriz', 'Automotive Oil Filter', 'CLS002', 'USD', 'PZA', 8.75, '8421.23.01', '8421.23.0100', now(), now()), (${TENANT_ID}, ${COMPANY_ID}, ${client_id}, 'PART-003', 'Tela de Algodón para Tapicería', 'Cotton Upholstery Fabric', 'CLS003', 'USD', 'MT', 12.00, '5208.31.01', '5208.31.0100', now(), now()), (${TENANT_ID}, ${COMPANY_ID}, ${client_id}, 'PART-004', 'Disco Duro SSD 500GB', '500GB SSD Hard Drive', 'CLS004', 'USD', 'PZA', 65.00, '8471.70.01', '8471.70.0100', now(), now()), (${TENANT_ID}, ${COMPANY_ID}, ${client_id}, 'PART-005', 'Sensor de Temperatura Digital', 'Digital Temperature Sensor', 'CLS001', 'USD', 'PZA', 5.25, '9025.19.01', '9025.19.0100', now(), now()) ON CONFLICT (tenant_id, company_id, part_number) DO NOTHING;" >/dev/null 2>&1 + fi +} + +# Insertar datos de ejemplo para pedimentos +seed_pedimentos() { + echo " → Insertando pedimentos..." + + # Primero obtener IDs de clientes + local client_ids + client_ids=$(exec_pg_sql "SELECT id FROM a76.clients_and_providers WHERE tenant_id = ${TENANT_ID} AND company_id = ${COMPANY_ID} AND client_or_provider IN ('CLIENT', 'BOTH') LIMIT 2;") + local client_id_1=$(echo "$client_ids" | sed -n '1p' | xargs) + local client_id_2=$(echo "$client_ids" | sed -n '2p' | xargs) + + if [ -n "$client_id_1" ]; then + exec_pg_sql "INSERT INTO a76.pedimentos (tenant_id, company_id, year, customs_office, license, pedimento_number, client_id, operation_type, pedimento_type, pedimento_code, regime, status, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, '24', '47', '3807', '8001234', ${client_id_1}, 'imp', 'normal', 'V1', 'ITE', 'draft', now(), now()), (${TENANT_ID}, ${COMPANY_ID}, '24', '47', '3807', '8001235', ${client_id_1}, 'exp', 'normal', 'V1', 'ETE', 'draft', now(), now()) ON CONFLICT (tenant_id, company_id, year, customs_office, license, pedimento_number) DO NOTHING;" >/dev/null 2>&1 + fi + + if [ -n "$client_id_2" ]; then + exec_pg_sql "INSERT INTO a76.pedimentos (tenant_id, company_id, year, customs_office, license, pedimento_number, client_id, operation_type, pedimento_type, pedimento_code, regime, status, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, '24', '47', '3807', '8001236', ${client_id_2}, 'imp', 'consolidated', 'V1', 'ITE', 'draft', now(), now()) ON CONFLICT (tenant_id, company_id, year, customs_office, license, pedimento_number) DO NOTHING;" >/dev/null 2>&1 + fi +} + +# Insertar datos de ejemplo para invoices +seed_invoices() { + echo " → Insertando facturas..." + + # Obtener IDs de clientes/proveedores + local provider_ids + provider_ids=$(exec_pg_sql "SELECT id FROM a76.clients_and_providers WHERE tenant_id = ${TENANT_ID} AND company_id = ${COMPANY_ID} AND client_or_provider IN ('PROVIDER', 'BOTH') LIMIT 2;") + local provider_id_1=$(echo "$provider_ids" | sed -n '1p' | xargs) + local provider_id_2=$(echo "$provider_ids" | sed -n '2p' | xargs) + + # Insertar facturas en invoice_header (mínimo requerido) + exec_pg_sql "INSERT INTO a76.invoice_header (tenant_id, company_id, system, operation_type, invoice_type, invoice_number, invoice_date, is_updated, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'DEF', 'INV-2024-001', '2024-01-10', false, now(), now()), (${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'exp', 'EXDEF', 'INV-2024-002', '2024-02-15', false, now(), now()), (${TENANT_ID}, ${COMPANY_ID}, 'fixed_asset', 'imp', 'TEM', 'INV-2024-003', '2024-03-05', false, now(), now()) ON CONFLICT DO NOTHING;" >/dev/null 2>&1 + + # Obtener IDs de las facturas recién creadas + local invoice_ids + invoice_ids=$(exec_pg_sql "SELECT id FROM a76.invoice_header WHERE tenant_id = ${TENANT_ID} AND company_id = ${COMPANY_ID} AND invoice_number IN ('INV-2024-001', 'INV-2024-002', 'INV-2024-003') ORDER BY invoice_number;") + local invoice_id_1=$(echo "$invoice_ids" | sed -n '1p' | xargs) + local invoice_id_2=$(echo "$invoice_ids" | sed -n '2p' | xargs) + local invoice_id_3=$(echo "$invoice_ids" | sed -n '3p' | xargs) + + # Insertar datos financieros para las facturas + if [ -n "$invoice_id_1" ]; then + exec_pg_sql "INSERT INTO a76.invoice_financials (tenant_id, company_id, invoice_id, currency, exchange_rate, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, ${invoice_id_1}, 'foreign', 17.50, now(), now()) ON CONFLICT DO NOTHING;" >/dev/null 2>&1 + fi + + if [ -n "$invoice_id_2" ]; then + exec_pg_sql "INSERT INTO a76.invoice_financials (tenant_id, company_id, invoice_id, currency, exchange_rate, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, ${invoice_id_2}, 'foreign', 17.45, now(), now()) ON CONFLICT DO NOTHING;" >/dev/null 2>&1 + fi + + if [ -n "$invoice_id_3" ]; then + exec_pg_sql "INSERT INTO a76.invoice_financials (tenant_id, company_id, invoice_id, currency, exchange_rate, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, ${invoice_id_3}, 'local', 1.00, now(), now()) ON CONFLICT DO NOTHING;" >/dev/null 2>&1 + fi + + # Insertar datos de compliance mexicano con proveedores + if [ -n "$invoice_id_1" ] && [ -n "$provider_id_1" ]; then + exec_pg_sql "INSERT INTO a76.invoice_compliance_mx (tenant_id, company_id, invoice_id, provider_id, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, ${invoice_id_1}, ${provider_id_1}, now(), now()) ON CONFLICT DO NOTHING;" >/dev/null 2>&1 + fi + + if [ -n "$invoice_id_2" ] && [ -n "$provider_id_1" ]; then + exec_pg_sql "INSERT INTO a76.invoice_compliance_mx (tenant_id, company_id, invoice_id, provider_id, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, ${invoice_id_2}, ${provider_id_1}, now(), now()) ON CONFLICT DO NOTHING;" >/dev/null 2>&1 + fi + + if [ -n "$invoice_id_3" ] && [ -n "$provider_id_2" ]; then + exec_pg_sql "INSERT INTO a76.invoice_compliance_mx (tenant_id, company_id, invoice_id, provider_id, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, ${invoice_id_3}, ${provider_id_2}, now(), now()) ON CONFLICT DO NOTHING;" >/dev/null 2>&1 + fi + + # Insertar datos de logística con incoterm + if [ -n "$invoice_id_1" ]; then + exec_pg_sql "INSERT INTO a76.invoice_logistics (tenant_id, company_id, invoice_id, transport_type, weight_type, incoterm, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, ${invoice_id_1}, 'none', 'kgs', 'FOB', now(), now()) ON CONFLICT DO NOTHING;" >/dev/null 2>&1 + fi + + if [ -n "$invoice_id_2" ]; then + exec_pg_sql "INSERT INTO a76.invoice_logistics (tenant_id, company_id, invoice_id, transport_type, weight_type, incoterm, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, ${invoice_id_2}, 'none', 'kgs', 'CIF', now(), now()) ON CONFLICT DO NOTHING;" >/dev/null 2>&1 + fi + + if [ -n "$invoice_id_3" ]; then + exec_pg_sql "INSERT INTO a76.invoice_logistics (tenant_id, company_id, invoice_id, transport_type, weight_type, incoterm, created_at, updated_at) VALUES (${TENANT_ID}, ${COMPANY_ID}, ${invoice_id_3}, 'none', 'kgs', 'EXW', now(), now()) ON CONFLICT DO NOTHING;" >/dev/null 2>&1 + fi +} + +# Ejecutar todas las funciones de seed +execute_seed_data() { + echo -e "\n${YELLOW}[SEED] Cargando datos de ejemplo...${NC}" + + seed_customs_brokers + seed_clients_and_providers + seed_packages + seed_classes + seed_parts + seed_pedimentos + seed_invoices + + echo -e "${GREEN}✓ Datos de ejemplo cargados exitosamente${NC}" + echo -e "${YELLOW} • 3 Agentes aduanales${NC}" + echo -e "${YELLOW} • 4 Clientes/Proveedores${NC}" + echo -e "${YELLOW} • 4 Tipos de paquete${NC}" + echo -e "${YELLOW} • 4 Clases${NC}" + echo -e "${YELLOW} • 5 Parts/Componentes${NC}" + echo -e "${YELLOW} • 3 Pedimentos${NC}" + echo -e "${YELLOW} • 3 Facturas${NC}" +} + # Variables de configuración KEYCLOAK_URL="${KEYCLOAK_URL:-http://localhost:8080/kcauth}" KEYCLOAK_ADMIN="${KEYCLOAK_ADMIN:-admin}" @@ -559,8 +745,10 @@ fi # Obtener información de la company COMPANY_INFO=$(exec_pg_sql "SELECT id, name FROM a76.company WHERE tenant_id = ${TENANT_ID} LIMIT 1;") +COMPANY_ID=$(exec_pg_sql "SELECT id FROM a76.company WHERE tenant_id = ${TENANT_ID} LIMIT 1;" | xargs) echo -e "${GREEN}✓ Company: ${COMPANY_INFO}${NC}" +echo -e "${GREEN}✓ Company ID: ${COMPANY_ID}${NC}" # Agregar tenant_id al usuario demo en Keycloak echo -e "\n${YELLOW}Asignando tenant_id al usuario demo...${NC}" @@ -635,6 +823,13 @@ else fi fi +############################################################################### +# 9. Cargar datos de ejemplo (opcional) +############################################################################### +if [ "$SEED_DATA" = true ]; then + execute_seed_data +fi + ############################################################################### # Resumen final ############################################################################### @@ -673,6 +868,18 @@ echo -e " ${GREEN}✓${NC} Plan: Enterprise (ilimitado)" echo -e " ${GREEN}✓${NC} Status: Activa" echo -e " ${GREEN}✓${NC} Features: API, Reportes Avanzados, Integraciones, Soporte Dedicado" echo -e " ${GREEN}✓${NC} Vigencia: 1 año" + +if [ "$SEED_DATA" = true ]; then +echo -e "\n${YELLOW}Datos de ejemplo:${NC}" +echo -e " ${GREEN}✓${NC} Agentes aduanales: 3" +echo -e " ${GREEN}✓${NC} Clientes/Proveedores: 4" +echo -e " ${GREEN}✓${NC} Tipos de paquete: 4" +echo -e " ${GREEN}✓${NC} Clases: 4" +echo -e " ${GREEN}✓${NC} Parts/Componentes: 5" +echo -e " ${GREEN}✓${NC} Pedimentos: 3" +echo -e " ${GREEN}✓${NC} Facturas: 3" +fi + echo -e "\n${YELLOW}Puedes acceder al sistema en:${NC}" echo -e " ${GREEN}http://localhost:5173${NC}" echo -e "\n${GREEN}════════════════════════════════════════════════════════${NC}\n"