From 4541629b3fd355093c4a426635c06938fa832774 Mon Sep 17 00:00:00 2001 From: Kevin_Ramirez Date: Mon, 30 Mar 2026 13:05:21 -0500 Subject: [PATCH] Parametros generales SCAII, con endpint y con la mayoria de pestanias hechas y funcionalaes --- .../versions/e76_app_settings_add_table.py | 40 + .../v1/modules/a76/app_settings/__init__.py | 1 + .../api/v1/modules/a76/app_settings/models.py | 33 + .../api/v1/modules/a76/app_settings/routes.py | 59 + .../v1/modules/a76/app_settings/schemas.py | 989 ++++++++++ .../v1/modules/a76/app_settings/service.py | 137 ++ backend/api/v1/modules/a76/router.py | 3 + .../src/lib/api/dashboard/a76/app-settings.ts | 34 + .../invoices/edit/items/items-tab-form.svelte | 1750 ----------------- .../settings/DynamicSettingForm.svelte | 79 + .../settings/SettingCategoryNav.svelte | 72 + .../settings/SettingFormField.svelte | 168 ++ .../settings/SettingHierarchyBadge.svelte | 31 + .../dashboard/settings/SsisGenTabsForm.svelte | 626 ++++++ .../dashboard/settings/settings-metadata.ts | 35 + .../src/lib/components/sidebar/modules.ts | 4 + .../dashboard/settings/general/+page.svelte | 171 ++ .../dashboard/settings/general/+page.ts | 1 + 18 files changed, 2483 insertions(+), 1750 deletions(-) create mode 100644 backend/alembic/versions/e76_app_settings_add_table.py create mode 100644 backend/api/v1/modules/a76/app_settings/__init__.py create mode 100644 backend/api/v1/modules/a76/app_settings/models.py create mode 100644 backend/api/v1/modules/a76/app_settings/routes.py create mode 100644 backend/api/v1/modules/a76/app_settings/schemas.py create mode 100644 backend/api/v1/modules/a76/app_settings/service.py create mode 100644 frontend/src/lib/api/dashboard/a76/app-settings.ts delete mode 100644 frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte create mode 100644 frontend/src/lib/components/dashboard/settings/DynamicSettingForm.svelte create mode 100644 frontend/src/lib/components/dashboard/settings/SettingCategoryNav.svelte create mode 100644 frontend/src/lib/components/dashboard/settings/SettingFormField.svelte create mode 100644 frontend/src/lib/components/dashboard/settings/SettingHierarchyBadge.svelte create mode 100644 frontend/src/lib/components/dashboard/settings/SsisGenTabsForm.svelte create mode 100644 frontend/src/lib/components/dashboard/settings/settings-metadata.ts create mode 100644 frontend/src/routes/dashboard/settings/general/+page.svelte create mode 100644 frontend/src/routes/dashboard/settings/general/+page.ts diff --git a/backend/alembic/versions/e76_app_settings_add_table.py b/backend/alembic/versions/e76_app_settings_add_table.py new file mode 100644 index 00000000..ba6391be --- /dev/null +++ b/backend/alembic/versions/e76_app_settings_add_table.py @@ -0,0 +1,40 @@ +"""add_app_settings_table + +Revision ID: e76_app_settings +Revises: c1a2b3d4e5f6 +Create Date: 2026-03-27 16:10:00.000000 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = 'e76_app_settings' +down_revision = 'c1a2b3d4e5f6' +branch_labels = None +depends_on = None + +def upgrade(): + # Create a76.app_settings table + op.create_table( + 'app_settings', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('tenant_id', sa.Integer(), nullable=True), + sa.Column('company_id', sa.Integer(), nullable=True), + sa.Column('settings', postgresql.JSONB(astext_type=sa.Text()), nullable=False, server_default='{}'), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.ForeignKeyConstraint(['company_id'], ['a76.company.id'], ), + sa.ForeignKeyConstraint(['tenant_id'], ['core.tenants.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('tenant_id', 'company_id', name='uq_app_settings_tenant_company'), + schema='a76' + ) + op.create_index(op.f('ix_a76_app_settings_company_id'), 'app_settings', ['company_id'], unique=False, schema='a76') + op.create_index(op.f('ix_a76_app_settings_tenant_id'), 'app_settings', ['tenant_id'], unique=False, schema='a76') + +def downgrade(): + op.drop_index(op.f('ix_a76_app_settings_tenant_id'), table_name='app_settings', schema='a76') + op.drop_index(op.f('ix_a76_app_settings_company_id'), table_name='app_settings', schema='a76') + op.drop_table('app_settings', schema='a76') diff --git a/backend/api/v1/modules/a76/app_settings/__init__.py b/backend/api/v1/modules/a76/app_settings/__init__.py new file mode 100644 index 00000000..2af0782c --- /dev/null +++ b/backend/api/v1/modules/a76/app_settings/__init__.py @@ -0,0 +1 @@ +# Module initialization for app_settings diff --git a/backend/api/v1/modules/a76/app_settings/models.py b/backend/api/v1/modules/a76/app_settings/models.py new file mode 100644 index 00000000..d9c8f8ca --- /dev/null +++ b/backend/api/v1/modules/a76/app_settings/models.py @@ -0,0 +1,33 @@ +from typing import Optional +from sqlalchemy import Integer, ForeignKey, UniqueConstraint +from sqlalchemy.dialects.postgresql import JSONB +from sqlalchemy.orm import Mapped, mapped_column +from core.database import Base +from api.v1.common.base_models import TimestampMixin + +class AppSetting(Base, TimestampMixin): + """ + Unified configuration table for Anexo 76. + Replaces 14 legacy tables using a hierarchical JSONB override system. + """ + __tablename__ = "app_settings" + __table_args__ = ( + UniqueConstraint("tenant_id", "company_id", name="uq_app_settings_tenant_company"), + {"schema": "a76"}, + ) + + id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) + + # Hierarchy levels (Nullable to allow Global/Tenant/Company scoping) + tenant_id: Mapped[Optional[int]] = mapped_column( + Integer, ForeignKey("core.tenants.id"), nullable=True, index=True + ) + company_id: Mapped[Optional[int]] = mapped_column( + Integer, ForeignKey("a76.company.id"), nullable=True, index=True + ) + + # The actual configuration payload + settings: Mapped[dict] = mapped_column(JSONB, nullable=False, default={}) + + def __repr__(self): + return f"" diff --git a/backend/api/v1/modules/a76/app_settings/routes.py b/backend/api/v1/modules/a76/app_settings/routes.py new file mode 100644 index 00000000..dde03984 --- /dev/null +++ b/backend/api/v1/modules/a76/app_settings/routes.py @@ -0,0 +1,59 @@ +from fastapi import APIRouter, Depends, Query, HTTPException +from sqlalchemy.orm import Session +from typing import Optional, Dict, Any +from core.database import get_core_db +from .service import AppSettingsService +from .schemas import AppSettingRequest, AppSettingResponse + +router = APIRouter(prefix="/a76/app-settings", tags=["a76 / app_settings"]) + +import logging +import traceback + +logger = logging.getLogger(__name__) + +@router.get("/resolved") +def get_resolved_settings( + tenant_id: int = Query(...), + company_id: int = Query(...), + db: Session = Depends(get_core_db) +): + """ + Returns the final merged configuration for a company. + Merges Global -> Tenant -> Company levels. + """ + try: + return AppSettingsService.get_resolved_settings(db, tenant_id, company_id) + except Exception as e: + logger.error(f"RESOLVE ERROR: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/upsert") +def upsert_settings( + payload: AppSettingRequest, + db: Session = Depends(get_core_db) +): + """ + Creates or updates an override for a specific level (Global, Tenant, or Company). + """ + try: + data = payload.settings.model_dump(exclude_unset=True) + return AppSettingsService.upsert_settings( + db, + payload.tenant_id, + payload.company_id, + data + ) + except Exception as e: + logger.error(f"UPSERT ERROR: {str(e)}", exc_info=True) + raise HTTPException(status_code=500, detail=str(e)) + +@router.put("/upsert") +def update_settings( + payload: AppSettingRequest, + db: Session = Depends(get_core_db) +): + """ + Alias for upsert_settings. + """ + return upsert_settings(payload, db) diff --git a/backend/api/v1/modules/a76/app_settings/schemas.py b/backend/api/v1/modules/a76/app_settings/schemas.py new file mode 100644 index 00000000..4cf77221 --- /dev/null +++ b/backend/api/v1/modules/a76/app_settings/schemas.py @@ -0,0 +1,989 @@ +from typing import Optional, Any, Dict +from pydantic import BaseModel, Field +from decimal import Decimal + +# --- Legacy Table Domains (Tarea 1) --- + +class SSisGenSettings(BaseModel): + """ + Legacy Table: SSisGen + General system parameters migrated from Clarion/WinDev. + """ + consecutivo: Optional[int] = None + dta: Optional[int] = None + dtaexpo: Optional[int] = None + subempresa: Optional[str] = None + patharch: Optional[str] = None + patharchtransmision: Optional[str] = None + pathtransexpo: Optional[str] = None + pathrespuesta: Optional[str] = None + patharchped: Optional[str] = None + patharchpedconsm: Optional[str] = None + pathgenimpotemp: Optional[str] = None + pathgenexpo: Optional[str] = None + actseguridad: Optional[int] = None + controldes: Optional[int] = None + diadesactual: Optional[int] = None + diavencimiento: Optional[int] = None + mensajevenc: Optional[int] = None + fechades: Optional[int] = None + factoriva: Optional[Decimal] = None + validasifra: Optional[int] = None + decimalespeso: Optional[int] = None + decimalescant: Optional[int] = None + decimalesvalor: Optional[int] = None + calvalbasetcped: Optional[int] = None + calvalbasetcpedexpo: Optional[int] = None + filtrocantidad: Optional[Decimal] = None + muestraarchcodbarras: Optional[int] = None + datoshist: Optional[int] = None + tipovenro: Optional[str] = None + cantvenro: Optional[int] = None + costoplanta: Optional[Decimal] = None + firmapacking: Optional[int] = None + advertenciatm: Optional[int] = None + tomarsaldosvenc: Optional[int] = None + costoimpofijo: Optional[int] = None + valparteexiste: Optional[int] = None + valmanifusado: Optional[int] = None + temporalfechapago: Optional[int] = None + asignadiasantdesc: Optional[int] = None + diasantdesc: Optional[int] = None + deshabilitardescparte: Optional[int] = None + deshabilitardescparteing: Optional[int] = None + asignafracameparte: Optional[int] = None + validadecencant: Optional[int] = None + usartranspamedocame: Optional[int] = None + mostraradvertenciaro: Optional[int] = None + escondaamexpacking: Optional[int] = None + calcdutypacking: Optional[int] = None + parammultiples: Optional[int] = None + fraccnivelpais: Optional[int] = None + covefechaemision: Optional[int] = None + valordllstcfacturaexpo: Optional[int] = None + interfaceaaconsolidada: Optional[int] = None + interfaceaatcfpff: Optional[str] = None + incluirobscoveobsimpo: Optional[int] = None + agregarincreimpo: Optional[int] = None + componentebom: Optional[int] = None + limitesubensamble: Optional[int] = None + actpdfreportes: Optional[int] = None + patharchpdfimpo: Optional[str] = None + patharchpdfexpo: Optional[str] = None + noimprimircons: Optional[int] = None + mostraradvertenciarovalor: Optional[int] = None + partesypedimentosporcliente: Optional[str] = None + mensajesvurfc: Optional[int] = None + mostrarpackinglistingles: Optional[int] = None + omitirempaqueencodigobarras: Optional[int] = None + restringepaisimpo: Optional[int] = None + bloqueoaldesactivarnumerodeparte: Optional[int] = None + restringpaisexpo: Optional[int] = None + geninformeanexo31: Optional[str] = None + utilizarfechapagopeddeundiaanterior: Optional[int] = None + utilizarequivalenciasdeumpornumerodeparte: Optional[int] = None + utilizartitulosalternativosimpresionfactura: Optional[int] = None + usarfactorconversionpornumerodeparte: Optional[int] = None + usarvude128o256: Optional[int] = None + usartcdelafechapagopedimpoendescarga: Optional[int] = None + solicitarcontrasenaadministrador: Optional[int] = None + agregarnumeroembarque: Optional[int] = None + utilizarumdeexistenciaentransmisionvu: Optional[int] = None + utilizarcodigodebrokerdeclienteenmainx30: Optional[int] = None + hojacalculosepararincrementablesanexo3: Optional[int] = None + hojacalculodesglosefacturaanexo3: Optional[int] = None + usarvaloragregadoenfacturaamericana: Optional[int] = None + ocultarinformacionfraccion: Optional[int] = None + resaltarsaldostempconcolor: Optional[int] = None + valoragregadoenfacturamexicana: Optional[int] = None + validarsectorprosecr8: Optional[int] = None + agregarsubtotalinterfazaa: Optional[int] = None + utilizarnombregenericomainx30: Optional[int] = None + utilizartcrespectoatipoped: Optional[int] = None + tomardecimalescompletos: Optional[int] = None + incluirremesaeninterfazaawinsaai: Optional[int] = None + cambiarpesosporcostounitario: Optional[int] = None + utilizarsolopartesnaftaenco: Optional[int] = None + bloqueodeediciondefacturas: Optional[int] = None + reasignafraccionclase: Optional[int] = None + calcularcostounitarioenbaseavalortotal: Optional[int] = None + parametroauxiliar: Optional[int] = None + usarcontroldefechasdeversion: Optional[int] = None + desactivaciondemodulos: Optional[int] = None + imprimirfacturaalterna: Optional[int] = None + activarexpedienteelectronico: Optional[int] = None + activarcatalogofraccionesamericanassifra: Optional[int] = None + informacionamericanasubtotal: Optional[int] = None + mostrarprogramaimmexprosec: Optional[int] = None + costounitarioporempaquefac: Optional[int] = None + transmitirfacalterna: Optional[int] = None + + +class SSisGen2Settings(BaseModel): + """ + Legacy Table: SSisGen2 + Extended system parameters. + """ + consecutivo: Optional[int] = None + actvaloragre: Optional[int] = None + valoragregadogen: Optional[Decimal] = None + + +class SSisGen3Settings(BaseModel): + """ + Legacy Table: SSisGen3 + Additional name-value parameters. + """ + parametro: Optional[str] = None + valorparametro: Optional[int] = None + + +class SSisMexSettings(BaseModel): + """Legacy Table: SSisMex (Mexican Purchases)""" + consecutivo: Optional[int] = None + prefijocm: Optional[str] = None + consecutivocm: Optional[str] = None + porparteclasemex: Optional[str] = None + porparteclaseame: Optional[str] = None + proveedor: Optional[str] = None + vendidoconsignado: Optional[str] = None + vendidoa: Optional[str] = None + enviadotransferido: Optional[str] = None + enviadoa: Optional[str] = None + flete: Optional[Decimal] = None + paisorigenmex: Optional[int] = None + numpartemex: Optional[int] = None + firmafmex: Optional[str] = None + fraccionimp: Optional[int] = None + tipofraccmex: Optional[int] = None + tasafraccmex: Optional[int] = None + umequivalentemex: Optional[int] = None + numparteame: Optional[int] = None + fraccioname: Optional[int] = None + paisorigename: Optional[int] = None + umequivalenteame: Optional[int] = None + firmafame: Optional[str] = None + impordencomp: Optional[int] = None + decimalespeso: Optional[int] = None + decimalescant: Optional[int] = None + decimalesvalor: Optional[int] = None + decimalescosto: Optional[int] = None + tipomoneda: Optional[str] = None + clavemoneda: Optional[str] = None + umauxiliarmex: Optional[int] = None + umalternamex: Optional[int] = None + transportista: Optional[str] = None + conductor: Optional[str] = None + transporte: Optional[str] = None + numtransporte: Optional[str] = None + observacione: Optional[str] = None + observacioni: Optional[str] = None + clavebultos: Optional[str] = None + packingordencompra: Optional[int] = None + firmaelectronica: Optional[str] = None + incluirlineadelpo: Optional[int] = None + lineapersonaaa: Optional[int] = None + agregarvaenprodterminados: Optional[int] = None + ocultarfechahora: Optional[int] = None + + +class SSisDefSettings(BaseModel): + """Legacy Table: SSisDef (Definitive Import)""" + consecutivo: Optional[int] = None + prefijoimpo: Optional[str] = None + consecutivoimpo: Optional[str] = None + porparteclasemex: Optional[str] = None + porparteclaseame: Optional[str] = None + pedimento: Optional[str] = None + tipo: Optional[str] = None + proveedor: Optional[str] = None + vendidoconsignado: Optional[str] = None + vendidoa: Optional[str] = None + enviadotransferido: Optional[str] = None + enviadoa: Optional[str] = None + aaduanal: Optional[str] = None + incoterm: Optional[str] = None + flete: Optional[Decimal] = None + identificador: Optional[str] = None + firmafmex: Optional[str] = None + firmafame: Optional[str] = None + codigobarras: Optional[int] = None + numpartemex: Optional[int] = None + numparteame: Optional[int] = None + fraccionimp: Optional[int] = None + tasafraccmex: Optional[int] = None + tipofraccmex: Optional[int] = None + paisorigenmex: Optional[int] = None + umequivalentemex: Optional[int] = None + umauxiliarmex: Optional[int] = None + fraccioname: Optional[int] = None + paisorigename: Optional[int] = None + umequivalenteame: Optional[int] = None + impordencomp: Optional[int] = None + decimalespeso: Optional[int] = None + decimalescant: Optional[int] = None + decimalesvalor: Optional[int] = None + decimalescosto: Optional[int] = None + tipomoneda: Optional[str] = None + clavemoneda: Optional[str] = None + cantlimite: Optional[Decimal] = None + pesolimite: Optional[Decimal] = None + valorlimite: Optional[Decimal] = None + umalternamex: Optional[int] = None + restringeimpopt: Optional[int] = None + partecomplementariamex: Optional[int] = None + pagoimpuesto: Optional[str] = None + formapago: Optional[str] = None + metvalor: Optional[str] = None + transportista: Optional[str] = None + aaduanalame: Optional[str] = None + conductor: Optional[str] = None + transporte: Optional[str] = None + numtransporte: Optional[str] = None + packingpedclave: Optional[int] = None + porordenporocpack: Optional[str] = None + actualizarpartepartida: Optional[int] = None + observacione: Optional[str] = None + observacioni: Optional[str] = None + clavebultos: Optional[str] = None + partecomplemexpartida: Optional[int] = None + cantlimitemin: Optional[Decimal] = None + pesolimitemin: Optional[Decimal] = None + valorlimitemin: Optional[Decimal] = None + packingordencompra: Optional[int] = None + firmaelectronica: Optional[str] = None + solicitarcontrasenaadministrador: Optional[int] = None + incluirlineadelpo: Optional[int] = None + lineapersonaaa: Optional[int] = None + agregarvaenprodterminados: Optional[int] = None + imprimirlote: Optional[int] = None + imprimirnumentrada: Optional[int] = None + ocultarfechahora: Optional[int] = None + + +class SSisExpoSettings(BaseModel): + """Legacy Table: SSisExpo (Exports)""" + tipofactura: Optional[str] = None + porparteclasemex: Optional[str] = None + porparteclaseame: Optional[str] = None + prefijoexpo: Optional[str] = None + consecutivoexpo: Optional[str] = None + pedimento: Optional[str] = None + proveedor: Optional[str] = None + vendidoconsignado: Optional[str] = None + vendidoa: Optional[str] = None + enviadotransferido: Optional[str] = None + enviadoa: Optional[str] = None + vendidopor: Optional[str] = None + aaduanal: Optional[str] = None + tipo: Optional[str] = None + incoterm: Optional[str] = None + flete: Optional[Decimal] = None + identificador: Optional[str] = None + tipomoneda: Optional[str] = None + moneda: Optional[str] = None + decimalespeso: Optional[int] = None + decimalescant: Optional[int] = None + decimalesvalor: Optional[int] = None + decimalescosto: Optional[int] = None + numpartemex: Optional[int] = None + paisorigenmex: Optional[int] = None + fraccionexp: Optional[int] = None + tasafraccmex: Optional[int] = None + costounitmex: Optional[int] = None + ordencompmex: Optional[int] = None + consfacmex: Optional[int] = None + umequivalentemex: Optional[int] = None + ocultarempaquemex: Optional[int] = None + costounitmpmex: Optional[int] = None + codigobarras: Optional[int] = None + partecomplementariamex: Optional[int] = None + valordllsmexpesos: Optional[int] = None + cantpncodbar: Optional[int] = None + dtacerocodbar: Optional[int] = None + firmafmex: Optional[str] = None + numparteame: Optional[int] = None + firmafame: Optional[str] = None + fraccioname: Optional[int] = None + paisorigename: Optional[int] = None + ordencompame: Optional[int] = None + umequivalenteame: Optional[int] = None + fdainformacioname: Optional[int] = None + paisopcame: Optional[int] = None + muestrafree: Optional[int] = None + fraccmexdes: Optional[int] = None + tipofraccmexdes: Optional[int] = None + colvalordes: Optional[int] = None + colpesodes: Optional[int] = None + descargaclase: Optional[int] = None + descargasust: Optional[int] = None + descargadef: Optional[int] = None + ventqueueact: Optional[int] = None + codigoscac: Optional[int] = None + formadesperdicio: Optional[str] = None + calvalorbasecosto: Optional[int] = None + calvabasecapt: Optional[int] = None + calpesobasedescarga: Optional[int] = None + calempaquebasecapt: Optional[int] = None + ocultcantcons: Optional[int] = None + prefijofraccmult: Optional[int] = None + cantlimite: Optional[Decimal] = None + pesolimite: Optional[Decimal] = None + valorlimite: Optional[Decimal] = None + costamepaisparte: Optional[int] = None + basecostompototal: Optional[str] = None + umequivalentemex2: Optional[int] = None + umequivalenteame2: Optional[int] = None + totalequivmex: Optional[int] = None + totalequivame: Optional[int] = None + infoadicional: Optional[str] = None + restringeexpomp: Optional[int] = None + calvatotagremp: Optional[str] = None + activaadvpedrtv1: Optional[int] = None + asignafrac9801: Optional[int] = None + valorexcfrac9801: Optional[Decimal] = None + pagoimpuesto: Optional[str] = None + formapago: Optional[str] = None + scrapsolonoduty: Optional[int] = None + descextraamepartes: Optional[int] = None + incambioregdesc: Optional[int] = None + escambioregimen: Optional[str] = None + descnocontemcant: Optional[int] = None + metvalor: Optional[str] = None + ocultarempaqueame: Optional[int] = None + transportista: Optional[str] = None + aaduanalame: Optional[str] = None + pesonetodesdiffac: Optional[Decimal] = None + enviadoporvendidopor: Optional[str] = None + conductor: Optional[str] = None + transporte: Optional[str] = None + numtransporte: Optional[str] = None + observacione: Optional[str] = None + observacioni: Optional[str] = None + clavebultos: Optional[str] = None + ocultacolva: Optional[int] = None + calccostosamebasemex: Optional[int] = None + proveedorexportador: Optional[str] = None + tipofraccmex: Optional[int] = None + partecomplemexpartida: Optional[int] = None + validacantparcandes: Optional[int] = None + cantlimitemin: Optional[Decimal] = None + pesolimitemin: Optional[Decimal] = None + valorlimitemin: Optional[Decimal] = None + costosamepais: Optional[str] = None + incluirfracamebil: Optional[int] = None + solicitarpswdactual: Optional[int] = None + solicitarpswddesactual: Optional[int] = None + buscarsaldosrecientes: Optional[int] = None + packingordenventa: Optional[int] = None + imprimirordenventa: Optional[int] = None + firmaelectronica: Optional[str] = None + mpf: Optional[int] = None + valormpf: Optional[Decimal] = None + colcantporpeso: Optional[int] = None + packingpedclave: Optional[int] = None + generafacturaimd: Optional[str] = None + generafacturaimdenbasea: Optional[str] = None + solicitarcontrasenaadministrador: Optional[int] = None + incluirlineadelpo: Optional[int] = None + valorpacking1dlls: Optional[int] = None + tomarvalormexenfacame: Optional[int] = None + lineapersonaaa: Optional[int] = None + calamebasemexmp: Optional[int] = None + calamebasemexva: Optional[int] = None + calamebasemexempaque: Optional[int] = None + usarvaenfacturaamericana: Optional[int] = None + activarfechacortesaldos: Optional[int] = None + fechacortesaldos: Optional[int] = None + imprimirlote: Optional[int] = None + imprimirnumentrada: Optional[int] = None + respaldardescargaendescargam: Optional[int] = None + ocultarfechahora: Optional[int] = None + calcularvaloresamericanosenbaseapartida: Optional[int] = None + validarestatusped: Optional[int] = None + descapais: Optional[int] = None + + +class SSisImpoSettings(BaseModel): + """Legacy Table: SSisImpo (General Import)""" + consecutivo: Optional[int] = None + prefijoimpo: Optional[str] = None + prefijocr: Optional[str] = None + consecutivoimpo: Optional[str] = None + consecutivocr: Optional[int] = None + porparteclasemex: Optional[str] = None + porparteclaseame: Optional[str] = None + pedimento: Optional[str] = None + tipo: Optional[str] = None + proveedor: Optional[str] = None + vendidoconsignado: Optional[str] = None + vendidoa: Optional[str] = None + enviadotransferido: Optional[str] = None + enviadoa: Optional[str] = None + aaduanal: Optional[str] = None + incoterm: Optional[str] = None + flete: Optional[Decimal] = None + identificador: Optional[str] = None + numpartemex: Optional[int] = None + numparteame: Optional[int] = None + firmafmex: Optional[str] = None + firmafame: Optional[str] = None + codigobarras: Optional[int] = None + fraccionimp: Optional[int] = None + tasafraccmex: Optional[int] = None + tipofraccmex: Optional[int] = None + paisorigenmex: Optional[int] = None + umequivalentemex: Optional[int] = None + umauxiliarmex: Optional[int] = None + perpagrenglon: Optional[int] = None + fraccioname: Optional[int] = None + paisorigename: Optional[int] = None + umequivalenteame: Optional[int] = None + impordencomp: Optional[int] = None + decimalespeso: Optional[int] = None + decimalescant: Optional[int] = None + decimalesvalor: Optional[int] = None + decimalescosto: Optional[int] = None + tipomoneda: Optional[str] = None + clavemoneda: Optional[str] = None + cantlimite: Optional[Decimal] = None + pesolimite: Optional[Decimal] = None + valorlimite: Optional[Decimal] = None + valorlimitepar: Optional[Decimal] = None + umalternamex: Optional[int] = None + restringeimpopt: Optional[int] = None + controlremesa: Optional[str] = None + remesainicio: Optional[int] = None + remesafinal: Optional[int] = None + leyendafacame: Optional[str] = None + calcigibasecapt: Optional[int] = None + umequivalentemex2: Optional[int] = None + umequivalenteame2: Optional[int] = None + partecomplementariamex: Optional[int] = None + pagoimpuesto: Optional[str] = None + formapago: Optional[str] = None + metvalor: Optional[str] = None + transportista: Optional[str] = None + aaduanalame: Optional[str] = None + conductor: Optional[str] = None + transporte: Optional[str] = None + numtransporte: Optional[str] = None + packingpedclave: Optional[int] = None + porordenporocpack: Optional[str] = None + actualizarpartepartida: Optional[int] = None + observacione: Optional[str] = None + observacioni: Optional[str] = None + clavebultos: Optional[str] = None + partecomplemexpartida: Optional[int] = None + cantlimitemin: Optional[Decimal] = None + pesolimitemin: Optional[Decimal] = None + valorlimitemin: Optional[Decimal] = None + valorlimiteparmin: Optional[Decimal] = None + packingordencompra: Optional[int] = None + firmaelectronica: Optional[str] = None + solicitarcontrasenaadministrador: Optional[int] = None + incluirlineadelpo: Optional[int] = None + lineapersonaaa: Optional[int] = None + agregarvaenprodterminados: Optional[int] = None + imprimirlote: Optional[int] = None + imprimirnumentrada: Optional[int] = None + ocultarfechahora: Optional[int] = None + + + + + +class QSisCMexSettings(BaseModel): + """Legacy Table: QSisCMex (Fixed Asset Mexican Purchase)""" + consecutivo: Optional[int] = None + prefijocm: Optional[str] = None + consecutivocm: Optional[str] = None + proveedor: Optional[str] = None + vendidoconsignado: Optional[str] = None + vendidoa: Optional[str] = None + enviadotransferido: Optional[str] = None + enviadoa: Optional[str] = None + flete: Optional[Decimal] = None + paisorigenmex: Optional[int] = None + firmafmex: Optional[str] = None + fraccionimp: Optional[int] = None + tipofraccmex: Optional[int] = None + tasafraccmex: Optional[int] = None + impordencomp: Optional[int] = None + decimalespeso: Optional[int] = None + decimalescant: Optional[int] = None + decimalesvalor: Optional[int] = None + decimalescosto: Optional[int] = None + tipomoneda: Optional[str] = None + clavemoneda: Optional[str] = None + transportista: Optional[str] = None + conductor: Optional[str] = None + transporte: Optional[str] = None + numtransporte: Optional[str] = None + observacione: Optional[str] = None + observacioni: Optional[str] = None + packingordencompra: Optional[int] = None + imprimirlote: Optional[int] = None + imprimirnumentrada: Optional[int] = None + + +class QSisDefSettings(BaseModel): + """Legacy Table: QSisDef (Fixed Asset Definitive Import)""" + consecutivo: Optional[int] = None + prefijoimpo: Optional[str] = None + consecutivoimpo: Optional[str] = None + pedimento: Optional[str] = None + tipo: Optional[str] = None + proveedor: Optional[str] = None + vendidoconsignado: Optional[str] = None + vendidoa: Optional[str] = None + enviadotransferido: Optional[str] = None + enviadoa: Optional[str] = None + aaduanal: Optional[str] = None + incoterm: Optional[str] = None + flete: Optional[Decimal] = None + identificador: Optional[str] = None + firmafmex: Optional[str] = None + firmafame: Optional[str] = None + generarassettag: Optional[int] = None + codigobarras: Optional[int] = None + fraccionimp: Optional[int] = None + tasafraccmex: Optional[int] = None + tipofraccmex: Optional[int] = None + fraccioname: Optional[int] = None + paisorigenmex: Optional[int] = None + paisorigename: Optional[int] = None + impordencomp: Optional[int] = None + decimalespeso: Optional[int] = None + decimalescant: Optional[int] = None + decimalesvalor: Optional[int] = None + decimalescosto: Optional[int] = None + tipomoneda: Optional[str] = None + tipomn: Optional[str] = None + tipome: Optional[str] = None + clavemoneda: Optional[str] = None + controlremesa: Optional[str] = None + remesainicio: Optional[int] = None + remesafinal: Optional[int] = None + cantlimite: Optional[Decimal] = None + pesolimite: Optional[Decimal] = None + valorlimite: Optional[Decimal] = None + metvalor: Optional[str] = None + pagoimpuesto: Optional[str] = None + formapago: Optional[str] = None + transportista: Optional[str] = None + aaduanalame: Optional[str] = None + conductor: Optional[str] = None + transporte: Optional[str] = None + numtransporte: Optional[str] = None + observacione: Optional[str] = None + observacioni: Optional[str] = None + numpartemex: Optional[int] = None + numparteame: Optional[int] = None + cantlimitemin: Optional[Decimal] = None + pesolimitemin: Optional[Decimal] = None + valorlimitemin: Optional[Decimal] = None + actualizarpartepartida: Optional[int] = None + packingordencompra: Optional[int] = None + solicitarcontrasenaadministrador: Optional[int] = None + imprimirlote: Optional[int] = None + imprimirnumentrada: Optional[int] = None + + +class QSisGenSettings(BaseModel): + """Legacy Table: QSisGen (Fixed Asset General)""" + consecutivo: Optional[int] = None + dta: Optional[int] = None + dtaexpo: Optional[int] = None + subempresa: Optional[str] = None + patharch: Optional[str] = None + patharchtransmision: Optional[str] = None + pathrespuesta: Optional[str] = None + patharchped: Optional[str] = None + patharchpedconsm: Optional[str] = None + pathgenimpotemp: Optional[str] = None + pathgenexpo: Optional[str] = None + aplicapermat: Optional[int] = None + actseguridad: Optional[int] = None + controldes: Optional[int] = None + diadesactual: Optional[int] = None + fechades: Optional[int] = None + ubiplanta: Optional[str] = None + datoshistoricos: Optional[int] = None + tipovenro: Optional[str] = None + cantvenro: Optional[int] = None + omitirimposubpcodbarras: Optional[str] = None + muestraarchcodbarras: Optional[int] = None + calvalbasetcped: Optional[int] = None + calvalbasetcpedexpo: Optional[int] = None + decimalespeso: Optional[int] = None + decimalescant: Optional[int] = None + decimalesvalor: Optional[int] = None + factoriva: Optional[Decimal] = None + filtrocantidad: Optional[Decimal] = None + firmapacking: Optional[int] = None + advertenciatm: Optional[int] = None + calcdepreciacion: Optional[str] = None + valmanifusado: Optional[int] = None + validadecencant: Optional[int] = None + mostraradvertenciaro: Optional[int] = None + usartranspamedocame: Optional[int] = None + escondaamexpacking: Optional[int] = None + cantvscantseries: Optional[int] = None + covefechaemision: Optional[int] = None + interfaceaaconsolidada: Optional[int] = None + interfaceaatcfpff: Optional[str] = None + incluirobscoveobsimpo: Optional[int] = None + agregarincreimpo: Optional[int] = None + repdescargolineal: Optional[int] = None + identificadornodoseriecove: Optional[int] = None + actpdfreportes: Optional[int] = None + patharchpdfimpo: Optional[str] = None + patharchpdfexpo: Optional[str] = None + mensajesvurfc: Optional[int] = None + mostrarpackinglistingles: Optional[int] = None + enviarsubpartidascove: Optional[int] = None + restringepaisimpo: Optional[int] = None + restringpaisexpo: Optional[int] = None + utilizarfechapagopeddeundiaanterior: Optional[int] = None + utilizartitulosalternativosimpresionfactura: Optional[int] = None + usartcdelafechapagopedimpoendescarga: Optional[int] = None + usarvude128o256: Optional[int] = None + agregarnumeroembarque: Optional[int] = None + utilizarumdeexistenciaentransmisionvu: Optional[int] = None + utilizarcodigodebrokerdeclienteenmainx30: Optional[int] = None + hojacalculosepararincrementablesanexo3: Optional[int] = None + hojacalculodesglosefacturaanexo3: Optional[int] = None + utilizarnombregenericomainx30: Optional[int] = None + utilizartcrespectoatipoped: Optional[int] = None + utilizarsolopartesnaftaenco: Optional[int] = None + cambiarpesosporcostounitario: Optional[int] = None + bloqueodeediciondefacturas: Optional[int] = None + calcularcostounitarioenbaseavalortotalscaf: Optional[int] = None + impresionfacturaalterna: Optional[int] = None + transmitirfacalterna: Optional[int] = None + + +class QSisExpoRepSettings(BaseModel): + """Legacy Table: QSisExpoRep (Fixed Asset Export Repeat?)""" + tipofactura: Optional[str] = None + prefijoexpo: Optional[str] = None + consecutivoexpo: Optional[str] = None + pedimento: Optional[str] = None + tipo: Optional[str] = None + proveedor: Optional[str] = None + vendidoconsignado: Optional[str] = None + vendidoa: Optional[str] = None + enviadotransferido: Optional[str] = None + enviadoa: Optional[str] = None + vendidopor: Optional[str] = None + aaduanal: Optional[str] = None + incoterm: Optional[str] = None + flete: Optional[Decimal] = None + identificador: Optional[str] = None + codigobarras: Optional[int] = None + cantpncodbar: Optional[int] = None + dtacerocodbar: Optional[int] = None + cbarrasvalor: Optional[str] = None + firmafmex: Optional[str] = None + fraccionexp: Optional[int] = None + tasafraccmex: Optional[int] = None + tipofraccmex: Optional[int] = None + numpartemex: Optional[int] = None + paisorigenmex: Optional[int] = None + firmafame: Optional[str] = None + fraccioname: Optional[int] = None + paisorigename: Optional[int] = None + numparteame: Optional[int] = None + codigoscac: Optional[int] = None + impordencomp: Optional[int] = None + decimalespeso: Optional[int] = None + decimalescant: Optional[int] = None + decimalesvalor: Optional[int] = None + decimalescosto: Optional[int] = None + tipomoneda: Optional[str] = None + clavemoneda: Optional[str] = None + tipomn: Optional[str] = None + tipome: Optional[str] = None + mostrarvalact: Optional[int] = None + pepsporclase: Optional[int] = None + pepsporfraccion: Optional[int] = None + pepspordescripcion: Optional[int] = None + cantlimite: Optional[Decimal] = None + pesolimite: Optional[Decimal] = None + valorlimite: Optional[Decimal] = None + ventqueueact: Optional[int] = None + valfacttc: Optional[str] = None + metvalor: Optional[str] = None + pagoimpuesto: Optional[str] = None + formapago: Optional[str] = None + transportista: Optional[str] = None + aaduanalame: Optional[str] = None + enviadoporvendidopor: Optional[str] = None + conductor: Optional[str] = None + transporte: Optional[str] = None + numtransporte: Optional[str] = None + observacione: Optional[str] = None + observacioni: Optional[str] = None + proveedorexportador: Optional[str] = None + cantlimitemin: Optional[Decimal] = None + pesolimitemin: Optional[Decimal] = None + valorlimitemin: Optional[Decimal] = None + solicitarpswdactual: Optional[int] = None + solicitarpswddesactual: Optional[int] = None + packingordenventa: Optional[int] = None + imprimirpo: Optional[int] = None + fdainformacioname: Optional[int] = None + packingpedclave: Optional[int] = None + solicitarcontrasenaadministrador: Optional[int] = None + imprimirlote: Optional[int] = None + imprimirnumentrada: Optional[int] = None + + +class QSisImpoSettings(BaseModel): + """Legacy Table: QSisImpo (Fixed Asset Import)""" + consecutivo: Optional[int] = None + prefijoimpo: Optional[str] = None + consecutivoimpo: Optional[str] = None + pedimento: Optional[str] = None + tipo: Optional[str] = None + proveedor: Optional[str] = None + vendidoconsignado: Optional[str] = None + vendidoa: Optional[str] = None + enviadotransferido: Optional[str] = None + enviadoa: Optional[str] = None + aaduanal: Optional[str] = None + incoterm: Optional[str] = None + flete: Optional[Decimal] = None + identificador: Optional[str] = None + firmafmex: Optional[str] = None + firmafame: Optional[str] = None + codigobarras: Optional[int] = None + fraccionimp: Optional[int] = None + tasafraccmex: Optional[int] = None + tipofraccmex: Optional[int] = None + fraccioname: Optional[int] = None + paisorigenmex: Optional[int] = None + paisorigename: Optional[int] = None + impordencomp: Optional[int] = None + generarassettag: Optional[int] = None + decimalespeso: Optional[int] = None + decimalescant: Optional[int] = None + decimalesvalor: Optional[int] = None + decimalescosto: Optional[int] = None + tipomoneda: Optional[str] = None + tipomn: Optional[str] = None + tipome: Optional[str] = None + clavemoneda: Optional[str] = None + controlremesa: Optional[str] = None + remesainicio: Optional[int] = None + remesafinal: Optional[int] = None + perpagrenglon: Optional[int] = None + cantlimite: Optional[Decimal] = None + pesolimite: Optional[Decimal] = None + valorlimite: Optional[Decimal] = None + metvalor: Optional[str] = None + pagoimpuesto: Optional[str] = None + formapago: Optional[str] = None + transportista: Optional[str] = None + aaduanalame: Optional[str] = None + conductor: Optional[str] = None + transporte: Optional[str] = None + numtransporte: Optional[str] = None + observacione: Optional[str] = None + observacioni: Optional[str] = None + numpartemex: Optional[int] = None + numparteame: Optional[int] = None + cantlimitemin: Optional[Decimal] = None + pesolimitemin: Optional[Decimal] = None + valorlimitemin: Optional[Decimal] = None + actualizarpartepartida: Optional[int] = None + packingordencompra: Optional[int] = None + solicitarcontrasenaadministrador: Optional[int] = None + imprimirlote: Optional[int] = None + imprimirnumentrada: Optional[int] = None + + +class QSisImpoRepSettings(BaseModel): + """Legacy Table: QSisImpoRep (Fixed Asset Import Repeat?)""" + tipofactura: Optional[str] = None + consecutivo: Optional[int] = None + prefijoimpo: Optional[str] = None + consecutivoimpo: Optional[str] = None + pedimento: Optional[str] = None + tipo: Optional[str] = None + proveedor: Optional[str] = None + vendidoconsignado: Optional[str] = None + vendidoa: Optional[str] = None + enviadotransferido: Optional[str] = None + enviadoa: Optional[str] = None + aaduanal: Optional[str] = None + incoterm: Optional[str] = None + flete: Optional[Decimal] = None + identificador: Optional[str] = None + firmafmex: Optional[str] = None + firmafame: Optional[str] = None + codigobarras: Optional[int] = None + fraccionimp: Optional[int] = None + tasafraccmex: Optional[int] = None + tipofraccmex: Optional[int] = None + fraccioname: Optional[int] = None + paisorigenmex: Optional[int] = None + paisorigename: Optional[int] = None + impordencomp: Optional[int] = None + generarassettag: Optional[int] = None + decimalespeso: Optional[int] = None + decimalescant: Optional[int] = None + decimalesvalor: Optional[int] = None + decimalescosto: Optional[int] = None + tipomoneda: Optional[str] = None + tipomn: Optional[str] = None + tipome: Optional[str] = None + clavemoneda: Optional[str] = None + controlremesa: Optional[str] = None + remesainicio: Optional[int] = None + remesafinal: Optional[int] = None + perpagrenglon: Optional[int] = None + cantlimite: Optional[Decimal] = None + pesolimite: Optional[Decimal] = None + valorlimite: Optional[Decimal] = None + ventqueueact: Optional[int] = None + metvalor: Optional[str] = None + pagoimpuesto: Optional[str] = None + formapago: Optional[str] = None + transportista: Optional[str] = None + aaduanalame: Optional[str] = None + conductor: Optional[str] = None + transporte: Optional[str] = None + numtransporte: Optional[str] = None + observacione: Optional[str] = None + observacioni: Optional[str] = None + numpartemex: Optional[int] = None + numparteame: Optional[int] = None + cantlimitemin: Optional[Decimal] = None + pesolimitemin: Optional[Decimal] = None + valorlimitemin: Optional[Decimal] = None + packingordencompra: Optional[int] = None + solicitarcontrasenaadministrador: Optional[int] = None + + +class QSisExpoSettings(BaseModel): + """Legacy Table: QSisExpo (Fixed Asset Export)""" + tipofactura: Optional[str] = None + escambioregimen: Optional[str] = None + prefijoexpo: Optional[str] = None + consecutivoexpo: Optional[str] = None + pedimento: Optional[str] = None + tipo: Optional[str] = None + proveedor: Optional[str] = None + vendidoconsignado: Optional[str] = None + vendidoa: Optional[str] = None + enviadotransferido: Optional[str] = None + enviadoa: Optional[str] = None + vendidopor: Optional[str] = None + aaduanal: Optional[str] = None + incoterm: Optional[str] = None + flete: Optional[Decimal] = None + identificador: Optional[str] = None + codigobarras: Optional[int] = None + cantpncodbar: Optional[int] = None + dtacerocodbar: Optional[int] = None + cbarrasvalor: Optional[str] = None + firmafmex: Optional[str] = None + fraccionexp: Optional[int] = None + tasafraccmex: Optional[int] = None + tipofraccmex: Optional[int] = None + numpartemex: Optional[int] = None + paisorigenmex: Optional[int] = None + costounitmex: Optional[int] = None + ocultarempaquemex: Optional[int] = None + firmafame: Optional[str] = None + fraccioname: Optional[int] = None + paisorigename: Optional[int] = None + fdainformacioname: Optional[int] = None + numparteame: Optional[int] = None + codigoscac: Optional[int] = None + impordencomp: Optional[int] = None + decimalespeso: Optional[int] = None + decimalescant: Optional[int] = None + decimalesvalor: Optional[int] = None + decimalescosto: Optional[int] = None + tipomoneda: Optional[str] = None + clavemoneda: Optional[str] = None + tipomn: Optional[str] = None + tipome: Optional[str] = None + mostrarvalact: Optional[int] = None + pepsporclase: Optional[int] = None + pepsporfraccion: Optional[int] = None + pepspordescripcion: Optional[int] = None + pepsporfraccalterna: Optional[int] = None + cantlimite: Optional[Decimal] = None + pesolimite: Optional[Decimal] = None + valorlimite: Optional[Decimal] = None + ventqueueact: Optional[int] = None + valfacttc: Optional[str] = None + metvalor: Optional[str] = None + pagoimpuesto: Optional[str] = None + formapago: Optional[str] = None + colcantporpeso: Optional[int] = None + transportista: Optional[str] = None + aaduanalame: Optional[str] = None + enviadoporvendidopor: Optional[str] = None + conductor: Optional[str] = None + transporte: Optional[str] = None + numtransporte: Optional[str] = None + observacione: Optional[str] = None + observacioni: Optional[str] = None + proveedorexportador: Optional[str] = None + cantlimitemin: Optional[Decimal] = None + pesolimitemin: Optional[Decimal] = None + valorlimitemin: Optional[Decimal] = None + solicitarpswdactual: Optional[int] = None + solicitarpswddesactual: Optional[int] = None + packingordenventa: Optional[int] = None + imprimirpo: Optional[int] = None + packingpedclave: Optional[int] = None + generafacturaimd: Optional[str] = None + generafacturaimdenbasea: Optional[str] = None + solicitarcontrasenaadministrador: Optional[int] = None + imprimirlote: Optional[int] = None + imprimirnumentrada: Optional[int] = None + + +class SettingsPayload(BaseModel): + """ + Master container for all possible overrides. + Using Optional fields so only the delta is required in JSONB. + """ + # SCAII (Invoices / Inventory) + ssisgen: Optional[SSisGenSettings] = None + ssisgen2: Optional[SSisGen2Settings] = None + ssisgen3: Optional[SSisGen3Settings] = None + ssismex: Optional[SSisMexSettings] = None + ssisdef: Optional[SSisDefSettings] = None + ssisexpo: Optional[SSisExpoSettings] = None + ssisimpo: Optional[SSisImpoSettings] = None + + # SCAF (Fixed Assets) + qsiscmex: Optional[QSisCMexSettings] = None + qsisdef: Optional[QSisDefSettings] = None + qsisgen: Optional[QSisGenSettings] = None + qsisexporep: Optional[QSisExpoRepSettings] = None + qsisimpo: Optional[QSisImpoSettings] = None + qsisimporep: Optional[QSisImpoRepSettings] = None + qsisexpo: Optional[QSisExpoSettings] = None + +class AppSettingRequest(BaseModel): + tenant_id: Optional[int] = None + company_id: Optional[int] = None + settings: SettingsPayload + +class AppSettingResponse(BaseModel): + id: int + tenant_id: Optional[int] + company_id: Optional[int] + settings: Dict[str, Any] + + class Config: + from_attributes = True + diff --git a/backend/api/v1/modules/a76/app_settings/service.py b/backend/api/v1/modules/a76/app_settings/service.py new file mode 100644 index 00000000..f2e6f36f --- /dev/null +++ b/backend/api/v1/modules/a76/app_settings/service.py @@ -0,0 +1,137 @@ +from typing import Optional, List, Dict, Any +from sqlalchemy import or_, and_, select, case, nulls_first +from sqlalchemy.orm import Session +from .models import AppSetting + +import logging +logger = logging.getLogger(__name__) + +from decimal import Decimal +def convert_decimals(obj: Any) -> Any: + """ + Recursively converts Decimal objects to floats for JSON serialization. + """ + if isinstance(obj, list): + return [convert_decimals(i) for i in obj] + elif isinstance(obj, dict): + return {k: convert_decimals(v) for k, v in obj.items()} + elif isinstance(obj, Decimal): + return float(obj) + return obj + +def deep_merge(dict1: Dict[str, Any], dict2: Dict[str, Any]) -> Dict[str, Any]: + """ + Recursively merges dict2 into dict1. + """ + for key, value in dict2.items(): + if key in dict1 and isinstance(dict1[key], dict) and isinstance(value, dict): + deep_merge(dict1[key], value) + else: + dict1[key] = value + return dict1 + +class AppSettingsService: + """ + Service to manage hierarchical configuration overrides. + Hierarchy: System (Global) -> Tenant -> Company. + """ + + @staticmethod + def get_resolved_settings(db: Session, tenant_id: int, company_id: int) -> Dict[str, Any]: + """ + Retrieves settings from all levels (Global -> Tenant -> Company) and merges them. + """ + stmt = ( + select(AppSetting) + .where( + or_( + and_(AppSetting.tenant_id.is_(None), AppSetting.company_id.is_(None)), + and_(AppSetting.tenant_id == tenant_id, AppSetting.company_id.is_(None)), + and_(AppSetting.tenant_id == tenant_id, AppSetting.company_id == company_id), + ) + ) + .order_by( + # Ensure the order is: Global (1) -> Tenant (2) -> Company (3) + case( + (and_(AppSetting.tenant_id.is_(None), AppSetting.company_id.is_(None)), 1), + (and_(AppSetting.tenant_id.is_not(None), AppSetting.company_id.is_(None)), 2), + (and_(AppSetting.tenant_id.is_not(None), AppSetting.company_id.is_not(None)), 3), + else_=4 + ).asc() + ) + ) + + results = db.execute(stmt).scalars().all() + logger.info(f"RESOLVE: Found {len(results)} rows for Hierarchy") + + resolved_settings = {} + for row in results: + level_name = "GLOBAL" if not row.tenant_id else ("TENANT" if not row.company_id else "COMPANY") + + # Use safe data logging to avoid crashes + settings_data = row.settings if row.settings else {} + keys = list(settings_data.keys()) if isinstance(settings_data, dict) else "not-a-dict" + logger.info(f"RESOLVE: Merging {level_name} layer with keys: {keys}") + + if isinstance(settings_data, dict): + deep_merge(resolved_settings, settings_data) + + return resolved_settings + + @staticmethod + def upsert_settings(db: Session, tenant_id: Optional[int], company_id: Optional[int], settings: Dict[str, Any]) -> AppSetting: + """ + Inserts or updates settings for a specific level. + """ + level_label = f"level(tenant={tenant_id}, company={company_id})" + logger.info(f"UPSERT Settings START: {level_label}, keys_to_update={list(settings.keys())}") + + # Ensure all Decimals are converted to floats before deep merge and save + settings = convert_decimals(settings) + + stmt = select(AppSetting).where( + and_( + AppSetting.tenant_id == tenant_id if tenant_id is not None else AppSetting.tenant_id.is_(None), + AppSetting.company_id == company_id if company_id is not None else AppSetting.company_id.is_(None) + ) + ) + + existing = db.execute(stmt).scalar_one_or_none() + + if existing: + # Deep merge at the root level (merging categories like ssisgen, ssismex, etc.) + logger.info(f"UPSERT: Updating existing row ID={existing.id}") + # Create a shallow copy of the top-level dict to ensure SQLAlchemy sees a new reference + new_settings = dict(existing.settings) if existing.settings else {} + + # Detailed logging of what's changing + for cat, data in settings.items(): + old_keys = list(new_settings.get(cat, {}).keys()) + new_keys = list(data.keys()) if isinstance(data, dict) else [] + logger.info(f"UPSERT: Merging category [{cat}]. Old keys: {old_keys}, New keys to merge/overwrite: {new_keys}") + + deep_merge(new_settings, settings) + # Second pass of conversion (merged results might still have Decimals if original row had them) + existing.settings = convert_decimals(new_settings) + + from sqlalchemy.orm.attributes import flag_modified + flag_modified(existing, "settings") + else: + logger.info(f"UPSERT: Creating NEW row for {level_label}") + existing = AppSetting( + tenant_id=tenant_id, + company_id=company_id, + settings=settings + ) + db.add(existing) + + try: + db.commit() + db.refresh(existing) + logger.info(f"UPSERT SUCCESS: Row ID={existing.id}, Final Settings Hash Keys={list(existing.settings.keys())}") + except Exception as e: + db.rollback() + logger.error(f"UPSERT FAILED: {str(e)}") + raise e + + return existing diff --git a/backend/api/v1/modules/a76/router.py b/backend/api/v1/modules/a76/router.py index 33e0dd48..7d8df946 100644 --- a/backend/api/v1/modules/a76/router.py +++ b/backend/api/v1/modules/a76/router.py @@ -47,6 +47,7 @@ from .reports.exportacion.transmission.MAINX30.routes import router as transmiss from .reports.importacion.transmission.temporal.MAINX30.routes import router as transmission_temporal_router from .reports.importacion.transmission.definitive.MAINX30.routes import router as transmission_definitive_router from .reports.importacion.winsaai.router import router as winsaai_router +from .app_settings.routes import router as app_settings_router from .manifests.manifest.routes import router as manifests_router from .manifests.driver.routes import router as manifest_drivers_router @@ -176,6 +177,8 @@ router.include_router( tags=["a76 / reports"] ) +router.include_router(app_settings_router) + # Registrar router de bitácora from .audit_log.router import router as audit_log_router router.include_router(audit_log_router, prefix="/a76/audit-log", tags=["Audit Log"]) diff --git a/frontend/src/lib/api/dashboard/a76/app-settings.ts b/frontend/src/lib/api/dashboard/a76/app-settings.ts new file mode 100644 index 00000000..573fa13b --- /dev/null +++ b/frontend/src/lib/api/dashboard/a76/app-settings.ts @@ -0,0 +1,34 @@ +import { BACKEND_URL } from '$lib/config/backend'; + +export interface AppSettingsRequest { + tenant_id?: number | null; + company_id?: number | null; + settings: Record; +} + +export const appSettingsApi = { + /** + * Resolves settings merging Global -> Tenant -> Company hierarchy + */ + async getResolved(tenantId: number, companyId: number): Promise> { + const response = await fetch(`${BACKEND_URL}/v1/a76/app-settings/resolved?tenant_id=${tenantId}&company_id=${companyId}`); + if (!response.ok) throw new Error('Error al obtener configuraciones'); + return response.json(); + }, + + /** + * Upserts an override at a specific level + */ + async upsert(payload: AppSettingsRequest): Promise { + const response = await fetch(`${BACKEND_URL}/v1/a76/app-settings/upsert`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload) + }); + if (!response.ok) { + const error = await response.json(); + throw new Error(error.detail || 'Error al guardar configuración'); + } + return response.json(); + } +}; diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte deleted file mode 100644 index 49b741da..00000000 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/items-tab-form.svelte +++ /dev/null @@ -1,1750 +0,0 @@ - - -
-
-
-
-

Items de la Factura

-

- Carga partidas, crea o aplica plantillas sin salir de esta vista. -

-
-
- - - -
-
- -
- - - - Línea - {#if operationType === 1} - Factura Impo - Línea - P/S - Cant. Importada - Clase - Número Parte - Descripción - Contiene Subpartida - Partida Principal - Acciones - {:else if showCrTrackingHeader} - Factura Impo - Línea - P/S - Clase - Descripcion Clase - Cant. Importada - U.M. - Preferencia - Contiene Subpartida - Partida Principal - Acciones - {:else if invoiceType === 'REP' || invoiceType === 'REPAR'} - Factura Impo - Línea - P/S - Clase - Número Parte - Descripcion Clase - Cant. Importada - U.M. - Preferencia - Contiene Subpartida - Partida Principal - Acciones - {:else} - P/S - Clase - Descripcion Clase - Cant. Importada - U.M. - Preferencia - Contiene Subpartida - Partida Principal - Acciones - {/if} - - - - {#if displayedItems.length === 0} - - - No hay items disponibles - - - {:else} - {#each displayedItems as item (item.id)} - handleRowClick(item)} - class="group/item-row cursor-pointer transition-colors hover:bg-muted/50 {focusedLine?.id === - item.id - ? 'bg-muted ring-1 ring-primary/20 ring-inset' - : ''}" - > - {#if operationType === 1} - {item.line_number} - {item.fa_data?.search_invoice || '-'} - {item.fa_data?.search_line || '-'} - {item.is_subitem ? 'S' : 'P'} - {item.quantity?.quantity || '0'} - {item.class_code || '-'} - {item.part_number_display || '-'} - - {item.description?.description_spanish || '-'} - - {item.fa_data?.contains_subitems ? 'Sí' : 'No'} - {item.warehouse || '-'} - {:else if showCrTrackingHeader} - {item.line_number} - {item.fa_data?.search_invoice || '-'} - {item.fa_data?.search_line || '-'} - {item.is_subitem ? 'S' : 'P'} - {item.class_code || '-'} - - {item.description?.description_spanish || '-'} - - {item.quantity?.quantity || '0'} - {item.unit_of_measure_code || '-'} - {item.reference_number || '-'} - {item.fa_data?.contains_subitems ? 'Sí' : 'No'} - {item.warehouse || '-'} - {:else if invoiceType === 'REP' || invoiceType === 'REPAR'} - {item.line_number} - {item.fa_data?.search_invoice || '-'} - {item.fa_data?.search_line || '-'} - {item.is_subitem ? 'S' : 'P'} - {item.class_code || '-'} - {item.part_number_display || '-'} - - {item.description?.description_spanish || '-'} - - {item.quantity?.quantity || '0'} - {item.unit_of_measure_code || '-'} - {item.reference_number || '-'} - {item.fa_data?.contains_subitems ? 'Sí' : 'No'} - {item.warehouse || '-'} - {:else} - {item.line_number} - {item.is_subitem ? 'S' : 'P'} - {item.class_code || '-'} - {item.class_description || '-'} - {item.quantity?.quantity || '0'} - {item.unit_of_measure_code || '-'} - {item.reference_number || '-'} - {item.fa_data?.contains_subitems ? 'Sí' : 'No'} - {item.warehouse || '-'} - {/if} - -
- - -
-
-
- {/each} - {#if isLoadingMore} - - - Cargando más items... - - - {/if} - {/if} -
-
-
- - {#if flattenedLines.length > 0} -
- Mostrando {displayedItems.length} de {flattenedLines.length} líneas -
- {/if} -
- -
- {#if operationType === 1} - -
-

- Descripción en español: -

-
- {#if focusedLine} -

- {focusedLine.description?.description_spanish || 'Sin descripción disponible.'} -

- {:else} -

- Selecciona una fila para ver la descripción. -

- {/if} -
-
- {/if} - - -
-
-

Cantidades:

-
-
-
- Partidas: {items.length || 0} -
-
- Bultos: 0 -
-
-
- Importada:{imported || 0}
- Peso neto: {net_weight || 0}
- Peso bruto: {gross_weight || 0}
-
- -

- Valores de importacion: -

- Dolares:0 USD
- Pesos: 0 MXN
- De Captura: 0 USD - -

- spacer -

- - Aduana:0 USD
- Aduana: 0 MXN
-
-
-
- - - - - - Confirmar Eliminación - - ¿Está seguro que desea eliminar este item? Esta acción no se puede deshacer. - - - - - - - - - - - { - if (!open) { - showUsePresetDialog = false; - selectedPreset = null; - } - }} -> - - -
-
- Usar plantilla - - Selecciona una plantilla predefinida para cargar sus partidas. - -
-
- - -
-
- - -
- -
-
-
- - -
-
- -
- {#if isLoadingPresets} -
- - Cargando... -
- {:else if filteredPresets.length === 0} -
- -

No se encontraron plantillas

-
- {:else} - {#each filteredPresets as preset} - - {/each} - {/if} -
-
- - -
- {#if !selectedPreset} -
-
- -
-

Selecciona una plantilla para ver sus detalles

-
- {:else} -
- -
-
-

- {selectedPreset.name} -

-

- {selectedPreset.description || 'Sin descripción disponible.'} -

-
-
-
- Creada -
-
- {selectedPreset.created_at - ? new Date(selectedPreset.created_at).toLocaleDateString(undefined, { - dateStyle: 'long' - }) - : '-'} -
-
-
- - -
- - - - # - Descripción del Item - Cant. - Costo (USD) - - - - {#if selectedPresetItems.length === 0} - - -
- - Esta plantilla no contiene items. -
-
-
- {:else} - {#each selectedPresetItems as item, i} - - - {i + 1} - - -
- - {item?.description?.description_spanish || 'Sin descripción'} - - {#if item.reference_number} - - REF: {item.reference_number} - - {/if} -
-
- - {item?.quantity?.quantity || 0} - - - ${(item?.financial?.unit_cost_usd || 0).toLocaleString(undefined, { - minimumFractionDigits: 2 - })} - -
- {/each} - {/if} -
-
-
-
- {/if} -
-
- - -
- - -
-
-
- - { - if (!open) { - createPresetName = ''; - createPresetDescription = ''; - } - }} -> - - - Crear plantilla - Guarda los elementos actuales como una plantilla reutilizable para inyectar en otras - partidas. - - -
-
-
- - -
-
- -