diff --git a/backend/alembic/env.py b/backend/alembic/env.py index 819d6e50..cf2b4a50 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -13,6 +13,7 @@ logger = logging.getLogger(__name__) # access to the values within the .ini file in use. config = context.config + def get_database_url(): """Obtiene la URL de la base de datos (PostgreSQL) desde variables de entorno o alembic.ini.""" # Intentar construir desde variables de entorno primero @@ -41,6 +42,7 @@ def get_database_url(): return url + # Configurar la URL de la base de datos database_url = get_database_url() @@ -54,7 +56,7 @@ if os.environ.get("ALEMBIC_DEBUG"): debug_url = before + "@" + after except Exception: debug_url = "postgresql://***:***@***" - logger.error("Error al ocultar la contraseña en la URL para debug.") + logger.error("Error al ocultar la contraseña en la URL para debug.") config.set_main_option("sqlalchemy.url", database_url) @@ -77,8 +79,9 @@ config = context.config fileConfig(config.config_file_name) target_metadata = Base.metadata + def import_models_from_dir(dir_path: str): - """Importa recursivamente cualquier archivo models.py desde dir_path y archivos en directorios models/""" + """Importa recursivamente cualquier archivo models.py desde dir_path y archivos en directorios models/""" for root, dirs, files in os.walk(dir_path): # Importar archivos models.py directos if "models.py" in files: @@ -90,7 +93,7 @@ def import_models_from_dir(dir_path: str): spec = importlib.util.spec_from_file_location(module_name, module_path) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) - + # Importar todos los archivos .py en directorios llamados "models" if os.path.basename(root) == "models": for file in files: @@ -99,18 +102,20 @@ def import_models_from_dir(dir_path: str): rel_path = os.path.relpath(module_path, BASE_DIR) module_name = rel_path.replace(os.sep, ".").replace(".py", "") try: - spec = importlib.util.spec_from_file_location(module_name, module_path) + spec = importlib.util.spec_from_file_location( + module_name, module_path + ) mod = importlib.util.module_from_spec(spec) spec.loader.exec_module(mod) except Exception as e: logger.warning(f"No se pudo importar {module_path}: {e}") + # Importar todos los models dentro de api/v1/modules y api/v1/modules/uploads modules_dir = os.path.join(BASE_DIR, "api", "v1", "modules") import_models_from_dir(modules_dir) - def run_migrations_offline() -> None: """Run migrations in 'offline' mode. @@ -147,10 +152,7 @@ def run_migrations_online() -> None: ) with connectable.connect() as connection: - context.configure( - connection=connection, - target_metadata=target_metadata - ) + context.configure(connection=connection, target_metadata=target_metadata) with context.begin_transaction(): context.run_migrations() @@ -159,4 +161,4 @@ def run_migrations_online() -> None: if context.is_offline_mode(): run_migrations_offline() else: - run_migrations_online() \ No newline at end of file + run_migrations_online() diff --git a/backend/alembic/versions/531bf8cdae06_create_material_types_table.py b/backend/alembic/versions/531bf8cdae06_create_material_types_table.py index 0727b115..dbc13ef0 100644 --- a/backend/alembic/versions/531bf8cdae06_create_material_types_table.py +++ b/backend/alembic/versions/531bf8cdae06_create_material_types_table.py @@ -1,10 +1,11 @@ """create material_types table Revision ID: 531bf8cdae06 -Revises: +Revises: Create Date: 2025-10-19 18:23:39.613953 """ + from typing import Sequence, Union from alembic import op @@ -12,7 +13,7 @@ import sqlalchemy as sa # revision identifiers, used by Alembic. -revision: str = '531bf8cdae06' +revision: str = "531bf8cdae06" down_revision: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None @@ -21,124 +22,147 @@ depends_on: Union[str, Sequence[str], None] = None def upgrade() -> None: """Upgrade schema.""" # ### commands auto generated by Alembic - please adjust! ### - op.create_table('containers', - sa.Column('key', sa.String(length=3), nullable=False), - sa.Column('description', sa.String(length=500), nullable=False), - sa.PrimaryKeyConstraint('key', name='containers_pkey'), - schema='public' + op.create_table( + "containers", + sa.Column("key", sa.String(length=3), nullable=False), + sa.Column("description", sa.String(length=500), nullable=False), + sa.PrimaryKeyConstraint("key", name="containers_pkey"), + schema="public", ) - op.create_table('countries', - sa.Column('m3_key', sa.String(length=3), nullable=False), - sa.Column('mex_key', sa.String(length=2), nullable=False), - sa.Column('ame_key', sa.String(length=2), nullable=False), - sa.Column('description_es', sa.String(length=50), nullable=False), - sa.Column('description_en', sa.String(length=50), nullable=False), - sa.PrimaryKeyConstraint('m3_key', name='countries_pkey'), - schema='public' + op.create_table( + "countries", + sa.Column("m3_key", sa.String(length=3), nullable=False), + sa.Column("mex_key", sa.String(length=2), nullable=False), + sa.Column("ame_key", sa.String(length=2), nullable=False), + sa.Column("description_es", sa.String(length=50), nullable=False), + sa.Column("description_en", sa.String(length=50), nullable=False), + sa.PrimaryKeyConstraint("m3_key", name="countries_pkey"), + schema="public", ) - op.create_index('ak_country_ame', 'countries', ['ame_key'], unique=True, schema='public') - op.create_table('currency_types', - sa.Column('code', sa.String(length=3), nullable=False), - sa.Column('currency_name', sa.String(length=15), nullable=False), - sa.Column('country_description', sa.String(length=50), nullable=False), - sa.PrimaryKeyConstraint('code', name='currency_types_pkey'), - schema='public' + op.create_index( + "ak_country_ame", "countries", ["ame_key"], unique=True, schema="public" ) - op.create_table('customs_sections', - sa.Column('customs_code', sa.String(length=3), nullable=False), - sa.Column('section_name', sa.String(length=255), nullable=False), - sa.PrimaryKeyConstraint('customs_code', name='customs_code_pkey'), - schema='public' + op.create_table( + "currency_types", + sa.Column("code", sa.String(length=3), nullable=False), + sa.Column("currency_name", sa.String(length=15), nullable=False), + sa.Column("country_description", sa.String(length=50), nullable=False), + sa.PrimaryKeyConstraint("code", name="currency_types_pkey"), + schema="public", ) - op.create_table('customs_warehouses', - sa.Column('key', sa.String(length=3), nullable=False), - sa.Column('customs', sa.String(length=100), nullable=False), - sa.Column('fiscalized_warehouse', sa.String(length=1000), nullable=False), - sa.PrimaryKeyConstraint('key', 'customs', name='pk_customs_warehouse'), - schema='public' + op.create_table( + "customs_sections", + sa.Column("customs_code", sa.String(length=3), nullable=False), + sa.Column("section_name", sa.String(length=255), nullable=False), + sa.PrimaryKeyConstraint("customs_code", name="customs_code_pkey"), + schema="public", ) - op.create_table('incoterms', - sa.Column('code', sa.String(length=5), nullable=False), - sa.Column('description_es', sa.String(length=256), nullable=False), - sa.Column('description_en', sa.String(length=256), nullable=False), - sa.PrimaryKeyConstraint('code', name='incoterms_pkey'), - schema='public' + op.create_table( + "customs_warehouses", + sa.Column("key", sa.String(length=3), nullable=False), + sa.Column("customs", sa.String(length=100), nullable=False), + sa.Column("fiscalized_warehouse", sa.String(length=1000), nullable=False), + sa.PrimaryKeyConstraint("key", "customs", name="pk_customs_warehouse"), + schema="public", ) - op.create_table('invoice_types', - sa.Column('key', sa.String(length=5), nullable=False), - sa.Column('description', sa.String(length=50), nullable=False), - sa.Column('note', sa.String(length=500), nullable=False), - sa.Column('type', sa.String(length=15), nullable=False), - sa.PrimaryKeyConstraint('key', name='invoice_types_pkey'), - schema='public' + op.create_table( + "incoterms", + sa.Column("code", sa.String(length=5), nullable=False), + sa.Column("description_es", sa.String(length=256), nullable=False), + sa.Column("description_en", sa.String(length=256), nullable=False), + sa.PrimaryKeyConstraint("code", name="incoterms_pkey"), + schema="public", ) - op.create_table('material_types', - sa.Column('key', sa.String(length=10), nullable=False), - sa.Column('type', sa.String(length=15), nullable=False), - sa.Column('description', sa.String(length=256), nullable=False), - sa.PrimaryKeyConstraint('key', name='material_types_pkey'), - schema='public' + op.create_table( + "invoice_types", + sa.Column("key", sa.String(length=5), nullable=False), + sa.Column("description", sa.String(length=50), nullable=False), + sa.Column("note", sa.String(length=500), nullable=False), + sa.Column("type", sa.String(length=15), nullable=False), + sa.PrimaryKeyConstraint("key", name="invoice_types_pkey"), + schema="public", ) - op.create_table('payment_methods', - sa.Column('key', sa.String(length=2), nullable=False), - sa.Column('description', sa.String(length=100), nullable=False), - sa.PrimaryKeyConstraint('key', name='payment_methods_pkey'), - schema='public' + op.create_table( + "material_types", + sa.Column("key", sa.String(length=10), nullable=False), + sa.Column("type", sa.String(length=15), nullable=False), + sa.Column("description", sa.String(length=256), nullable=False), + sa.PrimaryKeyConstraint("key", name="material_types_pkey"), + schema="public", ) - op.create_table('pedimento_codes', - sa.Column('code', sa.String(length=3), nullable=False), - sa.Column('description', sa.String(length=250), nullable=False), - sa.PrimaryKeyConstraint('code', name='pedimento_codes_pkey'), - schema='public' + op.create_table( + "payment_methods", + sa.Column("key", sa.String(length=2), nullable=False), + sa.Column("description", sa.String(length=100), nullable=False), + sa.PrimaryKeyConstraint("key", name="payment_methods_pkey"), + schema="public", ) - op.create_table('pedimento_regimens', - sa.Column('code', sa.String(length=3), nullable=False), - sa.Column('description', sa.String(length=100), nullable=False), - sa.PrimaryKeyConstraint('code', name='pedimento_regimens_pkey'), - schema='public' + op.create_table( + "pedimento_codes", + sa.Column("code", sa.String(length=3), nullable=False), + sa.Column("description", sa.String(length=250), nullable=False), + sa.PrimaryKeyConstraint("code", name="pedimento_codes_pkey"), + schema="public", ) - op.create_table('sectors', - sa.Column('key', sa.String(length=8), nullable=False), - sa.Column('description', sa.String(length=150), nullable=False), - sa.Column('authorized', sa.SmallInteger(), nullable=False), - sa.PrimaryKeyConstraint('key', name='sectors_pkey'), - schema='public' + op.create_table( + "pedimento_regimens", + sa.Column("code", sa.String(length=3), nullable=False), + sa.Column("description", sa.String(length=100), nullable=False), + sa.PrimaryKeyConstraint("code", name="pedimento_regimens_pkey"), + schema="public", ) - op.create_table('states', - sa.Column('m3_key', sa.String(length=3), nullable=False), - sa.Column('description', sa.String(length=50), nullable=False), - sa.Column('mex_key', sa.String(length=3), nullable=True), - sa.Column('ame_key', sa.String(length=2), nullable=True), - sa.PrimaryKeyConstraint('m3_key', 'description', name='states_pkey'), - schema='public' + op.create_table( + "sectors", + sa.Column("key", sa.String(length=8), nullable=False), + sa.Column("description", sa.String(length=150), nullable=False), + sa.Column("authorized", sa.SmallInteger(), nullable=False), + sa.PrimaryKeyConstraint("key", name="sectors_pkey"), + schema="public", ) - op.create_table('transport_modes', - sa.Column('key', sa.String(length=3), nullable=False), - sa.Column('name', sa.String(length=30), nullable=False), - sa.PrimaryKeyConstraint('key', name='transport_modes_pkey'), - schema='public' + op.create_table( + "states", + sa.Column("m3_key", sa.String(length=3), nullable=False), + sa.Column("description", sa.String(length=50), nullable=False), + sa.Column("mex_key", sa.String(length=3), nullable=True), + sa.Column("ame_key", sa.String(length=2), nullable=True), + sa.PrimaryKeyConstraint("m3_key", "description", name="states_pkey"), + schema="public", ) - op.create_table('transport_types', - sa.Column('transport_code', sa.String(length=2), nullable=False), - sa.Column('description', sa.String(length=100), nullable=False), - sa.PrimaryKeyConstraint('transport_code', name='transport_types_pkey'), - schema='public' + op.create_table( + "transport_modes", + sa.Column("key", sa.String(length=3), nullable=False), + sa.Column("name", sa.String(length=30), nullable=False), + sa.PrimaryKeyConstraint("key", name="transport_modes_pkey"), + schema="public", ) - op.create_table('valuation_methods', - sa.Column('key', sa.String(length=2), nullable=False), - sa.Column('description', sa.String(length=200), nullable=False), - sa.PrimaryKeyConstraint('key', name='valuation_methods_pkey'), - schema='public' - ) - op.create_table('code_pedimento_regimens', - sa.Column('id', sa.Integer(), nullable=False), - sa.Column('pedimento_code', sa.String(length=3), nullable=False), - sa.Column('regimen_code', sa.String(length=3), nullable=False), - sa.Column('type_code', sa.String(length=1), nullable=True), - sa.ForeignKeyConstraint(['pedimento_code'], ['public.pedimento_codes.code'], name='fk_codeped'), - sa.ForeignKeyConstraint(['regimen_code'], ['public.pedimento_regimens.code'], name='fk_regimenped'), - sa.PrimaryKeyConstraint('id', name='clave_pedimento_regimens_pkey'), - schema='public' + op.create_table( + "transport_types", + sa.Column("transport_code", sa.String(length=2), nullable=False), + sa.Column("description", sa.String(length=100), nullable=False), + sa.PrimaryKeyConstraint("transport_code", name="transport_types_pkey"), + schema="public", + ) + op.create_table( + "valuation_methods", + sa.Column("key", sa.String(length=2), nullable=False), + sa.Column("description", sa.String(length=200), nullable=False), + sa.PrimaryKeyConstraint("key", name="valuation_methods_pkey"), + schema="public", + ) + op.create_table( + "code_pedimento_regimens", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("pedimento_code", sa.String(length=3), nullable=False), + sa.Column("regimen_code", sa.String(length=3), nullable=False), + sa.Column("type_code", sa.String(length=1), nullable=True), + sa.ForeignKeyConstraint( + ["pedimento_code"], ["public.pedimento_codes.code"], name="fk_codeped" + ), + sa.ForeignKeyConstraint( + ["regimen_code"], ["public.pedimento_regimens.code"], name="fk_regimenped" + ), + sa.PrimaryKeyConstraint("id", name="clave_pedimento_regimens_pkey"), + schema="public", ) # ### end Alembic commands ### @@ -146,22 +170,22 @@ def upgrade() -> None: def downgrade() -> None: """Downgrade schema.""" # ### commands auto generated by Alembic - please adjust! ### - op.drop_table('code_pedimento_regimens', schema='public') - op.drop_table('valuation_methods', schema='public') - op.drop_table('transport_types', schema='public') - op.drop_table('transport_modes', schema='public') - op.drop_table('states', schema='public') - op.drop_table('sectors', schema='public') - op.drop_table('pedimento_regimens', schema='public') - op.drop_table('pedimento_codes', schema='public') - op.drop_table('payment_methods', schema='public') - op.drop_table('material_types', schema='public') - op.drop_table('invoice_types', schema='public') - op.drop_table('incoterms', schema='public') - op.drop_table('customs_warehouses', schema='public') - op.drop_table('customs_sections', schema='public') - op.drop_table('currency_types', schema='public') - op.drop_index('ak_country_ame', table_name='countries', schema='public') - op.drop_table('countries', schema='public') - op.drop_table('containers', schema='public') + op.drop_table("code_pedimento_regimens", schema="public") + op.drop_table("valuation_methods", schema="public") + op.drop_table("transport_types", schema="public") + op.drop_table("transport_modes", schema="public") + op.drop_table("states", schema="public") + op.drop_table("sectors", schema="public") + op.drop_table("pedimento_regimens", schema="public") + op.drop_table("pedimento_codes", schema="public") + op.drop_table("payment_methods", schema="public") + op.drop_table("material_types", schema="public") + op.drop_table("invoice_types", schema="public") + op.drop_table("incoterms", schema="public") + op.drop_table("customs_warehouses", schema="public") + op.drop_table("customs_sections", schema="public") + op.drop_table("currency_types", schema="public") + op.drop_index("ak_country_ame", table_name="countries", schema="public") + op.drop_table("countries", schema="public") + op.drop_table("containers", schema="public") # ### end Alembic commands ### diff --git a/backend/alembic/versions/7937209f9718_seed_initial_data.py b/backend/alembic/versions/7937209f9718_seed_initial_data.py index 0d5cd591..7b0644c0 100644 --- a/backend/alembic/versions/7937209f9718_seed_initial_data.py +++ b/backend/alembic/versions/7937209f9718_seed_initial_data.py @@ -5,152 +5,293 @@ Revises: 531bf8cdae06 Create Date: 2025-10-19 18:23:55.258800 """ + from typing import Sequence, Union from alembic import op import sqlalchemy as sa -from api.v1.modules.public.reference_data.pedimento_codes.seed import seed as pedimento_codes_seed -from api.v1.modules.public.reference_data.pedimento_regimens.seed import seed as pedimento_regimens_seed -from api.v1.modules.public.reference_data.code_pedimento_regimens.seed import seed as code_pedimento_regimens_seed -from api.v1.modules.public.reference_data.containers.seed import seed as container_types_seed +from api.v1.modules.public.reference_data.pedimento_codes.seed import ( + seed as pedimento_codes_seed, +) +from api.v1.modules.public.reference_data.pedimento_regimens.seed import ( + seed as pedimento_regimens_seed, +) +from api.v1.modules.public.reference_data.code_pedimento_regimens.seed import ( + seed as code_pedimento_regimens_seed, +) +from api.v1.modules.public.reference_data.containers.seed import ( + seed as container_types_seed, +) from api.v1.modules.public.reference_data.countries.seed import seed as countries_seed -from api.v1.modules.public.reference_data.currency_types.seed import seed as currency_types_seed -from api.v1.modules.public.reference_data.customs_sections.seed import seed as customs_sections_seed -from api.v1.modules.public.reference_data.customs_warehouses.seed import seed as customs_warehouses_seed +from api.v1.modules.public.reference_data.currency_types.seed import ( + seed as currency_types_seed, +) +from api.v1.modules.public.reference_data.customs_sections.seed import ( + seed as customs_sections_seed, +) +from api.v1.modules.public.reference_data.customs_warehouses.seed import ( + seed as customs_warehouses_seed, +) from api.v1.modules.public.reference_data.incoterms.seed import seed as incoterms_seed -from api.v1.modules.public.reference_data.invoice_types.seed import seed as invoice_types_seed -from api.v1.modules.public.reference_data.material_types.seed import seed as material_types_seed -from api.v1.modules.public.reference_data.payment_methods.seed import seed as payment_methods_seed +from api.v1.modules.public.reference_data.invoice_types.seed import ( + seed as invoice_types_seed, +) +from api.v1.modules.public.reference_data.material_types.seed import ( + seed as material_types_seed, +) +from api.v1.modules.public.reference_data.payment_methods.seed import ( + seed as payment_methods_seed, +) from api.v1.modules.public.reference_data.sectors.seed import seed as sectors_seed -from api.v1.modules.public.reference_data.transport_modes.seed import seed as transport_modes_seed -from api.v1.modules.public.reference_data.transport_types.seed import seed as transport_types_seed -from api.v1.modules.public.reference_data.valuation_methods.seed import seed as valuation_methods_seed +from api.v1.modules.public.reference_data.transport_modes.seed import ( + seed as transport_modes_seed, +) +from api.v1.modules.public.reference_data.transport_types.seed import ( + seed as transport_types_seed, +) +from api.v1.modules.public.reference_data.valuation_methods.seed import ( + seed as valuation_methods_seed, +) # revision identifiers, used by Alembic. -revision: str = '7937209f9718' -down_revision: Union[str, Sequence[str], None] = '531bf8cdae06' +revision: str = "7937209f9718" +down_revision: Union[str, Sequence[str], None] = "531bf8cdae06" branch_labels: Union[str, Sequence[str], None] = None -depends_on: Union[str, Sequence[str], None] = '531bf8cdae06' +depends_on: Union[str, Sequence[str], None] = "531bf8cdae06" + def upgrade() -> None: """Upgrade schema.""" - #Seeds - values_pc = ", ".join([f"('{code}', '{desc.replace(chr(39), chr(39)*2)}')" for code, desc in pedimento_codes_seed]) - op.execute(f""" + # Seeds + values_pc = ", ".join( + [ + f"('{code}', '{desc.replace(chr(39), chr(39)*2)}')" + for code, desc in pedimento_codes_seed + ] + ) + op.execute( + f""" INSERT INTO pedimento_codes (code, description) VALUES {values_pc} ON CONFLICT (code) DO NOTHING; - """) - - values_pr = ", ".join([f"('{code}', '{desc.replace(chr(39), chr(39)*2)}')" for code, desc in pedimento_regimens_seed]) - op.execute(f""" + """ + ) + + values_pr = ", ".join( + [ + f"('{code}', '{desc.replace(chr(39), chr(39)*2)}')" + for code, desc in pedimento_regimens_seed + ] + ) + op.execute( + f""" INSERT INTO public.pedimento_regimens (code, description) VALUES {values_pr} ON CONFLICT (code) DO NOTHING; - """) - - values_cpr = ", ".join([f"('{ped_code}', '{reg_code}', '{type_code}')" for ped_code, reg_code, type_code in code_pedimento_regimens_seed]) - op.execute(f""" + """ + ) + + values_cpr = ", ".join( + [ + f"('{ped_code}', '{reg_code}', '{type_code}')" + for ped_code, reg_code, type_code in code_pedimento_regimens_seed + ] + ) + op.execute( + f""" INSERT INTO public.code_pedimento_regimens (pedimento_code, regimen_code, type_code) VALUES {values_cpr} - """) - - values_c = ", ".join([f"('{key}', '{desc.replace(chr(39), chr(39)*2)}')" for key, desc in container_types_seed]) - op.execute(f""" + """ + ) + + values_c = ", ".join( + [ + f"('{key}', '{desc.replace(chr(39), chr(39)*2)}')" + for key, desc in container_types_seed + ] + ) + op.execute( + f""" INSERT INTO public.containers (key, description) VALUES {values_c} ON CONFLICT (key) DO NOTHING; - """) - - values_country = ", ".join([f"('{m3_key}', '{mex_key}', '{ame_key}', '{desc_es.replace(chr(39), chr(39)*2)}', '{desc_en.replace(chr(39), chr(39)*2)}')" for m3_key, mex_key, ame_key, desc_es, desc_en in countries_seed]) - op.execute(f""" + """ + ) + + values_country = ", ".join( + [ + f"('{m3_key}', '{mex_key}', '{ame_key}', '{desc_es.replace(chr(39), chr(39)*2)}', '{desc_en.replace(chr(39), chr(39)*2)}')" + for m3_key, mex_key, ame_key, desc_es, desc_en in countries_seed + ] + ) + op.execute( + f""" INSERT INTO public.countries (m3_key, mex_key, ame_key, description_es, description_en) VALUES {values_country} ON CONFLICT (m3_key) DO NOTHING; - """) - - values_ct = ", ".join([f"('{code}', '{currency_name.replace(chr(39), chr(39)*2)}', '{country_desc.replace(chr(39), chr(39)*2)}')" for code, currency_name, country_desc in currency_types_seed]) - op.execute(f""" + """ + ) + + values_ct = ", ".join( + [ + f"('{code}', '{currency_name.replace(chr(39), chr(39)*2)}', '{country_desc.replace(chr(39), chr(39)*2)}')" + for code, currency_name, country_desc in currency_types_seed + ] + ) + op.execute( + f""" INSERT INTO public.currency_types (code, currency_name, country_description) VALUES {values_ct} ON CONFLICT (code) DO NOTHING; - """) - - values_cs = ", ".join([f"('{code}', '{name.replace(chr(39), chr(39)*2)}')" for code, name in customs_sections_seed]) - op.execute(f""" + """ + ) + + values_cs = ", ".join( + [ + f"('{code}', '{name.replace(chr(39), chr(39)*2)}')" + for code, name in customs_sections_seed + ] + ) + op.execute( + f""" INSERT INTO public.customs_sections (customs_code, section_name) VALUES {values_cs} ON CONFLICT (customs_code) DO NOTHING; - """) - - values_cw = ", ".join([f"('{key}', '{customs.replace(chr(39), chr(39)*2)}', '{fiscalized_warehouse.replace(chr(39), chr(39)*2)}')" for key, customs, fiscalized_warehouse in customs_warehouses_seed]) - op.execute(f""" + """ + ) + + values_cw = ", ".join( + [ + f"('{key}', '{customs.replace(chr(39), chr(39)*2)}', '{fiscalized_warehouse.replace(chr(39), chr(39)*2)}')" + for key, customs, fiscalized_warehouse in customs_warehouses_seed + ] + ) + op.execute( + f""" INSERT INTO public.customs_warehouses (key, customs, fiscalized_warehouse) VALUES {values_cw} ON CONFLICT (key, customs) DO NOTHING; - """) - - values_incoterms = ", ".join([f"('{code}', '{desc_es.replace(chr(39), chr(39)*2)}', '{desc_en.replace(chr(39), chr(39)*2)}')" for code, desc_es, desc_en in incoterms_seed]) - op.execute(f""" + """ + ) + + values_incoterms = ", ".join( + [ + f"('{code}', '{desc_es.replace(chr(39), chr(39)*2)}', '{desc_en.replace(chr(39), chr(39)*2)}')" + for code, desc_es, desc_en in incoterms_seed + ] + ) + op.execute( + f""" INSERT INTO public.incoterms (code, description_es, description_en) VALUES {values_incoterms} ON CONFLICT (code) DO NOTHING; - """) - - values_it = ", ".join([f"('{key}', '{desc.replace(chr(39), chr(39)*2)}', '{note.replace(chr(39), chr(39)*2)}', '{type.replace(chr(39), chr(39)*2)}')" for key, desc, note, type in invoice_types_seed]) - op.execute(f""" + """ + ) + + values_it = ", ".join( + [ + f"('{key}', '{desc.replace(chr(39), chr(39)*2)}', '{note.replace(chr(39), chr(39)*2)}', '{type.replace(chr(39), chr(39)*2)}')" + for key, desc, note, type in invoice_types_seed + ] + ) + op.execute( + f""" INSERT INTO public.invoice_types (key, description, note, type) VALUES {values_it} ON CONFLICT (key) DO NOTHING; - """) - - values_mt = ", ".join([f"('{key}', '{desc.replace(chr(39), chr(39)*2)}', '{category.replace(chr(39), chr(39)*2)}')" for key, desc, category in material_types_seed]) - op.execute(f""" + """ + ) + + values_mt = ", ".join( + [ + f"('{key}', '{desc.replace(chr(39), chr(39)*2)}', '{category.replace(chr(39), chr(39)*2)}')" + for key, desc, category in material_types_seed + ] + ) + op.execute( + f""" INSERT INTO public.material_types (key, description, type) VALUES {values_mt} ON CONFLICT (key) DO NOTHING; - """) + """ + ) - values_pm = ", ".join([f"('{key}', '{desc.replace(chr(39), chr(39)*2)}')" for key, desc in payment_methods_seed]) - op.execute(f""" + values_pm = ", ".join( + [ + f"('{key}', '{desc.replace(chr(39), chr(39)*2)}')" + for key, desc in payment_methods_seed + ] + ) + op.execute( + f""" INSERT INTO public.payment_methods (key, description) VALUES {values_pm} ON CONFLICT (key) DO NOTHING; - """) - - values_sectors = ", ".join([f"('{key}', '{desc.replace(chr(39), chr(39)*2)}', '{authorized}')" for key, desc, authorized in sectors_seed]) - op.execute(f""" + """ + ) + + values_sectors = ", ".join( + [ + f"('{key}', '{desc.replace(chr(39), chr(39)*2)}', '{authorized}')" + for key, desc, authorized in sectors_seed + ] + ) + op.execute( + f""" INSERT INTO public.sectors (key, description, authorized) VALUES {values_sectors} ON CONFLICT (key) DO NOTHING; - """) - - values_tm = ", ".join([f"('{key}', '{name.replace(chr(39), chr(39)*2)}')" for key, name in transport_modes_seed]) - op.execute(f""" + """ + ) + + values_tm = ", ".join( + [ + f"('{key}', '{name.replace(chr(39), chr(39)*2)}')" + for key, name in transport_modes_seed + ] + ) + op.execute( + f""" INSERT INTO public.transport_modes (key, name) VALUES {values_tm} ON CONFLICT (key) DO NOTHING; - """) - - values_tt = ", ".join([f"('{code}', '{desc.replace(chr(39), chr(39)*2)}')" for code, desc in transport_types_seed]) - op.execute(f""" + """ + ) + + values_tt = ", ".join( + [ + f"('{code}', '{desc.replace(chr(39), chr(39)*2)}')" + for code, desc in transport_types_seed + ] + ) + op.execute( + f""" INSERT INTO public.transport_types (transport_code, description) VALUES {values_tt} ON CONFLICT (transport_code) DO NOTHING; - """) - - values_vm = ", ".join([f"('{key}', '{desc.replace(chr(39), chr(39)*2)}')" for key, desc in valuation_methods_seed]) - op.execute(f""" + """ + ) + + values_vm = ", ".join( + [ + f"('{key}', '{desc.replace(chr(39), chr(39)*2)}')" + for key, desc in valuation_methods_seed + ] + ) + op.execute( + f""" INSERT INTO public.valuation_methods (key, description) VALUES {values_vm} ON CONFLICT (key) DO NOTHING; - """) - + """ + ) + + def downgrade() -> None: """Downgrade schema.""" - + op.execute("DELETE FROM public.valuation_methods;") op.execute("DELETE FROM public.transport_types;") op.execute("DELETE FROM public.transport_modes;") diff --git a/backend/api/v1/modules/a24/q/q_classes/models.py b/backend/api/v1/modules/a24/q/q_classes/models.py index 4a22b159..cf9491e8 100644 --- a/backend/api/v1/modules/a24/q/q_classes/models.py +++ b/backend/api/v1/modules/a24/q/q_classes/models.py @@ -1,32 +1,42 @@ from decimal import Decimal -from sqlalchemy import Boolean, ForeignKeyConstraint, Integer, Numeric, PrimaryKeyConstraint, String +from sqlalchemy import ( + Boolean, + ForeignKeyConstraint, + Integer, + Numeric, + PrimaryKeyConstraint, + String, +) from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm.base import Mapped from core.database import Base + class QClasses(Base): - __tablename__ = 'q_classes' #QClases - __table_args__ = ( - PrimaryKeyConstraint('id', name='qclases_pk'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_qclasses_tenants'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_qclasses_company'), - ForeignKeyConstraint(['class_id'], ['classes.id'], name='fk_qclasses_classes'), - {'schema': 'a24'} + __tablename__ = "q_classes" # QClases + __table_args__ = ( + PrimaryKeyConstraint("id", name="qclases_pk"), + ForeignKeyConstraint( + ["tenant_id"], ["a76.tenants.id"], name="fk_qclasses_tenants" + ), + ForeignKeyConstraint( + ["company_id"], ["a76.company.id"], name="fk_qclasses_company" + ), + ForeignKeyConstraint(["class_id"], ["classes.id"], name="fk_qclasses_classes"), + {"schema": "a24"}, ) id: Mapped[int] = mapped_column(Integer, primary_key=True) - class_id: Mapped[int] = mapped_column(Integer, nullable=False) + class_id: Mapped[int] = mapped_column(Integer, nullable=False) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=True, index=True) - - import_tariff_code: Mapped[str] = mapped_column(String(10)) #FRACCIONIMPO - import_tariff_type: Mapped[str] = mapped_column(String(6)) #TIPOFRACIMPO - export_tariff_code: Mapped[str] = mapped_column(String(10)) #FRACCIONEXPO - export_tariff_type: Mapped[str] = mapped_column(String(6)) #TIPOFRACEXPO - depreciation_rate: Mapped[Decimal] = mapped_column(Numeric(5, 2)) #TASADEPRECIA - fda_code: Mapped[str] = mapped_column(String(20)) #FDA - eccn_code: Mapped[str] = mapped_column(String(20)) #ECCN - class_enabled: Mapped[bool] = mapped_column(Boolean) #HABILITADESHABILITACLASE - + import_tariff_code: Mapped[str] = mapped_column(String(10)) # FRACCIONIMPO + import_tariff_type: Mapped[str] = mapped_column(String(6)) # TIPOFRACIMPO + export_tariff_code: Mapped[str] = mapped_column(String(10)) # FRACCIONEXPO + export_tariff_type: Mapped[str] = mapped_column(String(6)) # TIPOFRACEXPO + depreciation_rate: Mapped[Decimal] = mapped_column(Numeric(5, 2)) # TASADEPRECIA + fda_code: Mapped[str] = mapped_column(String(20)) # FDA + eccn_code: Mapped[str] = mapped_column(String(20)) # ECCN + class_enabled: Mapped[bool] = mapped_column(Boolean) # HABILITADESHABILITACLASE diff --git a/backend/api/v1/modules/a24/s/s_classes/models.py b/backend/api/v1/modules/a24/s/s_classes/models.py index 047a4e93..4619887c 100644 --- a/backend/api/v1/modules/a24/s/s_classes/models.py +++ b/backend/api/v1/modules/a24/s/s_classes/models.py @@ -1,21 +1,34 @@ -from sqlalchemy import ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint +from sqlalchemy import ( + ForeignKeyConstraint, + Integer, + PrimaryKeyConstraint, + String, + UniqueConstraint, +) from sqlalchemy.orm import Mapped, mapped_column from sqlalchemy.orm.base import Mapped from core.database import Base + class SClasses(Base): - __tablename__ = 's_classes' #SClases - __table_args__ = ( - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_sclasses_tenants'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_sclasses_company'), - ForeignKeyConstraint(['class_id'], ['a76.clases.class_id'], name='fk_sclasses_classes'), - PrimaryKeyConstraint('id', name='sclases_pk'), - {'schema': 'a24'} + __tablename__ = "s_classes" # SClases + __table_args__ = ( + ForeignKeyConstraint( + ["tenant_id"], ["a76.tenants.id"], name="fk_sclasses_tenants" + ), + ForeignKeyConstraint( + ["company_id"], ["a76.company.id"], name="fk_sclasses_company" + ), + ForeignKeyConstraint( + ["class_id"], ["a76.clases.class_id"], name="fk_sclasses_classes" + ), + PrimaryKeyConstraint("id", name="sclases_pk"), + {"schema": "a24"}, ) id: Mapped[int] = mapped_column(Integer, primary_key=True) - class_id: Mapped[int] = mapped_column(Integer, nullable=False) #Id de clase + class_id: Mapped[int] = mapped_column(Integer, nullable=False) # Id de clase tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=True, index=True) - - stock_um: Mapped[str] = mapped_column(String(5)) #Unidad de medida para existencia - us_tariff_code: Mapped[str] = mapped_column(String(19)) #Fracción americana (USA) \ No newline at end of file + + stock_um: Mapped[str] = mapped_column(String(5)) # Unidad de medida para existencia + us_tariff_code: Mapped[str] = mapped_column(String(19)) # Fracción americana (USA) diff --git a/backend/api/v1/modules/a76/auth/__init__.py b/backend/api/v1/modules/a76/auth/__init__.py index 08c9bf95..a4f62822 100644 --- a/backend/api/v1/modules/a76/auth/__init__.py +++ b/backend/api/v1/modules/a76/auth/__init__.py @@ -1,6 +1,7 @@ """ Módulo de Authentication """ + from .routes import router __all__ = ["router"] diff --git a/backend/api/v1/modules/a76/auth/dto.py b/backend/api/v1/modules/a76/auth/dto.py index 23f97533..a9544367 100644 --- a/backend/api/v1/modules/a76/auth/dto.py +++ b/backend/api/v1/modules/a76/auth/dto.py @@ -1,58 +1,63 @@ """ DTOs para módulo de autenticación """ + from pydantic import BaseModel, EmailStr, Field from typing import Optional class LoginRequestDTO(BaseModel): """DTO para solicitud de login""" + username: str = Field(..., description="Usuario o email") password: str = Field(..., min_length=6, description="Contraseña") tenant_slug: str = Field(..., description="Slug del tenant") - + class Config: json_schema_extra = { "example": { "username": "usuario@ejemplo.com", "password": "password123", - "tenant_slug": "empresa-abc" + "tenant_slug": "empresa-abc", } } class TokenResponseDTO(BaseModel): """DTO para respuesta de token""" + access_token: str refresh_token: str token_type: str = "bearer" expires_in: int - + class Config: json_schema_extra = { "example": { "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", "token_type": "bearer", - "expires_in": 3600 + "expires_in": 3600, } } class RefreshTokenRequestDTO(BaseModel): """DTO para solicitud de refresh token""" + refresh_token: str = Field(..., description="Refresh token") class UserInfoResponseDTO(BaseModel): """DTO para información de usuario""" + sub: str email: Optional[str] = None name: Optional[str] = None preferred_username: Optional[str] = None tenant_id: Optional[int] = None roles: list[str] = [] - + class Config: json_schema_extra = { "example": { @@ -61,25 +66,29 @@ class UserInfoResponseDTO(BaseModel): "name": "Juan Pérez", "preferred_username": "jperez", "tenant_id": 1, - "roles": ["user", "admin"] + "roles": ["user", "admin"], } } class LogoutRequestDTO(BaseModel): """DTO para solicitud de logout""" + refresh_token: str = Field(..., description="Refresh token para invalidar") class RegisterRequestDTO(BaseModel): """DTO para solicitud de registro""" - username: str = Field(..., min_length=3, max_length=50, description="Nombre de usuario") + + username: str = Field( + ..., min_length=3, max_length=50, description="Nombre de usuario" + ) email: EmailStr = Field(..., description="Email del usuario") password: str = Field(..., min_length=8, description="Contraseña") first_name: str = Field(..., min_length=2, max_length=50, description="Nombre") last_name: str = Field(..., min_length=2, max_length=50, description="Apellido") tenant_slug: str = Field(..., description="Slug del tenant") - + class Config: json_schema_extra = { "example": { @@ -88,54 +97,57 @@ class RegisterRequestDTO(BaseModel): "password": "MiPassword123!", "first_name": "Juan", "last_name": "Pérez", - "tenant_slug": "empresa-abc" + "tenant_slug": "empresa-abc", } } class RegisterResponseDTO(BaseModel): """DTO para respuesta de registro""" + user_id: str username: str email: str message: str - + class Config: json_schema_extra = { "example": { "user_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "username": "jperez", "email": "jperez@ejemplo.com", - "message": "User registered successfully" + "message": "User registered successfully", } } class ExchangeCodeRequestDTO(BaseModel): """DTO para intercambiar authorization code por tokens (OAuth2 flow)""" + code: str = Field(..., description="Authorization code de OAuth2") redirect_uri: str = Field(..., description="Redirect URI usado en la autorización") tenant_slug: Optional[str] = Field(None, description="Slug del tenant (opcional)") - + class Config: json_schema_extra = { "example": { "code": "eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2Ii...", "redirect_uri": "http://localhost:5173/auth/callback", - "tenant_slug": "empresa-abc" + "tenant_slug": "empresa-abc", } } class SetCookieRequestDTO(BaseModel): """DTO para establecer cookies de autenticación""" + access_token: str = Field(..., description="Access token JWT") refresh_token: str = Field(..., description="Refresh token JWT") - + class Config: json_schema_extra = { "example": { "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", - "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9..." + "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...", } } diff --git a/backend/api/v1/modules/a76/auth/routes.py b/backend/api/v1/modules/a76/auth/routes.py index d0bf28ff..ea21e101 100644 --- a/backend/api/v1/modules/a76/auth/routes.py +++ b/backend/api/v1/modules/a76/auth/routes.py @@ -1,6 +1,7 @@ """ Endpoints API para autenticación """ + from fastapi import APIRouter, Depends, HTTPException, Response from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from sqlalchemy.orm import Session @@ -16,7 +17,7 @@ from .dto import ( RegisterRequestDTO, RegisterResponseDTO, ExchangeCodeRequestDTO, - SetCookieRequestDTO + SetCookieRequestDTO, ) from .service import AuthService @@ -26,12 +27,11 @@ security = HTTPBearer() @router.post("/register", response_model=RegisterResponseDTO, status_code=201) async def register( - register_data: RegisterRequestDTO, - db: Session = Depends(get_core_db) + register_data: RegisterRequestDTO, db: Session = Depends(get_core_db) ): """ Registra un nuevo usuario en Keycloak - + El usuario debe proporcionar: - username: Nombre de usuario único - email: Email único @@ -39,7 +39,7 @@ async def register( - first_name: Nombre - last_name: Apellido - tenant_slug: Slug del tenant al que pertenece - + El usuario se crea automáticamente en Keycloak con: - Cuenta habilitada - Rol 'user' asignado por defecto @@ -50,13 +50,10 @@ async def register( @router.post("/login", response_model=TokenResponseDTO) -async def login( - login_data: LoginRequestDTO, - db: Session = Depends(get_core_db) -): +async def login(login_data: LoginRequestDTO, db: Session = Depends(get_core_db)): """ Autentica usuario con Keycloak y retorna tokens JWT - + El usuario debe proporcionar: - username: Usuario o email - password: Contraseña @@ -68,8 +65,7 @@ async def login( @router.post("/refresh", response_model=TokenResponseDTO) async def refresh_token( - refresh_data: RefreshTokenRequestDTO, - db: Session = Depends(get_core_db) + refresh_data: RefreshTokenRequestDTO, db: Session = Depends(get_core_db) ): """ Refresca el access token usando el refresh token @@ -81,7 +77,7 @@ async def refresh_token( @router.get("/me", response_model=UserInfoResponseDTO) async def get_current_user_info( credentials: HTTPAuthorizationCredentials = Depends(security), - db: Session = Depends(get_core_db) + db: Session = Depends(get_core_db), ): """ Obtiene información del usuario actual desde el token @@ -94,7 +90,7 @@ async def get_current_user_info( async def logout( logout_data: LogoutRequestDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Cierra sesión invalidando el refresh token @@ -105,15 +101,14 @@ async def logout( @router.post("/exchange-code", response_model=TokenResponseDTO) async def exchange_code( - exchange_data: ExchangeCodeRequestDTO, - db: Session = Depends(get_core_db) + exchange_data: ExchangeCodeRequestDTO, db: Session = Depends(get_core_db) ): """ Intercambia un authorization code de OAuth2 por tokens - + Este endpoint es útil cuando el frontend usa el flujo de autorización con proveedores externos (Microsoft, Google, etc.) a través de Keycloak. - + El código se obtiene después de que el usuario se autentica con el proveedor externo y Keycloak lo redirige al frontend con el código en los query params. """ @@ -125,15 +120,15 @@ async def exchange_code( async def set_cookie( cookie_data: SetCookieRequestDTO, response: Response, - db: Session = Depends(get_core_db) + db: Session = Depends(get_core_db), ): """ Establece cookies HttpOnly con los tokens de autenticación - + Este endpoint se llama desde el frontend después de una autenticación SSO exitosa para establecer las cookies de sesión necesarias para la validación server-side en los layouts protegidos. - + Las cookies se configuran como: - HttpOnly: No accesibles desde JavaScript (mayor seguridad) - Secure: Solo se envían por HTTPS (en producción) @@ -145,7 +140,7 @@ async def set_cookie( try: # Validar el access token user_info = service.get_user_info(cookie_data.access_token) - + # Establecer las cookies # Access token cookie response.set_cookie( @@ -155,9 +150,9 @@ async def set_cookie( secure=False, # TODO: Cambiar a True en producción con HTTPS samesite="lax", # Protección CSRF max_age=3600, # 1 hora (ajustar según configuración del token) - path="/" + path="/", ) - + # Refresh token cookie response.set_cookie( key="refresh_token", @@ -166,17 +161,14 @@ async def set_cookie( secure=False, # TODO: Cambiar a True en producción con HTTPS samesite="lax", max_age=86400, # 24 horas (ajustar según configuración del token) - path="/" + path="/", ) - + return { "success": True, "message": "Cookies establecidas correctamente", - "user": user_info + "user": user_info, } - + except Exception as e: - raise HTTPException( - status_code=400, - detail=f"Error validando tokens: {str(e)}" - ) \ No newline at end of file + raise HTTPException(status_code=400, detail=f"Error validando tokens: {str(e)}") diff --git a/backend/api/v1/modules/a76/auth/service.py b/backend/api/v1/modules/a76/auth/service.py index e5e6f1a1..4c19bfc2 100644 --- a/backend/api/v1/modules/a76/auth/service.py +++ b/backend/api/v1/modules/a76/auth/service.py @@ -1,6 +1,7 @@ """ Servicio de autenticación con Keycloak """ + from keycloak import KeycloakOpenID, KeycloakAdmin from keycloak.exceptions import KeycloakError from fastapi import HTTPException @@ -15,7 +16,7 @@ from .dto import ( UserInfoResponseDTO, LogoutRequestDTO, RegisterRequestDTO, - RegisterResponseDTO + RegisterResponseDTO, ) logger = logging.getLogger(__name__) @@ -23,26 +24,26 @@ logger = logging.getLogger(__name__) class AuthService: """Servicio de autenticación""" - + def __init__(self, db: Session): self.db = db self.keycloak_openid = KeycloakOpenID( server_url=settings.KEYCLOAK_SERVER_URL, client_id=settings.KEYCLOAK_CLIENT_ID, realm_name=settings.KEYCLOAK_REALM, - client_secret_key=settings.KEYCLOAK_CLIENT_SECRET + client_secret_key=settings.KEYCLOAK_CLIENT_SECRET, ) - + def login(self, login_data: LoginRequestDTO) -> TokenResponseDTO: """ Autentica usuario y obtiene tokens - + Args: login_data: Credenciales de login - + Returns: TokenResponseDTO con access_token y refresh_token - + Raises: HTTPException: Si las credenciales son inválidas """ @@ -50,47 +51,50 @@ class AuthService: # Verificar que el tenant existe from api.v1.modules.a76.tenants.service import TenantService from api.v1.modules.a76.user_tenant.service import UserTenantService - + tenant_service = TenantService(self.db) user_tenant_service = UserTenantService(self.db) tenant = tenant_service.get_tenant_by_slug(login_data.tenant_slug) - + if not tenant: raise HTTPException(status_code=404, detail="Tenant not found") - + if not tenant.is_active: raise HTTPException(status_code=403, detail="Tenant is not active") - + # Crear nueva instancia de KeycloakOpenID con el realm del tenant keycloak_client = KeycloakOpenID( server_url=settings.KEYCLOAK_SERVER_URL, client_id=settings.KEYCLOAK_CLIENT_ID, realm_name=tenant.keycloak_realm, - client_secret_key=settings.KEYCLOAK_CLIENT_SECRET + client_secret_key=settings.KEYCLOAK_CLIENT_SECRET, ) - + # Obtener token de Keycloak token_response = keycloak_client.token( username=login_data.username, password=login_data.password, - grant_type=["password"] + grant_type=["password"], ) - + # Obtener información del usuario y verificar acceso al tenant user_info = keycloak_client.userinfo(token_response["access_token"]) user_id = user_info.get("sub") - + if user_id: # Verificar si el usuario tiene acceso a este tenant - has_access = user_tenant_service.user_has_access_to_tenant(user_id, tenant.id) - + has_access = user_tenant_service.user_has_access_to_tenant( + user_id, tenant.id + ) + if not has_access: - logger.warning(f"User {user_id} tried to access tenant {tenant.id} without permission") - raise HTTPException( - status_code=403, - detail="You don't have access to this tenant" + logger.warning( + f"User {user_id} tried to access tenant {tenant.id} without permission" ) - + raise HTTPException( + status_code=403, detail="You don't have access to this tenant" + ) + # Actualizar el tenant_id del usuario en Keycloak basado en el slug usado try: # Crear instancia de KeycloakAdmin para actualizar atributos @@ -100,19 +104,19 @@ class AuthService: password=settings.KEYCLOAK_ADMIN_PASSWORD, realm_name=tenant.keycloak_realm, user_realm_name="master", - verify=True + verify=True, ) - + # Obtener los datos actuales del usuario para no sobrescribirlos current_user = keycloak_admin.get_user(user_id) - + # Obtener los atributos actuales o crear un dict vacío current_attributes = current_user.get("attributes", {}) - + # Actualizar solo los atributos de tenant current_attributes["tenant_id"] = [str(tenant.id)] current_attributes["tenant_slug"] = [tenant.slug] - + # Actualizar el usuario enviando TODOS los campos para evitar que se borren update_payload = { "email": current_user.get("email"), @@ -120,23 +124,25 @@ class AuthService: "lastName": current_user.get("lastName"), "enabled": current_user.get("enabled", True), "emailVerified": current_user.get("emailVerified", False), - "attributes": current_attributes + "attributes": current_attributes, } - + keycloak_admin.update_user(user_id=user_id, payload=update_payload) - logger.info(f"Updated tenant_id={tenant.id} for user {login_data.username}") - + logger.info( + f"Updated tenant_id={tenant.id} for user {login_data.username}" + ) + except Exception as e: # No queremos que falle el login si no se puede actualizar el atributo logger.warning(f"Could not update tenant_id attribute: {str(e)}") - + return TokenResponseDTO( access_token=token_response["access_token"], refresh_token=token_response["refresh_token"], token_type="bearer", - expires_in=token_response["expires_in"] + expires_in=token_response["expires_in"], ) - + except KeycloakError as e: logger.warning(f"Keycloak authentication failed: {str(e)}") raise HTTPException(status_code=401, detail="Invalid credentials") @@ -145,14 +151,14 @@ class AuthService: except Exception as e: logger.error(f"Login error: {str(e)}") raise HTTPException(status_code=500, detail="Authentication error") - + def refresh_token(self, refresh_data: RefreshTokenRequestDTO) -> TokenResponseDTO: """ Refresca el access token usando refresh token - + Args: refresh_data: Refresh token - + Returns: TokenResponseDTO con nuevos tokens """ @@ -160,67 +166,69 @@ class AuthService: token_response = self.keycloak_openid.refresh_token( refresh_data.refresh_token ) - + return TokenResponseDTO( access_token=token_response["access_token"], refresh_token=token_response["refresh_token"], token_type="bearer", - expires_in=token_response["expires_in"] + expires_in=token_response["expires_in"], ) - + except KeycloakError as e: logger.warning(f"Token refresh failed: {str(e)}") - raise HTTPException(status_code=401, detail="Invalid or expired refresh token") + raise HTTPException( + status_code=401, detail="Invalid or expired refresh token" + ) except Exception as e: logger.error(f"Token refresh error: {str(e)}") raise HTTPException(status_code=500, detail="Token refresh error") - + def get_user_info(self, access_token: str) -> UserInfoResponseDTO: """ Obtiene información del usuario desde el token - + Args: access_token: Access token JWT - + Returns: UserInfoResponseDTO con información del usuario """ try: user_info = self.keycloak_openid.userinfo(access_token) - + # Extraer roles roles = [] if "realm_access" in user_info: roles = user_info["realm_access"].get("roles", []) - + # Extraer tenant_id si está presente tenant_id = user_info.get("tenant_id") if not tenant_id and "attributes" in user_info: tenant_id = user_info["attributes"].get("tenant_id") - + return UserInfoResponseDTO( sub=user_info.get("sub"), email=user_info.get("email"), name=user_info.get("name"), preferred_username=user_info.get("preferred_username"), tenant_id=int(tenant_id) if tenant_id else None, - roles=roles + roles=roles, ) - + except KeycloakError as e: logger.warning(f"Get user info failed: {str(e)}") raise HTTPException(status_code=401, detail="Invalid token") except Exception as e: logger.error(f"Get user info error: {str(e)}") raise HTTPException(status_code=500, detail="Error retrieving user info") - + def logout(self, logout_data: LogoutRequestDTO) -> dict: """ Cierra sesión invalidando el refresh token - + Args: logout_data: Refresh token a invalidar - + Returns: Dict con mensaje de éxito """ @@ -228,7 +236,7 @@ class AuthService: self.keycloak_openid.logout(logout_data.refresh_token) logger.info("User logged out successfully") return {"message": "Logged out successfully"} - + except KeycloakError as e: logger.warning(f"Logout failed: {str(e)}") # No lanzamos error aquí, el logout puede fallar si el token ya expiró @@ -236,32 +244,33 @@ class AuthService: except Exception as e: logger.error(f"Logout error: {str(e)}") raise HTTPException(status_code=500, detail="Logout error") - + def register(self, register_data: RegisterRequestDTO) -> RegisterResponseDTO: """ Registra un nuevo usuario en Keycloak - + Args: register_data: Datos del usuario a registrar - + Returns: RegisterResponseDTO con información del usuario creado - + Raises: HTTPException: Si el registro falla """ try: # Verificar que el tenant existe from api.v1.modules.a76.tenants.service import TenantService + tenant_service = TenantService(self.db) tenant = tenant_service.get_tenant_by_slug(register_data.tenant_slug) - + if not tenant: raise HTTPException(status_code=404, detail="Tenant not found") - + if not tenant.is_active: raise HTTPException(status_code=403, detail="Tenant is not active") - + # Crear instancia de KeycloakAdmin para gestión de usuarios keycloak_admin = KeycloakAdmin( server_url=settings.KEYCLOAK_SERVER_URL, @@ -269,9 +278,9 @@ class AuthService: password=settings.KEYCLOAK_ADMIN_PASSWORD, realm_name=tenant.keycloak_realm, user_realm_name="master", # El admin suele estar en master realm - verify=True + verify=True, ) - + # Preparar datos del usuario para Keycloak user_data = { "username": register_data.username, @@ -280,20 +289,19 @@ class AuthService: "lastName": register_data.last_name, "enabled": True, "emailVerified": False, - "credentials": [{ - "type": "password", - "value": register_data.password, - "temporary": False - }], - "attributes": { - "tenant_id": str(tenant.id), - "tenant_slug": tenant.slug - } + "credentials": [ + { + "type": "password", + "value": register_data.password, + "temporary": False, + } + ], + "attributes": {"tenant_id": str(tenant.id), "tenant_slug": tenant.slug}, } - + # Crear usuario en Keycloak user_id = keycloak_admin.create_user(user_data) - + # Asignar rol por defecto (user) - opcional, solo si existe try: user_role = keycloak_admin.get_realm_role("user") @@ -303,15 +311,16 @@ class AuthService: except KeycloakError as e: # El rol 'user' no existe, no es un error crítico logger.warning(f"Could not assign 'user' role: {str(e)}") - + # Agregar el usuario al tenant en la base de datos try: from api.v1.modules.a76.user_tenant.service import UserTenantService + user_tenant_service = UserTenantService(self.db) user_tenant_service.add_user_to_tenant( keycloak_user_id=user_id, tenant_id=tenant.id, - role="user" # Rol por defecto + role="user", # Rol por defecto ) logger.info(f"Added user {user_id} to tenant {tenant.id} in database") except Exception as e: @@ -323,106 +332,116 @@ class AuthService: except: pass raise HTTPException( - status_code=500, - detail="Failed to register user in database" + status_code=500, detail="Failed to register user in database" ) - - logger.info(f"User registered: {register_data.username} (tenant: {tenant.slug}, user_id: {user_id})") - + + logger.info( + f"User registered: {register_data.username} (tenant: {tenant.slug}, user_id: {user_id})" + ) + return RegisterResponseDTO( user_id=user_id, username=register_data.username, email=register_data.email, - message="User registered successfully" + message="User registered successfully", ) - + except KeycloakError as e: error_message = str(e) logger.warning(f"Keycloak registration failed: {error_message}") - + # Mensajes de error más específicos if "User exists" in error_message or "409" in error_message: - raise HTTPException(status_code=409, detail="Username or email already exists") + raise HTTPException( + status_code=409, detail="Username or email already exists" + ) elif "Invalid" in error_message: raise HTTPException(status_code=400, detail="Invalid user data") else: raise HTTPException(status_code=500, detail="Registration error") - + except HTTPException: raise except Exception as e: logger.error(f"Registration error: {str(e)}") raise HTTPException(status_code=500, detail="Registration error") - + def exchange_code(self, exchange_data) -> TokenResponseDTO: """ Intercambia un authorization code por tokens - + Este método se usa cuando el frontend recibe un código de autorización después de un login con proveedor externo (Microsoft, Google, etc.) a través de Keycloak. - + Args: exchange_data: Datos del código y redirect_uri - + Returns: TokenResponseDTO con access_token y refresh_token - + Raises: HTTPException: Si el código es inválido o expiró """ try: # Importar el DTO aquí para evitar referencias circulares from .dto import ExchangeCodeRequestDTO - + # Intercambiar código por tokens usando Keycloak token_response = self.keycloak_openid.token( - grant_type='authorization_code', + grant_type="authorization_code", code=exchange_data.code, - redirect_uri=exchange_data.redirect_uri + redirect_uri=exchange_data.redirect_uri, ) - + logger.info(f"Code exchanged successfully") - + # Si se proporciona tenant_slug, podríamos validar que el usuario pertenece a ese tenant # Por ahora simplemente retornamos los tokens if exchange_data.tenant_slug: # Decodificar token para obtener tenant_id del usuario - user_info = self.keycloak_openid.introspect(token_response['access_token']) - user_tenant_id = user_info.get('tenant_id') - + user_info = self.keycloak_openid.introspect( + token_response["access_token"] + ) + user_tenant_id = user_info.get("tenant_id") + # Validar que el tenant existe y está activo from api.v1.modules.a76.tenants.service import TenantService + tenant_service = TenantService(self.db) tenant = tenant_service.get_tenant_by_slug(exchange_data.tenant_slug) - + if not tenant: raise HTTPException(status_code=404, detail="Tenant not found") - + if not tenant.is_active: raise HTTPException(status_code=403, detail="Tenant is not active") - + # Opcional: Verificar que el usuario pertenece al tenant # Esto depende de cómo manejes los tenants en tu aplicación - + return TokenResponseDTO( - access_token=token_response['access_token'], - refresh_token=token_response['refresh_token'], - token_type=token_response.get('token_type', 'bearer'), - expires_in=token_response.get('expires_in', 3600) + access_token=token_response["access_token"], + refresh_token=token_response["refresh_token"], + token_type=token_response.get("token_type", "bearer"), + expires_in=token_response.get("expires_in", 3600), ) - + except KeycloakError as e: error_message = str(e) logger.warning(f"Code exchange failed: {error_message}") - + if "invalid_grant" in error_message.lower(): - raise HTTPException(status_code=400, detail="Invalid or expired authorization code") + raise HTTPException( + status_code=400, detail="Invalid or expired authorization code" + ) elif "invalid_client" in error_message.lower(): - raise HTTPException(status_code=401, detail="Invalid client credentials") + raise HTTPException( + status_code=401, detail="Invalid client credentials" + ) else: raise HTTPException(status_code=500, detail="Token exchange error") - + except HTTPException: raise except Exception as e: diff --git a/backend/api/v1/modules/a76/classes/__init__.py b/backend/api/v1/modules/a76/classes/__init__.py index 8d0a42ce..1a3d8bed 100644 --- a/backend/api/v1/modules/a76/classes/__init__.py +++ b/backend/api/v1/modules/a76/classes/__init__.py @@ -1,6 +1,7 @@ """ Módulo de Class """ + from .routes import router __all__ = ["router"] diff --git a/backend/api/v1/modules/a76/classes/dto.py b/backend/api/v1/modules/a76/classes/dto.py index 7607c48e..b67cc6ef 100644 --- a/backend/api/v1/modules/a76/classes/dto.py +++ b/backend/api/v1/modules/a76/classes/dto.py @@ -2,6 +2,7 @@ DTOs (Data Transfer Objects) para módulo de clases SCAII y SCAF Reemplaza schemas.py siguiendo enfoque DDD y estilo NestJS """ + from pydantic import BaseModel, Field from typing import Optional from datetime import datetime @@ -9,17 +10,38 @@ from datetime import datetime class ClassCreateDTO(BaseModel): """DTO para crear una clase""" + client_id: int = Field(..., description="Client key") class_code: str = Field(..., max_length=8, description="Class code") - description_spanish: Optional[str] = Field(None, max_length=500, description="Description in Spanish") - description_english: Optional[str] = Field(None, max_length=500, description="Description in English") - material_key: Optional[str] = Field(None, max_length=10, description="Material key (homologated TIPOMAT/TIPOMATEQUIPO)") - unit_of_measure: Optional[str] = Field(None, max_length=5, description="Unit of measure (homologated UNIMEDIDA)") - fraction: Optional[str] = Field(None, max_length=10, description="Mexican tariff fraction") - us_fraction: Optional[str] = Field(None, max_length=16, description="US tariff fraction") - sub_key: Optional[str] = Field(None, max_length=5, description="Sub classification key") - physical_review: Optional[int] = Field(None, description="Physical review indicator") - iva_exempt_fraction: Optional[str] = Field(None, max_length=4, description="IVA exempt fraction") + description_spanish: Optional[str] = Field( + None, max_length=500, description="Description in Spanish" + ) + description_english: Optional[str] = Field( + None, max_length=500, description="Description in English" + ) + material_key: Optional[str] = Field( + None, + max_length=10, + description="Material key (homologated TIPOMAT/TIPOMATEQUIPO)", + ) + unit_of_measure: Optional[str] = Field( + None, max_length=5, description="Unit of measure (homologated UNIMEDIDA)" + ) + fraction: Optional[str] = Field( + None, max_length=10, description="Mexican tariff fraction" + ) + us_fraction: Optional[str] = Field( + None, max_length=16, description="US tariff fraction" + ) + sub_key: Optional[str] = Field( + None, max_length=5, description="Sub classification key" + ) + physical_review: Optional[int] = Field( + None, description="Physical review indicator" + ) + iva_exempt_fraction: Optional[str] = Field( + None, max_length=4, description="IVA exempt fraction" + ) class Config: from_attributes = True @@ -27,15 +49,36 @@ class ClassCreateDTO(BaseModel): class ClassUpdateDTO(BaseModel): """DTO para actualizar una clase""" - description_spanish: Optional[str] = Field(None, max_length=500, description="Description in Spanish") - description_english: Optional[str] = Field(None, max_length=500, description="Description in English") - material_key: Optional[str] = Field(None, max_length=10, description="Material key (homologated TIPOMAT/TIPOMATEQUIPO)") - unit_of_measure: Optional[str] = Field(None, max_length=5, description="Unit of measure (homologated UNIMEDIDA)") - fraction: Optional[str] = Field(None, max_length=10, description="Mexican tariff fraction") - us_fraction: Optional[str] = Field(None, max_length=16, description="US tariff fraction") - sub_key: Optional[str] = Field(None, max_length=5, description="Sub classification key") - physical_review: Optional[int] = Field(None, description="Physical review indicator") - iva_exempt_fraction: Optional[str] = Field(None, max_length=4, description="IVA exempt fraction") + + description_spanish: Optional[str] = Field( + None, max_length=500, description="Description in Spanish" + ) + description_english: Optional[str] = Field( + None, max_length=500, description="Description in English" + ) + material_key: Optional[str] = Field( + None, + max_length=10, + description="Material key (homologated TIPOMAT/TIPOMATEQUIPO)", + ) + unit_of_measure: Optional[str] = Field( + None, max_length=5, description="Unit of measure (homologated UNIMEDIDA)" + ) + fraction: Optional[str] = Field( + None, max_length=10, description="Mexican tariff fraction" + ) + us_fraction: Optional[str] = Field( + None, max_length=16, description="US tariff fraction" + ) + sub_key: Optional[str] = Field( + None, max_length=5, description="Sub classification key" + ) + physical_review: Optional[int] = Field( + None, description="Physical review indicator" + ) + iva_exempt_fraction: Optional[str] = Field( + None, max_length=4, description="IVA exempt fraction" + ) class Config: from_attributes = True @@ -43,6 +86,7 @@ class ClassUpdateDTO(BaseModel): class ClassResponseDTO(BaseModel): """DTO para respuesta de clase""" + client_id: int class_code: str description_spanish: Optional[str] = None @@ -61,6 +105,7 @@ class ClassResponseDTO(BaseModel): class ClassBasicDTO(BaseModel): """DTO para información básica de clase""" + client_id: int class_code: str description_spanish: Optional[str] = None @@ -74,6 +119,7 @@ class ClassBasicDTO(BaseModel): class ClassListDTO(BaseModel): """DTO para lista de clases""" + classes: list[ClassBasicDTO] total: int page: int @@ -85,13 +131,15 @@ class ClassListDTO(BaseModel): class ClassSearchDTO(BaseModel): """DTO para búsqueda de clases""" + client_id: Optional[int] = Field(None, description="Filter by client key") class_code: Optional[str] = Field(None, description="Search by class code") description: Optional[str] = Field(None, description="Search in descriptions") material_key: Optional[str] = Field(None, description="Filter by material key") fraction: Optional[str] = Field(None, description="Filter by tariff fraction") - physical_review: Optional[int] = Field(None, description="Filter by physical review indicator") + physical_review: Optional[int] = Field( + None, description="Filter by physical review indicator" + ) class Config: from_attributes = True - diff --git a/backend/api/v1/modules/a76/classes/models.py b/backend/api/v1/modules/a76/classes/models.py index babb9994..65980816 100644 --- a/backend/api/v1/modules/a76/classes/models.py +++ b/backend/api/v1/modules/a76/classes/models.py @@ -1,8 +1,17 @@ """ Modelos ORM para gestión de clases SCAII y SCAF """ + from typing import TYPE_CHECKING, Optional -from sqlalchemy import Integer, String, SmallInteger, ForeignKey, PrimaryKeyConstraint, ForeignKeyConstraint, UniqueConstraint +from sqlalchemy import ( + Integer, + String, + SmallInteger, + ForeignKey, + PrimaryKeyConstraint, + ForeignKeyConstraint, + UniqueConstraint, +) from sqlalchemy.orm import Mapped, mapped_column, relationship from core.database import Base @@ -15,54 +24,78 @@ class Class(Base): """ Modelo para la tabla GClases - Información de clases en sistemas SCAII y SCAF """ + __tablename__ = "classes" __table_args__ = ( - PrimaryKeyConstraint('id', name='classes_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_classes_tenant'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_classes_company'), - ForeignKeyConstraint(['client_id'], ['a76.client_provider.id'], name='fk_classes_client'), - ForeignKeyConstraint(['material_key'], ['public.material_types.key'], name='fk_classes_material_type'), - UniqueConstraint('tenant_id', 'company_id', 'class_code', name='uq_classes_client_id_class_code'), - {"schema": "a76"} - ) - + PrimaryKeyConstraint("id", name="classes_pkey"), + ForeignKeyConstraint( + ["tenant_id"], ["a76.tenants.id"], name="fk_classes_tenant" + ), + ForeignKeyConstraint( + ["company_id"], ["a76.company.id"], name="fk_classes_company" + ), + ForeignKeyConstraint( + ["client_id"], ["a76.client_provider.id"], name="fk_classes_client" + ), + ForeignKeyConstraint( + ["material_key"], + ["public.material_types.key"], + name="fk_classes_material_type", + ), + UniqueConstraint( + "tenant_id", + "company_id", + "class_code", + name="uq_classes_client_id_class_code", + ), + {"schema": "a76"}, + ) + id: Mapped[int] = mapped_column(Integer, primary_key=True) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - client_id: Mapped[int] = mapped_column(Integer) - - # Unique constraint compuesta - class_code: Mapped[str] = mapped_column(String(8)) #CLASE - + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + client_id: Mapped[int] = mapped_column(Integer) + + # Unique constraint compuesta + class_code: Mapped[str] = mapped_column(String(8)) # CLASE + # Basic information - description_es: Mapped[Optional[str]] = mapped_column(String(500)) #DESCRIPCIONE - description_en: Mapped[Optional[str]] = mapped_column(String(500)) #DESCRIPCIONI - + description_es: Mapped[Optional[str]] = mapped_column(String(500)) # DESCRIPCIONE + description_en: Mapped[Optional[str]] = mapped_column(String(500)) # DESCRIPCIONI + # Material and measurement - material_key: Mapped[Optional[str]] = mapped_column(String(10), ForeignKey('public.material_types.key')) # CLAVEMAT - homologated from TIPOMAT/TIPOMATEQUIPO - unit_of_measure: Mapped[Optional[str]] = mapped_column(String(5)) # UNIMED - homologated from UNIMEDIDA - + material_key: Mapped[Optional[str]] = mapped_column( + String(10), ForeignKey("public.material_types.key") + ) # CLAVEMAT - homologated from TIPOMAT/TIPOMATEQUIPO + unit_of_measure: Mapped[Optional[str]] = mapped_column( + String(5) + ) # UNIMED - homologated from UNIMEDIDA + # Tariff fractions fraction: Mapped[Optional[str]] = mapped_column(String(10)) # FRACCION - us_fraction: Mapped[Optional[str]] = mapped_column(String(16)) # FRACCIONAME - US tariff fraction - + us_fraction: Mapped[Optional[str]] = mapped_column( + String(16) + ) # FRACCIONAME - US tariff fraction + # Additional classification sub_key: Mapped[Optional[str]] = mapped_column(String(5)) # CLAVESUB physical_review: Mapped[Optional[int]] = mapped_column(SmallInteger) # REVFISICA - iva_exempt_fraction: Mapped[Optional[str]] = mapped_column(String(4)) # FRACCIONEXENTAIVA - + iva_exempt_fraction: Mapped[Optional[str]] = mapped_column( + String(4) + ) # FRACCIONEXENTAIVA + # Relationships - material_type: Mapped[Optional["MaterialType"]] = relationship(foreign_keys=[material_key]) - + material_type: Mapped[Optional["MaterialType"]] = relationship( + foreign_keys=[material_key] + ) + # Inverse relationship with GParts that have this class parts: Mapped[list["Part"]] = relationship( primaryjoin="and_(Class.client_id == Part.client_id, Class.class_code == Part.part_class)", foreign_keys="[Part.client_id, Part.part_class]", viewonly=True, - back_populates="part_class_info" + back_populates="part_class_info", ) - + def __repr__(self) -> str: return f"" - - diff --git a/backend/api/v1/modules/a76/classes/routes.py b/backend/api/v1/modules/a76/classes/routes.py index ffea97f0..ef4c85fd 100644 --- a/backend/api/v1/modules/a76/classes/routes.py +++ b/backend/api/v1/modules/a76/classes/routes.py @@ -1,6 +1,7 @@ """ Endpoints API para gestión de clases SCAII y SCAF """ + from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.orm import Session from typing import List, Optional @@ -9,28 +10,33 @@ from core.database import get_core_db from core.security import get_current_user, has_role from .service import ClassService from .dto import ( - ClassCreateDTO, - ClassUpdateDTO, + ClassCreateDTO, + ClassUpdateDTO, ClassResponseDTO, ClassBasicDTO, ClassListDTO, - ClassSearchDTO + ClassSearchDTO, ) router = APIRouter(prefix="/classes", tags=["Classes"]) + @router.get("/", response_model=ClassListDTO) async def list_classes( skip: int = Query(0, ge=0, description="Number of records to skip"), - limit: int = Query(100, ge=1, le=1000, description="Maximum number of records to return"), + limit: int = Query( + 100, ge=1, le=1000, description="Maximum number of records to return" + ), client_id: Optional[int] = Query(None, description="Filter by client key"), class_code: Optional[str] = Query(None, description="Search by class code"), description: Optional[str] = Query(None, description="Search in descriptions"), material_key: Optional[str] = Query(None, description="Filter by material key"), fraction: Optional[str] = Query(None, description="Filter by tariff fraction"), - physical_review: Optional[int] = Query(None, description="Filter by physical review indicator"), + physical_review: Optional[int] = Query( + None, description="Filter by physical review indicator" + ), db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ List classes with optional filters and pagination @@ -49,7 +55,7 @@ async def list_classes( description=description, material_key=material_key, fraction=fraction, - physical_review=physical_review + physical_review=physical_review, ) return service.list_classes(skip, limit, search_params) @@ -60,7 +66,7 @@ async def get_classes_by_client( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Get all classes for a specific client @@ -80,7 +86,7 @@ async def get_classes_by_client( async def search_by_fraction( fraction: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Search classes by tariff fraction @@ -93,7 +99,7 @@ async def search_by_fraction( async def search_by_material( material_key: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Search classes by material key @@ -102,11 +108,13 @@ async def search_by_material( return service.search_by_material(material_key) -@router.get("/search/unit-measure/{unit_of_measure}", response_model=List[ClassBasicDTO]) +@router.get( + "/search/unit-measure/{unit_of_measure}", response_model=List[ClassBasicDTO] +) async def get_classes_by_unit_measure( unit_of_measure: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Get classes by unit of measure @@ -115,11 +123,13 @@ async def get_classes_by_unit_measure( return service.get_classes_by_unit_measure(unit_of_measure) -@router.get("/search/physical-review/{physical_review}", response_model=List[ClassBasicDTO]) +@router.get( + "/search/physical-review/{physical_review}", response_model=List[ClassBasicDTO] +) async def get_classes_by_physical_review( physical_review: int, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Get classes by physical review indicator @@ -130,8 +140,7 @@ async def get_classes_by_physical_review( @router.get("/statistics", response_model=dict) async def get_classes_statistics( - db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) ): """ Get basic classes statistics @@ -145,7 +154,7 @@ async def get_class( client_id: int, class_code: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Get class by composite key (client_id + class_code) @@ -154,16 +163,17 @@ async def get_class( class_obj = service.get_class(client_id, class_code) if not class_obj: raise HTTPException( - status_code=404, - detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found" + status_code=404, + detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found", ) return class_obj + @router.post("/", response_model=ClassResponseDTO, status_code=status.HTTP_201_CREATED) async def create_class( class_data: ClassCreateDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Create a new class in the system @@ -171,13 +181,14 @@ async def create_class( service = ClassService(db) return service.create_class(class_data) + @router.put("/{client_id}/{class_code}", response_model=ClassResponseDTO) async def update_class( client_id: int, class_code: str, class_data: ClassUpdateDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Update class information @@ -186,8 +197,8 @@ async def update_class( class_obj = service.update_class(client_id, class_code, class_data) if not class_obj: raise HTTPException( - status_code=404, - detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found" + status_code=404, + detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found", ) return class_obj @@ -197,18 +208,18 @@ async def delete_class( client_id: int, class_code: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Delete class from the system - + Note: This will completely remove the class from the system. """ service = ClassService(db) if not service.delete_class(client_id, class_code): raise HTTPException( - status_code=404, - detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found" + status_code=404, + detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found", ) @@ -218,7 +229,7 @@ async def get_class_basic_info( client_id: int, class_code: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Get basic information for a class @@ -227,17 +238,17 @@ async def get_class_basic_info( class_obj = service.get_class(client_id, class_code) if not class_obj: raise HTTPException( - status_code=404, - detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found" + status_code=404, + detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found", ) - + return ClassBasicDTO( client_id=class_obj.client_id, class_code=class_obj.class_code, description_spanish=class_obj.description_spanish, description_english=class_obj.description_english, material_key=class_obj.material_key, - fraction=class_obj.fraction + fraction=class_obj.fraction, ) @@ -246,7 +257,7 @@ async def get_class_tariff_info( client_id: int, class_code: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Get tariff information for a class (fractions, IVA exempt, etc.) @@ -255,10 +266,10 @@ async def get_class_tariff_info( class_obj = service.get_class(client_id, class_code) if not class_obj: raise HTTPException( - status_code=404, - detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found" + status_code=404, + detail=f"Class with client_id '{client_id}' and class_code '{class_code}' not found", ) - + return { "client_id": class_obj.client_id, "class_code": class_obj.class_code, @@ -266,7 +277,5 @@ async def get_class_tariff_info( "us_fraction": class_obj.us_fraction, "iva_exempt_fraction": class_obj.iva_exempt_fraction, "sub_key": class_obj.sub_key, - "physical_review": class_obj.physical_review + "physical_review": class_obj.physical_review, } - - diff --git a/backend/api/v1/modules/a76/classes/service.py b/backend/api/v1/modules/a76/classes/service.py index b7ddf7d5..d515cce7 100644 --- a/backend/api/v1/modules/a76/classes/service.py +++ b/backend/api/v1/modules/a76/classes/service.py @@ -1,6 +1,7 @@ """ Capa de servicio para lógica de negocio de clases SCAII y SCAF """ + from sqlalchemy.orm import Session from sqlalchemy.exc import IntegrityError from sqlalchemy import or_, and_, func @@ -10,12 +11,12 @@ import logging from .models import Class from .dto import ( - ClassCreateDTO, - ClassUpdateDTO, + ClassCreateDTO, + ClassUpdateDTO, ClassResponseDTO, ClassBasicDTO, ClassListDTO, - ClassSearchDTO + ClassSearchDTO, ) logger = logging.getLogger(__name__) @@ -23,38 +24,42 @@ logger = logging.getLogger(__name__) class ClassService: """Servicio para gestión de clases SCAII y SCAF""" - + def __init__(self, db: Session): self.db = db - + def create_class(self, class_data: ClassCreateDTO) -> ClassResponseDTO: """ Crea una nueva clase en el sistema - + Args: class_data: Datos de la clase a crear - + Returns: ClassResponseDTO con información de la clase creada - + Raises: HTTPException: Si la clase ya existe o error en la creación """ try: # Verificar que no exista la clase - existing = self.db.query(Class).filter( - and_( - Class.client_id == class_data.client_id, - Class.class_code == class_data.class_code + existing = ( + self.db.query(Class) + .filter( + and_( + Class.client_id == class_data.client_id, + Class.class_code == class_data.class_code, + ) ) - ).first() - + .first() + ) + if existing: raise HTTPException( - status_code=400, - detail=f"Class with client_id '{class_data.client_id}' and class_code '{class_data.class_code}' already exists" + status_code=400, + detail=f"Class with client_id '{class_data.client_id}' and class_code '{class_data.class_code}' already exists", ) - + # Crear clase db_class = Class( client_id=class_data.client_id, @@ -67,171 +72,181 @@ class ClassService: us_fraction=class_data.us_fraction, sub_key=class_data.sub_key, physical_review=class_data.physical_review, - iva_exempt_fraction=class_data.iva_exempt_fraction + iva_exempt_fraction=class_data.iva_exempt_fraction, ) - + self.db.add(db_class) self.db.commit() self.db.refresh(db_class) - + logger.info(f"Class created: {db_class.client_id}-{db_class.class_code}") - + return ClassResponseDTO.model_validate(db_class) - + except IntegrityError as e: self.db.rollback() logger.error(f"IntegrityError creating class: {str(e)}") - raise HTTPException(status_code=400, detail="Class with this client_id and class_code already exists") + raise HTTPException( + status_code=400, + detail="Class with this client_id and class_code already exists", + ) except HTTPException: raise except Exception as e: self.db.rollback() logger.error(f"Error creating class: {str(e)}") raise HTTPException(status_code=500, detail="Error creating class") - + def get_class(self, client_id: int, class_code: str) -> Optional[ClassResponseDTO]: """ Obtiene una clase por clave compuesta - + Args: client_id: Clave del cliente class_code: Código de clase - + Returns: ClassResponseDTO o None si no existe """ - class_obj = self.db.query(Class).filter( - and_( - Class.client_id == client_id, - Class.class_code == class_code - ) - ).first() - + class_obj = ( + self.db.query(Class) + .filter(and_(Class.client_id == client_id, Class.class_code == class_code)) + .first() + ) + if not class_obj: return None return ClassResponseDTO.model_validate(class_obj) - + def list_classes( - self, - skip: int = 0, - limit: int = 100, - search_params: Optional[ClassSearchDTO] = None + self, + skip: int = 0, + limit: int = 100, + search_params: Optional[ClassSearchDTO] = None, ) -> ClassListDTO: """ Lista clases con filtros - + Args: skip: Número de registros a omitir limit: Número máximo de registros a retornar search_params: Parámetros de búsqueda - + Returns: ClassListDTO con la lista paginada """ query = self.db.query(Class) - + # Aplicar filtros si se proporcionan if search_params: if search_params.client_id: query = query.filter(Class.client_id == search_params.client_id) - + if search_params.class_code: - query = query.filter(Class.class_code.ilike(f"%{search_params.class_code}%")) - + query = query.filter( + Class.class_code.ilike(f"%{search_params.class_code}%") + ) + if search_params.description: description_pattern = f"%{search_params.description}%" query = query.filter( or_( Class.description_spanish.ilike(description_pattern), - Class.description_english.ilike(description_pattern) + Class.description_english.ilike(description_pattern), ) ) - + if search_params.material_key: - query = query.filter(Class.material_key.ilike(f"%{search_params.material_key}%")) - + query = query.filter( + Class.material_key.ilike(f"%{search_params.material_key}%") + ) + if search_params.fraction: - query = query.filter(Class.fraction.ilike(f"%{search_params.fraction}%")) - + query = query.filter( + Class.fraction.ilike(f"%{search_params.fraction}%") + ) + if search_params.physical_review is not None: - query = query.filter(Class.physical_review == search_params.physical_review) - + query = query.filter( + Class.physical_review == search_params.physical_review + ) + # Contar total total = query.count() - + # Aplicar paginación classes = query.offset(skip).limit(limit).all() - + # Convertir a DTOs básicos class_dtos = [ClassBasicDTO.model_validate(class_obj) for class_obj in classes] - + return ClassListDTO( classes=class_dtos, total=total, page=(skip // limit) + 1 if limit > 0 else 1, - size=len(class_dtos) + size=len(class_dtos), ) - - def update_class(self, client_id: int, class_code: str, class_data: ClassUpdateDTO) -> Optional[ClassResponseDTO]: + + def update_class( + self, client_id: int, class_code: str, class_data: ClassUpdateDTO + ) -> Optional[ClassResponseDTO]: """ Actualiza una clase - + Args: client_id: Clave del cliente class_code: Código de clase class_data: Datos a actualizar - + Returns: ClassResponseDTO actualizado o None si no existe """ - class_obj = self.db.query(Class).filter( - and_( - Class.client_id == client_id, - Class.class_code == class_code - ) - ).first() - + class_obj = ( + self.db.query(Class) + .filter(and_(Class.client_id == client_id, Class.class_code == class_code)) + .first() + ) + if not class_obj: return None - + try: # Actualizar solo campos proporcionados update_data = class_data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(class_obj, field, value) - + self.db.commit() self.db.refresh(class_obj) logger.info(f"Class updated: {client_id}-{class_code}") - + return ClassResponseDTO.model_validate(class_obj) - + except Exception as e: self.db.rollback() logger.error(f"Error updating class {client_id}-{class_code}: {str(e)}") raise HTTPException(status_code=500, detail="Error updating class") - + def delete_class(self, client_id: int, class_code: str) -> bool: """ Elimina una clase - + Args: client_id: Clave del cliente class_code: Código de clase - + Returns: True si se eliminó, False si no existe """ - class_obj = self.db.query(Class).filter( - and_( - Class.client_id == client_id, - Class.class_code == class_code - ) - ).first() - + class_obj = ( + self.db.query(Class) + .filter(and_(Class.client_id == client_id, Class.class_code == class_code)) + .first() + ) + if not class_obj: return False - + try: self.db.delete(class_obj) self.db.commit() @@ -241,54 +256,75 @@ class ClassService: self.db.rollback() logger.error(f"Error deleting class {client_id}-{class_code}: {str(e)}") raise HTTPException(status_code=500, detail="Error deleting class") - + def search_by_fraction(self, fraction: str) -> List[ClassBasicDTO]: """Busca clases por fracción arancelaria""" - classes = self.db.query(Class).filter(Class.fraction.ilike(f"%{fraction}%")).all() + classes = ( + self.db.query(Class).filter(Class.fraction.ilike(f"%{fraction}%")).all() + ) return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes] - - def search_by_client(self, client_id: int, skip: int = 0, limit: int = 100) -> List[ClassBasicDTO]: + + def search_by_client( + self, client_id: int, skip: int = 0, limit: int = 100 + ) -> List[ClassBasicDTO]: """Obtiene todas las clases de un cliente específico""" - classes = self.db.query(Class).filter(Class.client_id == client_id).offset(skip).limit(limit).all() + classes = ( + self.db.query(Class) + .filter(Class.client_id == client_id) + .offset(skip) + .limit(limit) + .all() + ) return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes] - + def search_by_material(self, material_key: str) -> List[ClassBasicDTO]: """Busca clases por clave de material""" - classes = self.db.query(Class).filter(Class.material_key.ilike(f"%{material_key}%")).all() + classes = ( + self.db.query(Class) + .filter(Class.material_key.ilike(f"%{material_key}%")) + .all() + ) return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes] - - def get_classes_by_physical_review(self, physical_review: int) -> List[ClassBasicDTO]: + + def get_classes_by_physical_review( + self, physical_review: int + ) -> List[ClassBasicDTO]: """Obtiene clases por indicador de revisión física""" - classes = self.db.query(Class).filter(Class.physical_review == physical_review).all() + classes = ( + self.db.query(Class).filter(Class.physical_review == physical_review).all() + ) return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes] - + def get_classes_statistics(self) -> dict: """Obtiene estadísticas básicas de clases""" total_classes = self.db.query(Class).count() - + # Contar por clientes clients_count = self.db.query(Class.client_id).distinct().count() - + # Contar por revisión física physical_review_stats = {} for i in range(3): # Asumiendo valores 0, 1, 2 count = self.db.query(Class).filter(Class.physical_review == i).count() physical_review_stats[f"physical_review_{i}"] = count - + # Contar clases con fracciones with_fraction = self.db.query(Class).filter(Class.fraction.isnot(None)).count() - with_us_fraction = self.db.query(Class).filter(Class.us_fraction.isnot(None)).count() - + with_us_fraction = ( + self.db.query(Class).filter(Class.us_fraction.isnot(None)).count() + ) + return { "total_classes": total_classes, "clients_with_classes": clients_count, "classes_with_fraction": with_fraction, "classes_with_us_fraction": with_us_fraction, - **physical_review_stats + **physical_review_stats, } - + def get_classes_by_unit_measure(self, unit_of_measure: str) -> List[ClassBasicDTO]: """Obtiene clases por unidad de medida""" - classes = self.db.query(Class).filter(Class.unit_of_measure == unit_of_measure).all() + classes = ( + self.db.query(Class).filter(Class.unit_of_measure == unit_of_measure).all() + ) return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes] - diff --git a/backend/api/v1/modules/a76/client_and_provider/__init__.py b/backend/api/v1/modules/a76/client_and_provider/__init__.py index 9621b9c4..bdcba0ad 100644 --- a/backend/api/v1/modules/a76/client_and_provider/__init__.py +++ b/backend/api/v1/modules/a76/client_and_provider/__init__.py @@ -1,6 +1,7 @@ """ Módulo de Client & Provider """ + from .routes import router __all__ = ["router"] diff --git a/backend/api/v1/modules/a76/client_and_provider/dto.py b/backend/api/v1/modules/a76/client_and_provider/dto.py index be248661..25bc4d59 100644 --- a/backend/api/v1/modules/a76/client_and_provider/dto.py +++ b/backend/api/v1/modules/a76/client_and_provider/dto.py @@ -2,6 +2,7 @@ DTOs (Data Transfer Objects) para módulo de clientes y proveedores Reemplaza schemas.py siguiendo enfoque DDD y estilo NestJS """ + from pydantic import BaseModel, Field, EmailStr from typing import Optional from datetime import datetime @@ -11,11 +12,18 @@ from decimal import Decimal # DTOs para dirección class ClientProviderAddressDTO(BaseModel): """DTO para dirección de cliente/proveedor""" - municipality: Optional[str] = Field(None, max_length=150, description="Municipality") + + municipality: Optional[str] = Field( + None, max_length=150, description="Municipality" + ) streets: Optional[str] = Field(None, max_length=100, description="Streets") neighborhood: Optional[str] = Field(None, max_length=40, description="Neighborhood") - interior_number: Optional[str] = Field(None, max_length=20, description="Interior number") - exterior_number: Optional[str] = Field(None, max_length=20, description="Exterior number") + interior_number: Optional[str] = Field( + None, max_length=20, description="Interior number" + ) + exterior_number: Optional[str] = Field( + None, max_length=20, description="Exterior number" + ) postal_code: Optional[str] = Field(None, max_length=15, description="Postal code") city: Optional[str] = Field(None, max_length=30, description="City") state: Optional[str] = Field(None, max_length=30, description="State") @@ -33,26 +41,49 @@ class ClientProviderAddressDTO(BaseModel): # DTOs para programas class ClientProviderProgramsDTO(BaseModel): """DTO para programas de cliente/proveedor""" + program: Optional[str] = Field(None, max_length=7, description="Program") - program_number: Optional[str] = Field(None, max_length=40, description="Program number") + program_number: Optional[str] = Field( + None, max_length=40, description="Program number" + ) prosec: Optional[int] = Field(None, description="PROSEC") - prosec_authorization: Optional[str] = Field(None, max_length=20, description="PROSEC authorization") + prosec_authorization: Optional[str] = Field( + None, max_length=20, description="PROSEC authorization" + ) secon_auth_date: Optional[int] = Field(None, description="SECON authorization date") - manufacturer_id: Optional[str] = Field(None, max_length=25, description="Manufacturer ID") + manufacturer_id: Optional[str] = Field( + None, max_length=25, description="Manufacturer ID" + ) tax_id: Optional[str] = Field(None, max_length=30, description="Tax ID") broker: Optional[str] = Field(None, max_length=6, description="Broker") - import_broker: Optional[str] = Field(None, max_length=6, description="Import broker") + import_broker: Optional[str] = Field( + None, max_length=6, description="Import broker" + ) transfer_key: Optional[str] = Field(None, max_length=8, description="Transfer key") - secon_authorization: Optional[str] = Field(None, max_length=20, description="SECON authorization") - applied_proportion: Optional[Decimal] = Field(None, description="Applied proportion") - is_certified_company: Optional[str] = Field(None, max_length=1, description="Is certified company") - certified_company_registry: Optional[str] = Field(None, max_length=40, description="Certified company registry") - donation_auth_number: Optional[str] = Field(None, max_length=50, description="Donation authorization number") + secon_authorization: Optional[str] = Field( + None, max_length=20, description="SECON authorization" + ) + applied_proportion: Optional[Decimal] = Field( + None, description="Applied proportion" + ) + is_certified_company: Optional[str] = Field( + None, max_length=1, description="Is certified company" + ) + certified_company_registry: Optional[str] = Field( + None, max_length=40, description="Certified company registry" + ) + donation_auth_number: Optional[str] = Field( + None, max_length=50, description="Donation authorization number" + ) ctpat_svi: Optional[str] = Field(None, max_length=100, description="CTPAT SVI") - tax_registry_number: Optional[str] = Field(None, max_length=40, description="Tax registry number") + tax_registry_number: Optional[str] = Field( + None, max_length=40, description="Tax registry number" + ) subassembly_service: Optional[int] = Field(None, description="Subassembly service") autse_dates: Optional[int] = Field(None, description="AUTSE dates") - autse_number: Optional[str] = Field(None, max_length=300, description="AUTSE number") + autse_number: Optional[str] = Field( + None, max_length=300, description="AUTSE number" + ) class Config: from_attributes = True @@ -61,26 +92,43 @@ class ClientProviderProgramsDTO(BaseModel): # DTOs principales class ClientProviderCreateDTO(BaseModel): """DTO para crear cliente/proveedor""" + client_id: str = Field(..., max_length=8, description="Client ID") - type_nat_foreign: Optional[str] = Field(None, max_length=1, description="Type national/foreign") + type_nat_foreign: Optional[str] = Field( + None, max_length=1, description="Type national/foreign" + ) name: Optional[str] = Field(None, max_length=256, description="Name") short_name: Optional[str] = Field(None, max_length=10, description="Short name") rfc: Optional[str] = Field(None, max_length=30, description="RFC") curp: Optional[str] = Field(None, max_length=19, description="CURP") - client_or_provider: Optional[str] = Field(None, max_length=1, description="Client or provider") + client_or_provider: Optional[str] = Field( + None, max_length=1, description="Client or provider" + ) linking: Optional[str] = Field(None, max_length=1, description="Linking") - transform_subassembly: Optional[str] = Field(None, max_length=1, description="Transform subassembly") - extra_information: Optional[str] = Field(None, max_length=399, description="Extra information") + transform_subassembly: Optional[str] = Field( + None, max_length=1, description="Transform subassembly" + ) + extra_information: Optional[str] = Field( + None, max_length=399, description="Extra information" + ) web_key: Optional[str] = Field(None, max_length=40, description="Web key") - responsible: Optional[str] = Field(None, max_length=80, description="Responsible person") + responsible: Optional[str] = Field( + None, max_length=80, description="Responsible person" + ) position: Optional[str] = Field(None, max_length=30, description="Position") incoterm: Optional[str] = Field(None, max_length=19, description="Incoterm") - is_national_provider: Optional[str] = Field(None, max_length=2, description="Is national provider") + is_national_provider: Optional[str] = Field( + None, max_length=2, description="Is national provider" + ) enabled_disabled: Optional[int] = Field(None, description="Enabled/Disabled status") - + # Nested DTOs - address: Optional[ClientProviderAddressDTO] = Field(None, description="Address information") - programs: Optional[ClientProviderProgramsDTO] = Field(None, description="Programs information") + address: Optional[ClientProviderAddressDTO] = Field( + None, description="Address information" + ) + programs: Optional[ClientProviderProgramsDTO] = Field( + None, description="Programs information" + ) class Config: from_attributes = True @@ -88,25 +136,42 @@ class ClientProviderCreateDTO(BaseModel): class ClientProviderUpdateDTO(BaseModel): """DTO para actualizar cliente/proveedor""" - type_nat_foreign: Optional[str] = Field(None, max_length=1, description="Type national/foreign") + + type_nat_foreign: Optional[str] = Field( + None, max_length=1, description="Type national/foreign" + ) name: Optional[str] = Field(None, max_length=256, description="Name") short_name: Optional[str] = Field(None, max_length=10, description="Short name") rfc: Optional[str] = Field(None, max_length=30, description="RFC") curp: Optional[str] = Field(None, max_length=19, description="CURP") - client_or_provider: Optional[str] = Field(None, max_length=1, description="Client or provider") + client_or_provider: Optional[str] = Field( + None, max_length=1, description="Client or provider" + ) linking: Optional[str] = Field(None, max_length=1, description="Linking") - transform_subassembly: Optional[str] = Field(None, max_length=1, description="Transform subassembly") - extra_information: Optional[str] = Field(None, max_length=399, description="Extra information") + transform_subassembly: Optional[str] = Field( + None, max_length=1, description="Transform subassembly" + ) + extra_information: Optional[str] = Field( + None, max_length=399, description="Extra information" + ) web_key: Optional[str] = Field(None, max_length=40, description="Web key") - responsible: Optional[str] = Field(None, max_length=80, description="Responsible person") + responsible: Optional[str] = Field( + None, max_length=80, description="Responsible person" + ) position: Optional[str] = Field(None, max_length=30, description="Position") incoterm: Optional[str] = Field(None, max_length=19, description="Incoterm") - is_national_provider: Optional[str] = Field(None, max_length=2, description="Is national provider") + is_national_provider: Optional[str] = Field( + None, max_length=2, description="Is national provider" + ) enabled_disabled: Optional[int] = Field(None, description="Enabled/Disabled status") - + # Nested DTOs - address: Optional[ClientProviderAddressDTO] = Field(None, description="Address information") - programs: Optional[ClientProviderProgramsDTO] = Field(None, description="Programs information") + address: Optional[ClientProviderAddressDTO] = Field( + None, description="Address information" + ) + programs: Optional[ClientProviderProgramsDTO] = Field( + None, description="Programs information" + ) class Config: from_attributes = True @@ -114,6 +179,7 @@ class ClientProviderUpdateDTO(BaseModel): class ClientProviderResponseDTO(BaseModel): """DTO para respuesta de cliente/proveedor""" + client_id: str type_nat_foreign: Optional[str] = None name: Optional[str] = None @@ -130,7 +196,7 @@ class ClientProviderResponseDTO(BaseModel): incoterm: Optional[str] = None is_national_provider: Optional[str] = None enabled_disabled: Optional[int] = None - + # Nested DTOs address: Optional[ClientProviderAddressDTO] = None programs: Optional[ClientProviderProgramsDTO] = None @@ -142,6 +208,7 @@ class ClientProviderResponseDTO(BaseModel): # DTOs para respuestas específicas class ClientProviderBasicDTO(BaseModel): """DTO para información básica de cliente/proveedor""" + client_id: str name: Optional[str] = None short_name: Optional[str] = None @@ -155,6 +222,7 @@ class ClientProviderBasicDTO(BaseModel): class ClientProviderListDTO(BaseModel): """DTO para lista de clientes/proveedores""" + clients: list[ClientProviderBasicDTO] total: int page: int @@ -162,4 +230,3 @@ class ClientProviderListDTO(BaseModel): class Config: from_attributes = True - diff --git a/backend/api/v1/modules/a76/client_and_provider/models.py b/backend/api/v1/modules/a76/client_and_provider/models.py index 8c407369..2730e15e 100644 --- a/backend/api/v1/modules/a76/client_and_provider/models.py +++ b/backend/api/v1/modules/a76/client_and_provider/models.py @@ -1,9 +1,18 @@ """ Modelos ORM para gestión de clientes y proveedores """ + from typing import Optional from decimal import Decimal -from sqlalchemy import Integer, String, SmallInteger, Numeric, ForeignKey, PrimaryKeyConstraint, ForeignKeyConstraint +from sqlalchemy import ( + Integer, + String, + SmallInteger, + Numeric, + ForeignKey, + PrimaryKeyConstraint, + ForeignKeyConstraint, +) from sqlalchemy.orm import Mapped, mapped_column, relationship from core.database import Base @@ -12,21 +21,28 @@ class ClientProvider(Base): """ Modelo para la tabla GClientesPro - Información de clientes y proveedores """ + __tablename__ = "client_provider" __table_args__ = ( - PrimaryKeyConstraint('id', name='client_provider_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_client_provider_tenant'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_client_provider_company'), - {"schema": "a76"} + PrimaryKeyConstraint("id", name="client_provider_pkey"), + ForeignKeyConstraint( + ["tenant_id"], ["a76.tenants.id"], name="fk_client_provider_tenant" + ), + ForeignKeyConstraint( + ["company_id"], ["a76.company.id"], name="fk_client_provider_company" + ), + {"schema": "a76"}, ) - + # Primary key id: Mapped[int] = mapped_column(Integer, primary_key=True) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - + # Basic information - type_nat_foreign: Mapped[Optional[str]] = mapped_column(String(1)) # TIPO NACIONAL/EXTRANJERO + type_nat_foreign: Mapped[Optional[str]] = mapped_column( + String(1) + ) # TIPO NACIONAL/EXTRANJERO name: Mapped[Optional[str]] = mapped_column(String(256)) short_name: Mapped[Optional[str]] = mapped_column(String(10)) rfc: Mapped[Optional[str]] = mapped_column(String(30)) @@ -40,31 +56,45 @@ class ClientProvider(Base): position: Mapped[Optional[str]] = mapped_column(String(30)) incoterm: Mapped[Optional[str]] = mapped_column(String(19)) is_national_provider: Mapped[Optional[str]] = mapped_column(String(2)) - enabled_disabled: Mapped[Optional[int]] = mapped_column(SmallInteger) - + enabled_disabled: Mapped[Optional[int]] = mapped_column(SmallInteger) + # Relationships - address: Mapped[Optional["ClientProviderAddress"]] = relationship(back_populates="client_provider", uselist=False, cascade="all, delete-orphan") - programs: Mapped[Optional["ClientProviderPrograms"]] = relationship(back_populates="client_provider", uselist=False, cascade="all, delete-orphan") + address: Mapped[Optional["ClientProviderAddress"]] = relationship( + back_populates="client_provider", uselist=False, cascade="all, delete-orphan" + ) + programs: Mapped[Optional["ClientProviderPrograms"]] = relationship( + back_populates="client_provider", uselist=False, cascade="all, delete-orphan" + ) class ClientProviderAddress(Base): """ Modelo para la tabla GClientesPro_Direccion - Dirección de clientes y proveedores """ + __tablename__ = "client_provider_address" __table_args__ = ( - PrimaryKeyConstraint('id', name='client_provider_address_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_client_provider_address_tenant'), - ForeignKeyConstraint(['client_id'], ['a76.client_provider.id'], ondelete='CASCADE', name='fk_client_provider_address_client'), - {"schema": "a76"} + PrimaryKeyConstraint("id", name="client_provider_address_pkey"), + ForeignKeyConstraint( + ["tenant_id"], ["a76.tenants.id"], name="fk_client_provider_address_tenant" + ), + ForeignKeyConstraint( + ["client_id"], + ["a76.client_provider.id"], + ondelete="CASCADE", + name="fk_client_provider_address_client", + ), + {"schema": "a76"}, ) - + # Primary key (foreign key) id: Mapped[int] = mapped_column(Integer, primary_key=True) - client_id: Mapped[int] = mapped_column(Integer, ForeignKey('a76.client_provider.id', ondelete='CASCADE')) - + client_id: Mapped[int] = mapped_column( + Integer, ForeignKey("a76.client_provider.id", ondelete="CASCADE") + ) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - + # Address information municipality: Mapped[Optional[str]] = mapped_column(String(150)) streets: Mapped[Optional[str]] = mapped_column(String(100)) @@ -80,7 +110,7 @@ class ClientProviderAddress(Base): email: Mapped[Optional[str]] = mapped_column(String(100)) contact: Mapped[Optional[str]] = mapped_column(String(50)) reference: Mapped[Optional[str]] = mapped_column(String(250)) - + # Relationship client_provider: Mapped["ClientProvider"] = relationship(back_populates="address") @@ -89,20 +119,30 @@ class ClientProviderPrograms(Base): """ Modelo para la tabla GClientesPro_Programas - Programas de clientes y proveedores """ + __tablename__ = "client_provider_programs" __table_args__ = ( - PrimaryKeyConstraint('id', name='client_provider_programs_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_client_provider_programs_tenant'), - ForeignKeyConstraint(['client_id'], ['a76.client_provider.id'], ondelete='CASCADE', name='fk_client_provider_programs_client'), - {"schema": "a76"} + PrimaryKeyConstraint("id", name="client_provider_programs_pkey"), + ForeignKeyConstraint( + ["tenant_id"], ["a76.tenants.id"], name="fk_client_provider_programs_tenant" + ), + ForeignKeyConstraint( + ["client_id"], + ["a76.client_provider.id"], + ondelete="CASCADE", + name="fk_client_provider_programs_client", + ), + {"schema": "a76"}, ) - + # Primary key (foreign key) id: Mapped[int] = mapped_column(Integer, primary_key=True) - client_id: Mapped[int] = mapped_column(Integer, ForeignKey('a76.client_provider.id', ondelete='CASCADE')) - + client_id: Mapped[int] = mapped_column( + Integer, ForeignKey("a76.client_provider.id", ondelete="CASCADE") + ) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - + # Program information program: Mapped[Optional[str]] = mapped_column(String(7)) program_number: Mapped[Optional[str]] = mapped_column(String(40)) @@ -124,8 +164,6 @@ class ClientProviderPrograms(Base): subassembly_service: Mapped[Optional[int]] = mapped_column(SmallInteger) autse_dates: Mapped[Optional[int]] = mapped_column() autse_number: Mapped[Optional[str]] = mapped_column(String(300)) - + # Relationship client_provider: Mapped["ClientProvider"] = relationship(back_populates="programs") - - diff --git a/backend/api/v1/modules/a76/client_and_provider/routes.py b/backend/api/v1/modules/a76/client_and_provider/routes.py index cafb6428..02e7183b 100644 --- a/backend/api/v1/modules/a76/client_and_provider/routes.py +++ b/backend/api/v1/modules/a76/client_and_provider/routes.py @@ -1,6 +1,7 @@ """ Endpoints API para gestión de clientes y proveedores """ + from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.orm import Session from typing import List, Optional @@ -9,21 +10,23 @@ from core.database import get_core_db from core.security import get_current_user, has_role from .service import ClientProviderService from .dto import ( - ClientProviderCreateDTO, - ClientProviderUpdateDTO, + ClientProviderCreateDTO, + ClientProviderUpdateDTO, ClientProviderResponseDTO, ClientProviderBasicDTO, - ClientProviderListDTO + ClientProviderListDTO, ) router = APIRouter(prefix="/clients-providers") -@router.post("/", response_model=ClientProviderResponseDTO, status_code=status.HTTP_201_CREATED) +@router.post( + "/", response_model=ClientProviderResponseDTO, status_code=status.HTTP_201_CREATED +) async def create_client_provider( client_data: ClientProviderCreateDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Create a new client or provider in the system @@ -46,12 +49,16 @@ async def create_client_provider( @router.get("/", response_model=ClientProviderListDTO) async def list_clients_providers( skip: int = Query(0, ge=0, description="Number of records to skip"), - limit: int = Query(100, ge=1, le=1000, description="Maximum number of records to return"), + limit: int = Query( + 100, ge=1, le=1000, description="Maximum number of records to return" + ), search: Optional[str] = Query(None, description="Search text for name, RFC, or ID"), - client_or_provider: Optional[str] = Query(None, regex="^[CP]$", description="Filter by type: C=Client, P=Provider"), + client_or_provider: Optional[str] = Query( + None, regex="^[CP]$", description="Filter by type: C=Client, P=Provider" + ), enabled_only: bool = Query(False, description="Show only enabled records"), db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ List clients and providers with optional filters and pagination @@ -64,7 +71,9 @@ async def list_clients_providers( raise HTTPException(status_code=403, detail="Access denied: Tenant or Company not found") service = ClientProviderService(db) - return service.list_clients_providers(skip, limit, search, client_or_provider, enabled_only) + return service.list_clients_providers( + skip, limit, search, client_or_provider, enabled_only + ) @router.get("/clients", response_model=List[ClientProviderBasicDTO]) @@ -72,7 +81,7 @@ async def get_clients_only( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Get only clients (client_or_provider = 'C') @@ -93,7 +102,7 @@ async def get_providers_only( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Get only providers (client_or_provider = 'P') @@ -113,7 +122,7 @@ async def get_providers_only( async def search_by_rfc( rfc: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Search clients/providers by RFC @@ -133,7 +142,7 @@ async def search_by_rfc( async def get_client_provider( client_id: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Get client/provider by ID with all related information @@ -148,7 +157,9 @@ async def get_client_provider( service = ClientProviderService(db) client = service.get_client_provider(client_id) if not client: - raise HTTPException(status_code=404, detail=f"Client/Provider with ID '{client_id}' not found") + raise HTTPException( + status_code=404, detail=f"Client/Provider with ID '{client_id}' not found" + ) return client @@ -157,7 +168,7 @@ async def update_client_provider( client_id: str, client_data: ClientProviderUpdateDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Update client/provider information @@ -172,7 +183,9 @@ async def update_client_provider( service = ClientProviderService(db) client = service.update_client_provider(client_id, client_data) if not client: - raise HTTPException(status_code=404, detail=f"Client/Provider with ID '{client_id}' not found") + raise HTTPException( + status_code=404, detail=f"Client/Provider with ID '{client_id}' not found" + ) return client @@ -180,11 +193,11 @@ async def update_client_provider( async def delete_client_provider( client_id: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Delete client/provider from the system - + Note: This will completely remove the client/provider and all related data. """ # Validate access to the tenant and company @@ -196,14 +209,16 @@ async def delete_client_provider( service = ClientProviderService(db) if not service.delete_client_provider(client_id): - raise HTTPException(status_code=404, detail=f"Client/Provider with ID '{client_id}' not found") + raise HTTPException( + status_code=404, detail=f"Client/Provider with ID '{client_id}' not found" + ) @router.patch("/{client_id}/toggle-status", response_model=ClientProviderResponseDTO) async def toggle_client_provider_status( client_id: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Toggle client/provider enabled/disabled status @@ -218,7 +233,9 @@ async def toggle_client_provider_status( service = ClientProviderService(db) client = service.toggle_status(client_id) if not client: - raise HTTPException(status_code=404, detail=f"Client/Provider with ID '{client_id}' not found") + raise HTTPException( + status_code=404, detail=f"Client/Provider with ID '{client_id}' not found" + ) return client @@ -227,7 +244,7 @@ async def toggle_client_provider_status( async def get_client_provider_address( client_id: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Get only address information for a client/provider @@ -242,19 +259,18 @@ async def get_client_provider_address( service = ClientProviderService(db) client = service.get_client_provider(client_id) if not client: - raise HTTPException(status_code=404, detail=f"Client/Provider with ID '{client_id}' not found") - - return { - "client_id": client.client_id, - "address": client.address - } + raise HTTPException( + status_code=404, detail=f"Client/Provider with ID '{client_id}' not found" + ) + + return {"client_id": client.client_id, "address": client.address} @router.get("/{client_id}/programs", response_model=dict) async def get_client_provider_programs( client_id: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Get only programs information for a client/provider @@ -269,19 +285,18 @@ async def get_client_provider_programs( service = ClientProviderService(db) client = service.get_client_provider(client_id) if not client: - raise HTTPException(status_code=404, detail=f"Client/Provider with ID '{client_id}' not found") - - return { - "client_id": client.client_id, - "programs": client.programs - } + raise HTTPException( + status_code=404, detail=f"Client/Provider with ID '{client_id}' not found" + ) + + return {"client_id": client.client_id, "programs": client.programs} @router.get("/{client_id}/basic", response_model=ClientProviderBasicDTO) async def get_client_provider_basic_info( client_id: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Get basic information for a client/provider (without address and programs) @@ -296,14 +311,15 @@ async def get_client_provider_basic_info( service = ClientProviderService(db) client = service.get_client_provider(client_id) if not client: - raise HTTPException(status_code=404, detail=f"Client/Provider with ID '{client_id}' not found") - + raise HTTPException( + status_code=404, detail=f"Client/Provider with ID '{client_id}' not found" + ) + return ClientProviderBasicDTO( client_id=client.client_id, name=client.name, short_name=client.short_name, rfc=client.rfc, client_or_provider=client.client_or_provider, - enabled_disabled=client.enabled_disabled + enabled_disabled=client.enabled_disabled, ) - diff --git a/backend/api/v1/modules/a76/client_and_provider/service.py b/backend/api/v1/modules/a76/client_and_provider/service.py index f12763b7..41b6d479 100644 --- a/backend/api/v1/modules/a76/client_and_provider/service.py +++ b/backend/api/v1/modules/a76/client_and_provider/service.py @@ -1,6 +1,7 @@ """ Capa de servicio para lógica de negocio de clientes y proveedores """ + from sqlalchemy.orm import Session, joinedload from sqlalchemy.exc import IntegrityError from sqlalchemy import or_, and_ @@ -10,13 +11,13 @@ import logging from .models import ClientProvider, ClientProviderAddress, ClientProviderPrograms from .dto import ( - ClientProviderCreateDTO, - ClientProviderUpdateDTO, + ClientProviderCreateDTO, + ClientProviderUpdateDTO, ClientProviderResponseDTO, ClientProviderBasicDTO, ClientProviderListDTO, ClientProviderAddressDTO, - ClientProviderProgramsDTO + ClientProviderProgramsDTO, ) logger = logging.getLogger(__name__) @@ -24,29 +25,38 @@ logger = logging.getLogger(__name__) class ClientProviderService: """Servicio para gestión de clientes y proveedores""" - + def __init__(self, db: Session): self.db = db - - def create_client_provider(self, client_data: ClientProviderCreateDTO) -> ClientProviderResponseDTO: + + def create_client_provider( + self, client_data: ClientProviderCreateDTO + ) -> ClientProviderResponseDTO: """ Crea un nuevo cliente/proveedor en el sistema - + Args: client_data: Datos del cliente/proveedor a crear - + Returns: ClientProviderResponseDTO con información del cliente/proveedor creado - + Raises: HTTPException: Si el cliente ya existe o error en la creación """ try: # Verificar que no exista el cliente - existing = self.db.query(ClientProvider).filter(ClientProvider.client_id == client_data.client_id).first() + existing = ( + self.db.query(ClientProvider) + .filter(ClientProvider.client_id == client_data.client_id) + .first() + ) if existing: - raise HTTPException(status_code=400, detail=f"Client with ID '{client_data.client_id}' already exists") - + raise HTTPException( + status_code=400, + detail=f"Client with ID '{client_data.client_id}' already exists", + ) + # Crear cliente/proveedor principal db_client = ClientProvider( client_id=client_data.client_id, @@ -64,92 +74,106 @@ class ClientProviderService: position=client_data.position, incoterm=client_data.incoterm, is_national_provider=client_data.is_national_provider, - enabled_disabled=client_data.enabled_disabled + enabled_disabled=client_data.enabled_disabled, ) - + self.db.add(db_client) self.db.flush() # Para obtener el ID antes del commit - + # Crear dirección si se proporciona if client_data.address: db_address = ClientProviderAddress( client_id=client_data.client_id, - **client_data.address.model_dump(exclude_unset=True) + **client_data.address.model_dump(exclude_unset=True), ) self.db.add(db_address) - + # Crear programas si se proporciona if client_data.programs: db_programs = ClientProviderPrograms( client_id=client_data.client_id, - **client_data.programs.model_dump(exclude_unset=True) + **client_data.programs.model_dump(exclude_unset=True), ) self.db.add(db_programs) - + self.db.commit() self.db.refresh(db_client) - - logger.info(f"Client/Provider created: {db_client.client_id} - {db_client.name}") - + + logger.info( + f"Client/Provider created: {db_client.client_id} - {db_client.name}" + ) + return self._get_client_with_relations(client_data.client_id) - + except IntegrityError as e: self.db.rollback() logger.error(f"IntegrityError creating client/provider: {str(e)}") - raise HTTPException(status_code=400, detail="Client/Provider with this ID already exists") + raise HTTPException( + status_code=400, detail="Client/Provider with this ID already exists" + ) except HTTPException: raise except Exception as e: self.db.rollback() logger.error(f"Error creating client/provider: {str(e)}") - raise HTTPException(status_code=500, detail="Error creating client/provider") - - def get_client_provider(self, client_id: str) -> Optional[ClientProviderResponseDTO]: + raise HTTPException( + status_code=500, detail="Error creating client/provider" + ) + + def get_client_provider( + self, client_id: str + ) -> Optional[ClientProviderResponseDTO]: """ Obtiene un cliente/proveedor por ID - + Args: client_id: ID del cliente/proveedor - + Returns: ClientProviderResponseDTO o None si no existe """ return self._get_client_with_relations(client_id) - - def _get_client_with_relations(self, client_id: str) -> Optional[ClientProviderResponseDTO]: + + def _get_client_with_relations( + self, client_id: str + ) -> Optional[ClientProviderResponseDTO]: """Método privado para obtener cliente con relaciones""" - client = self.db.query(ClientProvider).options( - joinedload(ClientProvider.address), - joinedload(ClientProvider.programs) - ).filter(ClientProvider.client_id == client_id).first() - + client = ( + self.db.query(ClientProvider) + .options( + joinedload(ClientProvider.address), joinedload(ClientProvider.programs) + ) + .filter(ClientProvider.client_id == client_id) + .first() + ) + if not client: return None return ClientProviderResponseDTO.model_validate(client) - + def list_clients_providers( - self, - skip: int = 0, - limit: int = 100, + self, + skip: int = 0, + limit: int = 100, search: Optional[str] = None, client_or_provider: Optional[str] = None, - enabled_only: bool = False + enabled_only: bool = False, ) -> ClientProviderListDTO: """ Lista clientes/proveedores con filtros - + Args: skip: Número de registros a omitir limit: Número máximo de registros a retornar search: Texto de búsqueda (nombre, RFC, ID) client_or_provider: Filtrar por tipo (C=Cliente, P=Proveedor) enabled_only: Si True, solo retorna activos - + Returns: ClientProviderListDTO con la lista paginada """ query = self.db.query(ClientProvider) - + # Aplicar filtros if search: search_pattern = f"%{search}%" @@ -158,56 +182,72 @@ class ClientProviderService: ClientProvider.name.ilike(search_pattern), ClientProvider.short_name.ilike(search_pattern), ClientProvider.rfc.ilike(search_pattern), - ClientProvider.client_id.ilike(search_pattern) + ClientProvider.client_id.ilike(search_pattern), ) ) - + if client_or_provider: - query = query.filter(ClientProvider.client_or_provider == client_or_provider) - + query = query.filter( + ClientProvider.client_or_provider == client_or_provider + ) + if enabled_only: query = query.filter(ClientProvider.enabled_disabled == 1) - + # Contar total total = query.count() - + # Aplicar paginación clients = query.offset(skip).limit(limit).all() - + # Convertir a DTOs básicos - client_dtos = [ClientProviderBasicDTO.model_validate(client) for client in clients] - + client_dtos = [ + ClientProviderBasicDTO.model_validate(client) for client in clients + ] + return ClientProviderListDTO( clients=client_dtos, total=total, page=(skip // limit) + 1 if limit > 0 else 1, - size=len(client_dtos) + size=len(client_dtos), ) - - def update_client_provider(self, client_id: str, client_data: ClientProviderUpdateDTO) -> Optional[ClientProviderResponseDTO]: + + def update_client_provider( + self, client_id: str, client_data: ClientProviderUpdateDTO + ) -> Optional[ClientProviderResponseDTO]: """ Actualiza un cliente/proveedor - + Args: client_id: ID del cliente/proveedor a actualizar client_data: Datos a actualizar - + Returns: ClientProviderResponseDTO actualizado o None si no existe """ - client = self.db.query(ClientProvider).filter(ClientProvider.client_id == client_id).first() + client = ( + self.db.query(ClientProvider) + .filter(ClientProvider.client_id == client_id) + .first() + ) if not client: return None - + try: # Actualizar campos del cliente principal - update_data = client_data.model_dump(exclude_unset=True, exclude={'address', 'programs'}) + update_data = client_data.model_dump( + exclude_unset=True, exclude={"address", "programs"} + ) for field, value in update_data.items(): setattr(client, field, value) - + # Actualizar dirección if client_data.address: - address = self.db.query(ClientProviderAddress).filter(ClientProviderAddress.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) @@ -217,13 +257,17 @@ class ClientProviderService: # Crear nueva dirección address = ClientProviderAddress( client_id=client_id, - **client_data.address.model_dump(exclude_unset=True) + **client_data.address.model_dump(exclude_unset=True), ) self.db.add(address) - + # Actualizar programas if client_data.programs: - programs = self.db.query(ClientProviderPrograms).filter(ClientProviderPrograms.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) @@ -233,34 +277,40 @@ class ClientProviderService: # Crear nuevos programas programs = ClientProviderPrograms( client_id=client_id, - **client_data.programs.model_dump(exclude_unset=True) + **client_data.programs.model_dump(exclude_unset=True), ) self.db.add(programs) - + self.db.commit() logger.info(f"Client/Provider updated: {client_id}") - + return self._get_client_with_relations(client_id) - + except Exception as e: self.db.rollback() logger.error(f"Error updating client/provider {client_id}: {str(e)}") - raise HTTPException(status_code=500, detail="Error updating client/provider") - + raise HTTPException( + status_code=500, detail="Error updating client/provider" + ) + def delete_client_provider(self, client_id: str) -> bool: """ Elimina un cliente/proveedor - + Args: client_id: ID del cliente/proveedor a eliminar - + Returns: True si se eliminó, False si no existe """ - client = self.db.query(ClientProvider).filter(ClientProvider.client_id == client_id).first() + client = ( + self.db.query(ClientProvider) + .filter(ClientProvider.client_id == client_id) + .first() + ) if not client: return False - + try: self.db.delete(client) # Las relaciones se eliminan en cascada self.db.commit() @@ -269,41 +319,61 @@ class ClientProviderService: except Exception as e: self.db.rollback() logger.error(f"Error deleting client/provider {client_id}: {str(e)}") - raise HTTPException(status_code=500, detail="Error deleting client/provider") - - def get_clients_only(self, skip: int = 0, limit: int = 100) -> List[ClientProviderBasicDTO]: + raise HTTPException( + status_code=500, detail="Error deleting client/provider" + ) + + def get_clients_only( + self, skip: int = 0, limit: int = 100 + ) -> List[ClientProviderBasicDTO]: """Obtiene solo clientes (C)""" - query = self.db.query(ClientProvider).filter(ClientProvider.client_or_provider == 'C') + query = self.db.query(ClientProvider).filter( + ClientProvider.client_or_provider == "C" + ) clients = query.offset(skip).limit(limit).all() return [ClientProviderBasicDTO.model_validate(client) for client in clients] - - def get_providers_only(self, skip: int = 0, limit: int = 100) -> List[ClientProviderBasicDTO]: + + def get_providers_only( + self, skip: int = 0, limit: int = 100 + ) -> List[ClientProviderBasicDTO]: """Obtiene solo proveedores (P)""" - query = self.db.query(ClientProvider).filter(ClientProvider.client_or_provider == 'P') + query = self.db.query(ClientProvider).filter( + ClientProvider.client_or_provider == "P" + ) providers = query.offset(skip).limit(limit).all() - return [ClientProviderBasicDTO.model_validate(provider) for provider in providers] - + return [ + ClientProviderBasicDTO.model_validate(provider) for provider in providers + ] + def search_by_rfc(self, rfc: str) -> List[ClientProviderBasicDTO]: """Busca clientes/proveedores por RFC""" - clients = self.db.query(ClientProvider).filter(ClientProvider.rfc.ilike(f"%{rfc}%")).all() + clients = ( + self.db.query(ClientProvider) + .filter(ClientProvider.rfc.ilike(f"%{rfc}%")) + .all() + ) return [ClientProviderBasicDTO.model_validate(client) for client in clients] - + def toggle_status(self, client_id: str) -> Optional[ClientProviderResponseDTO]: """Cambia el estado habilitado/deshabilitado""" - client = self.db.query(ClientProvider).filter(ClientProvider.client_id == client_id).first() + client = ( + self.db.query(ClientProvider) + .filter(ClientProvider.client_id == client_id) + .first() + ) if not client: return None - + # Toggle status (1 = habilitado, 0 = deshabilitado) client.enabled_disabled = 1 if client.enabled_disabled == 0 else 0 - + try: self.db.commit() - logger.info(f"Client/Provider status toggled: {client_id} -> {client.enabled_disabled}") + logger.info( + f"Client/Provider status toggled: {client_id} -> {client.enabled_disabled}" + ) return self._get_client_with_relations(client_id) except Exception as e: self.db.rollback() logger.error(f"Error toggling status for {client_id}: {str(e)}") raise HTTPException(status_code=500, detail="Error updating status") - - diff --git a/backend/api/v1/modules/a76/company/__init__.py b/backend/api/v1/modules/a76/company/__init__.py index 1930a6ac..6f72af22 100644 --- a/backend/api/v1/modules/a76/company/__init__.py +++ b/backend/api/v1/modules/a76/company/__init__.py @@ -1,6 +1,7 @@ """ Módulo de Company """ + from .routes import router __all__ = ["router"] diff --git a/backend/api/v1/modules/a76/company/dto.py b/backend/api/v1/modules/a76/company/dto.py index 592dd07b..104a014f 100644 --- a/backend/api/v1/modules/a76/company/dto.py +++ b/backend/api/v1/modules/a76/company/dto.py @@ -2,6 +2,7 @@ DTOs (Data Transfer Objects) para módulo de empresa Reemplaza schemas.py siguiendo enfoque DDD y estilo NestJS """ + from pydantic import BaseModel, Field from typing import Optional from datetime import datetime @@ -9,47 +10,80 @@ from datetime import datetime class CompanyCreateDTO(BaseModel): """DTO para crear una empresa""" - id: str = Field(default='EMP', max_length=3, description="Company ID") + + id: str = Field(default="EMP", max_length=3, description="Company ID") consecutive: bool = Field(default=True, description="Unique record control") name: Optional[str] = Field(None, max_length=255, description="Company name") rfc: Optional[str] = Field(None, max_length=30, description="Company RFC") - main_activity: Optional[str] = Field(None, max_length=255, description="Main activity") - + main_activity: Optional[str] = Field( + None, max_length=255, description="Main activity" + ) + # Program information program: Optional[str] = Field(None, max_length=10, description="Program") - program_number: Optional[str] = Field(None, max_length=40, description="Program number") + program_number: Optional[str] = Field( + None, max_length=40, description="Program number" + ) prosec: Optional[int] = Field(None, description="PROSEC") - prosec_authorization: Optional[str] = Field(None, max_length=20, description="PROSEC authorization") - + prosec_authorization: Optional[str] = Field( + None, max_length=20, description="PROSEC authorization" + ) + # Identifiers - manufacturer_id: Optional[str] = Field(None, max_length=25, description="Manufacturer ID") - broker_company: Optional[str] = Field(None, max_length=10, description="Broker company") - + manufacturer_id: Optional[str] = Field( + None, max_length=25, description="Manufacturer ID" + ) + broker_company: Optional[str] = Field( + None, max_length=10, description="Broker company" + ) + # Responsible person - responsible: Optional[str] = Field(None, max_length=80, description="Responsible person") - responsible_name: Optional[str] = Field(None, max_length=20, description="Responsible first name") - responsible_last_name: Optional[str] = Field(None, max_length=20, description="Responsible last name") - responsible_mother_last_name: Optional[str] = Field(None, max_length=20, description="Responsible mother's last name") - responsible_rfc: Optional[str] = Field(None, max_length=30, description="Responsible RFC") - position: Optional[str] = Field(None, max_length=30, description="Responsible position") - + responsible: Optional[str] = Field( + None, max_length=80, description="Responsible person" + ) + responsible_name: Optional[str] = Field( + None, max_length=20, description="Responsible first name" + ) + responsible_last_name: Optional[str] = Field( + None, max_length=20, description="Responsible last name" + ) + responsible_mother_last_name: Optional[str] = Field( + None, max_length=20, description="Responsible mother's last name" + ) + responsible_rfc: Optional[str] = Field( + None, max_length=30, description="Responsible RFC" + ) + position: Optional[str] = Field( + None, max_length=30, description="Responsible position" + ) + # Configuration logo: Optional[str] = Field(None, max_length=255, description="Company logo") has_express_line: Optional[bool] = Field(None, description="Has express line") - order_format_type: Optional[str] = Field(None, max_length=19, description="Order format type") + order_format_type: Optional[str] = Field( + None, max_length=19, description="Order format type" + ) previous_code: Optional[int] = Field(None, description="Previous code") is_service_company: Optional[bool] = Field(None, description="Is service company") - + # Client and subassembly client_name: Optional[str] = Field(None, max_length=300, description="Client name") - subassembly_mode: Optional[str] = Field(None, max_length=7, description="Subassembly mode") - + subassembly_mode: Optional[str] = Field( + None, max_length=7, description="Subassembly mode" + ) + # Additional information curp: Optional[str] = Field(None, max_length=19, description="CURP") - inter_db_name: Optional[str] = Field(None, max_length=100, description="Inter DB name") + inter_db_name: Optional[str] = Field( + None, max_length=100, description="Inter DB name" + ) ctpat_svi: Optional[str] = Field(None, max_length=100, description="CTPAT SVI") - trusted_exporter_number: Optional[str] = Field(None, max_length=50, description="Trusted exporter number") - prevalidator_key: Optional[str] = Field(None, max_length=20, description="Prevalidator key") + trusted_exporter_number: Optional[str] = Field( + None, max_length=50, description="Trusted exporter number" + ) + prevalidator_key: Optional[str] = Field( + None, max_length=20, description="Prevalidator key" + ) seventh_amendment: Optional[bool] = Field(None, description="Seventh amendment") class Config: @@ -58,45 +92,78 @@ class CompanyCreateDTO(BaseModel): class CompanyUpdateDTO(BaseModel): """DTO para actualizar una empresa""" + name: Optional[str] = Field(None, max_length=255, description="Company name") rfc: Optional[str] = Field(None, max_length=30, description="Company RFC") - main_activity: Optional[str] = Field(None, max_length=255, description="Main activity") - + main_activity: Optional[str] = Field( + None, max_length=255, description="Main activity" + ) + # Program information program: Optional[str] = Field(None, max_length=10, description="Program") - program_number: Optional[str] = Field(None, max_length=40, description="Program number") + program_number: Optional[str] = Field( + None, max_length=40, description="Program number" + ) prosec: Optional[int] = Field(None, description="PROSEC") - prosec_authorization: Optional[str] = Field(None, max_length=20, description="PROSEC authorization") - + prosec_authorization: Optional[str] = Field( + None, max_length=20, description="PROSEC authorization" + ) + # Identifiers - manufacturer_id: Optional[str] = Field(None, max_length=25, description="Manufacturer ID") - broker_company: Optional[str] = Field(None, max_length=10, description="Broker company") - + manufacturer_id: Optional[str] = Field( + None, max_length=25, description="Manufacturer ID" + ) + broker_company: Optional[str] = Field( + None, max_length=10, description="Broker company" + ) + # Responsible person - responsible: Optional[str] = Field(None, max_length=80, description="Responsible person") - responsible_name: Optional[str] = Field(None, max_length=20, description="Responsible first name") - responsible_last_name: Optional[str] = Field(None, max_length=20, description="Responsible last name") - responsible_mother_last_name: Optional[str] = Field(None, max_length=20, description="Responsible mother's last name") - responsible_rfc: Optional[str] = Field(None, max_length=30, description="Responsible RFC") - position: Optional[str] = Field(None, max_length=30, description="Responsible position") - + responsible: Optional[str] = Field( + None, max_length=80, description="Responsible person" + ) + responsible_name: Optional[str] = Field( + None, max_length=20, description="Responsible first name" + ) + responsible_last_name: Optional[str] = Field( + None, max_length=20, description="Responsible last name" + ) + responsible_mother_last_name: Optional[str] = Field( + None, max_length=20, description="Responsible mother's last name" + ) + responsible_rfc: Optional[str] = Field( + None, max_length=30, description="Responsible RFC" + ) + position: Optional[str] = Field( + None, max_length=30, description="Responsible position" + ) + # Configuration logo: Optional[str] = Field(None, max_length=255, description="Company logo") has_express_line: Optional[bool] = Field(None, description="Has express line") - order_format_type: Optional[str] = Field(None, max_length=19, description="Order format type") + order_format_type: Optional[str] = Field( + None, max_length=19, description="Order format type" + ) previous_code: Optional[int] = Field(None, description="Previous code") is_service_company: Optional[bool] = Field(None, description="Is service company") - + # Client and subassembly client_name: Optional[str] = Field(None, max_length=300, description="Client name") - subassembly_mode: Optional[str] = Field(None, max_length=7, description="Subassembly mode") - + subassembly_mode: Optional[str] = Field( + None, max_length=7, description="Subassembly mode" + ) + # Additional information curp: Optional[str] = Field(None, max_length=19, description="CURP") - inter_db_name: Optional[str] = Field(None, max_length=100, description="Inter DB name") + inter_db_name: Optional[str] = Field( + None, max_length=100, description="Inter DB name" + ) ctpat_svi: Optional[str] = Field(None, max_length=100, description="CTPAT SVI") - trusted_exporter_number: Optional[str] = Field(None, max_length=50, description="Trusted exporter number") - prevalidator_key: Optional[str] = Field(None, max_length=20, description="Prevalidator key") + trusted_exporter_number: Optional[str] = Field( + None, max_length=50, description="Trusted exporter number" + ) + prevalidator_key: Optional[str] = Field( + None, max_length=20, description="Prevalidator key" + ) seventh_amendment: Optional[bool] = Field(None, description="Seventh amendment") class Config: @@ -105,22 +172,23 @@ class CompanyUpdateDTO(BaseModel): class CompanyResponseDTO(BaseModel): """DTO para respuesta de empresa""" - id: str - consecutive: bool + + id: int + tenant_id: int name: Optional[str] = None rfc: Optional[str] = None main_activity: Optional[str] = None - + # Program information program: Optional[str] = None program_number: Optional[str] = None prosec: Optional[int] = None prosec_authorization: Optional[str] = None - + # Identifiers manufacturer_id: Optional[str] = None broker_company: Optional[str] = None - + # Responsible person responsible: Optional[str] = None responsible_name: Optional[str] = None @@ -128,18 +196,18 @@ class CompanyResponseDTO(BaseModel): responsible_mother_last_name: Optional[str] = None responsible_rfc: Optional[str] = None position: Optional[str] = None - + # Configuration logo: Optional[str] = None has_express_line: Optional[bool] = None order_format_type: Optional[str] = None previous_code: Optional[int] = None is_service_company: Optional[bool] = None - + # Client and subassembly client_name: Optional[str] = None subassembly_mode: Optional[str] = None - + # Additional information curp: Optional[str] = None inter_db_name: Optional[str] = None @@ -147,11 +215,10 @@ class CompanyResponseDTO(BaseModel): trusted_exporter_number: Optional[str] = None prevalidator_key: Optional[str] = None seventh_amendment: Optional[bool] = None - + # Timestamps created_at: datetime updated_at: Optional[datetime] = None class Config: from_attributes = True - diff --git a/backend/api/v1/modules/a76/company/models.py b/backend/api/v1/modules/a76/company/models.py index 8ab99331..6809b658 100644 --- a/backend/api/v1/modules/a76/company/models.py +++ b/backend/api/v1/modules/a76/company/models.py @@ -1,9 +1,20 @@ """ Modelos ORM para gestión de empresa """ + from typing import Optional from datetime import datetime -from sqlalchemy import DateTime, Integer, String, Boolean, SmallInteger, ForeignKey, PrimaryKeyConstraint, ForeignKeyConstraint, UniqueConstraint +from sqlalchemy import ( + DateTime, + Integer, + String, + Boolean, + SmallInteger, + ForeignKey, + PrimaryKeyConstraint, + ForeignKeyConstraint, + UniqueConstraint, +) from sqlalchemy.sql import func from sqlalchemy.orm import Mapped, mapped_column from core.database import Base @@ -13,32 +24,35 @@ class Company(Base): """ Modelo para la tabla Company - Información de la empresa """ + __tablename__ = "company" __table_args__ = ( - PrimaryKeyConstraint('id', name='company_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_company_tenant'), - {"schema": "a76"} + PrimaryKeyConstraint("id", name="company_pkey"), + ForeignKeyConstraint( + ["tenant_id"], ["a76.tenants.id"], name="fk_company_tenant" + ), + {"schema": "a76"}, ) - + # Primary key id: Mapped[int] = mapped_column(Integer, primary_key=True) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - + # Información básica de la empresa name: Mapped[Optional[str]] = mapped_column(String(255)) rfc: Mapped[Optional[str]] = mapped_column(String(30)) main_activity: Mapped[Optional[str]] = mapped_column(String(255)) - + # Información del programa program: Mapped[Optional[str]] = mapped_column(String(10)) program_number: Mapped[Optional[str]] = mapped_column(String(40)) prosec: Mapped[Optional[int]] = mapped_column(SmallInteger) prosec_authorization: Mapped[Optional[str]] = mapped_column(String(20)) - + # Identificadores manufacturer_id: Mapped[Optional[str]] = mapped_column(String(25)) broker_company: Mapped[Optional[str]] = mapped_column(String(10)) - + # Responsable responsible: Mapped[Optional[str]] = mapped_column(String(80)) responsible_name: Mapped[Optional[str]] = mapped_column(String(20)) @@ -46,29 +60,33 @@ class Company(Base): responsible_mother_last_name: Mapped[Optional[str]] = mapped_column(String(20)) responsible_rfc: Mapped[Optional[str]] = mapped_column(String(30)) position: Mapped[Optional[str]] = mapped_column(String(30)) - + # Configuración logo: Mapped[Optional[str]] = mapped_column(String(255)) has_express_line: Mapped[Optional[bool]] = mapped_column(Boolean) order_format_type: Mapped[Optional[str]] = mapped_column(String(19)) previous_code: Mapped[Optional[int]] = mapped_column(SmallInteger) is_service_company: Mapped[Optional[bool]] = mapped_column(Boolean) - + # Cliente y submaquila client_name: Mapped[Optional[str]] = mapped_column(String(300)) subassembly_mode: Mapped[Optional[str]] = mapped_column(String(7)) - + # Información adicional curp: Mapped[Optional[str]] = mapped_column(String(19)) inter_db_name: Mapped[Optional[str]] = mapped_column(String(100)) ctpat_svi: Mapped[Optional[str]] = mapped_column(String(100)) trusted_exporter_number: Mapped[Optional[str]] = mapped_column(String(50)) prevalidator_key: Mapped[Optional[str]] = mapped_column(String(20)) - seventh_amendment: Mapped[Optional[bool]] = mapped_column(Boolean) # FINALCONTADORAELECTRONICO renombrado - + seventh_amendment: Mapped[Optional[bool]] = mapped_column( + Boolean + ) # FINALCONTADORAELECTRONICO renombrado + # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) - updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) - deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - - + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now(), onupdate=func.now() + ) + deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) diff --git a/backend/api/v1/modules/a76/company/routes.py b/backend/api/v1/modules/a76/company/routes.py index 1b72f7a0..a948dfe0 100644 --- a/backend/api/v1/modules/a76/company/routes.py +++ b/backend/api/v1/modules/a76/company/routes.py @@ -1,27 +1,30 @@ """ Endpoints API para gestión de empresa """ + from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from typing import Optional from core.database import get_core_db -from core.security import get_current_user, has_role +from core.security import get_current_user, has_role, get_tenant_from_token from .service import CompanyService from .dto import CompanyCreateDTO, CompanyUpdateDTO, CompanyResponseDTO router = APIRouter(prefix="/company") -@router.post("/", response_model=CompanyResponseDTO, status_code=status.HTTP_201_CREATED) +@router.post( + "/", response_model=CompanyResponseDTO, status_code=status.HTTP_201_CREATED +) async def create_company( company_data: CompanyCreateDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Create a new company in the system - + Only one company can exist per system due to the unique consecutive field. """ service = CompanyService(db) @@ -30,12 +33,11 @@ async def create_company( @router.get("/", response_model=Optional[CompanyResponseDTO]) async def get_company( - db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) ): """ Get the registered company information - + Returns the unique company in the system or None if it doesn't exist. """ service = CompanyService(db) @@ -45,73 +47,48 @@ async def get_company( return company -@router.get("/{company_id}", response_model=CompanyResponseDTO) -async def get_company_by_id( - company_id: str, - db: Session = Depends(get_core_db), +@router.get("/my-companies", response_model=list[CompanyResponseDTO]) +async def get_my_companies( + db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) ): """ - Get company by specific ID - """ - service = CompanyService(db) - company = service.get_company_by_id(company_id) - if not company: - raise HTTPException(status_code=404, detail=f"Company with ID '{company_id}' not found") - return company - - -@router.put("/{company_id}", response_model=CompanyResponseDTO) -async def update_company( - company_id: str, - company_data: CompanyUpdateDTO, - db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) -): - """ - Update company information - """ - service = CompanyService(db) - company = service.update_company(company_id, company_data) - if not company: - raise HTTPException(status_code=404, detail=f"Company with ID '{company_id}' not found") - return company - - -@router.delete("/{company_id}", status_code=status.HTTP_204_NO_CONTENT) -async def delete_company( - company_id: str, - db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) -): - """ - Delete company from the system + Get all companies that belong to the user's tenant - Note: This will completely remove the company from the system. + Returns a list of companies associated with the tenant_id from the user's token """ + tenant_id = get_tenant_from_token(current_user) + if not tenant_id: + raise HTTPException( + status_code=400, + detail="Tenant ID not found in token" + ) + service = CompanyService(db) - if not service.delete_company(company_id): - raise HTTPException(status_code=404, detail=f"Company with ID '{company_id}' not found") + companies = service.get_companies_by_tenant(tenant_id) + + return companies @router.get("/status/exists", response_model=dict) async def check_company_exists( - db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) ): """ Check if a company is registered in the system """ service = CompanyService(db) exists = service.exists_company() - return {"exists": exists, "message": "Company found" if exists else "No company registered"} + return { + "exists": exists, + "message": "Company found" if exists else "No company registered", + } # Specific endpoints for important fields @router.get("/info/basic", response_model=dict) async def get_company_basic_info( - db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) ): """ Get basic company information (name, RFC, main activity) @@ -120,19 +97,18 @@ async def get_company_basic_info( company = service.get_company() if not company: raise HTTPException(status_code=404, detail="No company found") - + return { "name": company.name, "rfc": company.rfc, "main_activity": company.main_activity, - "logo": company.logo + "logo": company.logo, } @router.get("/info/responsible", response_model=dict) async def get_company_responsible_info( - db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) ): """ Get company responsible person information @@ -141,21 +117,20 @@ async def get_company_responsible_info( company = service.get_company() if not company: raise HTTPException(status_code=404, detail="No company found") - + return { "responsible": company.responsible, "responsible_name": company.responsible_name, "responsible_last_name": company.responsible_last_name, "responsible_mother_last_name": company.responsible_mother_last_name, "responsible_rfc": company.responsible_rfc, - "position": company.position + "position": company.position, } @router.get("/info/program", response_model=dict) async def get_company_program_info( - db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) ): """ Get company program information @@ -164,13 +139,67 @@ async def get_company_program_info( company = service.get_company() if not company: raise HTTPException(status_code=404, detail="No company found") - + return { "program": company.program, "program_number": company.program_number, "prosec": company.prosec, "prosec_authorization": company.prosec_authorization, - "manufacturer_id": company.manufacturer_id + "manufacturer_id": company.manufacturer_id, } +@router.get("/{company_id}", response_model=CompanyResponseDTO) +async def get_company_by_id( + company_id: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + """ + Get company by specific ID + """ + service = CompanyService(db) + company = service.get_company_by_id(company_id) + if not company: + raise HTTPException( + status_code=404, detail=f"Company with ID '{company_id}' not found" + ) + return company + + +@router.put("/{company_id}", response_model=CompanyResponseDTO) +async def update_company( + company_id: str, + company_data: CompanyUpdateDTO, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + """ + Update company information + """ + service = CompanyService(db) + company = service.update_company(company_id, company_data) + if not company: + raise HTTPException( + status_code=404, detail=f"Company with ID '{company_id}' not found" + ) + return company + + +@router.delete("/{company_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_company( + company_id: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + """ + Delete company from the system + + Note: This will completely remove the company from the system. + """ + service = CompanyService(db) + if not service.delete_company(company_id): + raise HTTPException( + status_code=404, detail=f"Company with ID '{company_id}' not found" + ) + diff --git a/backend/api/v1/modules/a76/company/service.py b/backend/api/v1/modules/a76/company/service.py index 249f7803..67ccdfbf 100644 --- a/backend/api/v1/modules/a76/company/service.py +++ b/backend/api/v1/modules/a76/company/service.py @@ -1,6 +1,7 @@ """ Capa de servicio para lógica de negocio de empresa """ + from sqlalchemy.orm import Session from sqlalchemy.exc import IntegrityError from fastapi import HTTPException @@ -15,29 +16,34 @@ logger = logging.getLogger(__name__) class CompanyService: """Servicio para gestión de empresa""" - + def __init__(self, db: Session): self.db = db - + def create_company(self, company_data: CompanyCreateDTO) -> CompanyResponseDTO: """ Crea una nueva empresa en el sistema - + Args: company_data: Datos de la empresa a crear - + Returns: CompanyResponseDTO con información de la empresa creada - + Raises: HTTPException: Si ya existe una empresa o error en la creación """ try: # Verificar que no exista ya una empresa (solo puede haber una por el consecutivo único) - existing = self.db.query(Company).filter(Company.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") - + raise HTTPException( + status_code=400, + detail="A company is already registered in the system", + ) + # Crear empresa db_company = Company( id=company_data.id, @@ -69,32 +75,35 @@ class CompanyService: ctpat_svi=company_data.ctpat_svi, trusted_exporter_number=company_data.trusted_exporter_number, prevalidator_key=company_data.prevalidator_key, - seventh_amendment=company_data.seventh_amendment + seventh_amendment=company_data.seventh_amendment, ) - + self.db.add(db_company) self.db.commit() self.db.refresh(db_company) - + logger.info(f"Company created: {db_company.id} - {db_company.name}") - + return CompanyResponseDTO.model_validate(db_company) - + except IntegrityError as e: self.db.rollback() logger.error(f"IntegrityError creating company: {str(e)}") - raise HTTPException(status_code=400, detail="Integrity error: A company already exists in the system") + raise HTTPException( + status_code=400, + detail="Integrity error: A company already exists in the system", + ) except HTTPException: raise except Exception as e: self.db.rollback() logger.error(f"Error creating company: {str(e)}") raise HTTPException(status_code=500, detail="Error creating company") - + def get_company(self) -> Optional[CompanyResponseDTO]: """ Obtiene la empresa (solo puede haber una) - + Returns: CompanyResponseDTO o None si no existe """ @@ -102,14 +111,14 @@ class CompanyService: if not company: return None return CompanyResponseDTO.model_validate(company) - + def get_company_by_id(self, company_id: str) -> Optional[CompanyResponseDTO]: """ Obtiene una empresa por ID - + Args: company_id: ID de la empresa - + Returns: CompanyResponseDTO o None si no existe """ @@ -117,27 +126,29 @@ class CompanyService: if not company: return None return CompanyResponseDTO.model_validate(company) - - def update_company(self, company_id: str, company_data: CompanyUpdateDTO) -> Optional[CompanyResponseDTO]: + + def update_company( + self, company_id: str, company_data: CompanyUpdateDTO + ) -> Optional[CompanyResponseDTO]: """ Actualiza una empresa - + Args: company_id: ID de la empresa a actualizar company_data: Datos a actualizar - + Returns: CompanyResponseDTO actualizada o None si no existe """ company = self.db.query(Company).filter(Company.id == company_id).first() if not company: return None - + # Actualizar solo campos proporcionados update_data = company_data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(company, field, value) - + try: self.db.commit() self.db.refresh(company) @@ -147,21 +158,21 @@ class CompanyService: self.db.rollback() logger.error(f"Error updating company {company_id}: {str(e)}") raise HTTPException(status_code=500, detail="Error updating company") - + def delete_company(self, company_id: str) -> bool: """ Elimina una empresa - + Args: company_id: ID de la empresa a eliminar - + Returns: True si se eliminó, False si no existe """ company = self.db.query(Company).filter(Company.id == company_id).first() if not company: return False - + try: self.db.delete(company) self.db.commit() @@ -171,14 +182,34 @@ class CompanyService: self.db.rollback() logger.error(f"Error deleting company {company_id}: {str(e)}") raise HTTPException(status_code=500, detail="Error deleting company") - + def exists_company(self) -> bool: """ Verifica si existe una empresa registrada - + Returns: True si existe una empresa, False en caso contrario """ - return self.db.query(Company).filter(Company.consecutive == True).first() is not None + return ( + self.db.query(Company).filter(Company.consecutive == True).first() + is not None + ) + def get_companies_by_tenant(self, tenant_id: int) -> List[CompanyResponseDTO]: + """ + Obtiene todas las compañías que pertenecen a un tenant específico + Args: + tenant_id: ID del tenant + + Returns: + Lista de CompanyResponseDTO + """ + companies = ( + self.db.query(Company) + .filter(Company.tenant_id == tenant_id) + .order_by(Company.name) + .all() + ) + + return [CompanyResponseDTO.model_validate(company) for company in companies] diff --git a/backend/api/v1/modules/a76/country_rule_oct/dto.py b/backend/api/v1/modules/a76/country_rule_oct/dto.py index 0dd0d789..8352bb4d 100644 --- a/backend/api/v1/modules/a76/country_rule_oct/dto.py +++ b/backend/api/v1/modules/a76/country_rule_oct/dto.py @@ -4,15 +4,18 @@ DTOs for CountryRuleOct. from pydantic import BaseModel + class CountryRuleOctBaseDTO(BaseModel): permission: str line: int fraction: str country_code: str + class CountryRuleOctCreateDTO(CountryRuleOctBaseDTO): pass + class CountryRuleOctResponseDTO(CountryRuleOctBaseDTO): class Config: - from_attributes = True \ No newline at end of file + from_attributes = True diff --git a/backend/api/v1/modules/a76/country_rule_oct/models.py b/backend/api/v1/modules/a76/country_rule_oct/models.py index 38eb2b29..2e02b91d 100644 --- a/backend/api/v1/modules/a76/country_rule_oct/models.py +++ b/backend/api/v1/modules/a76/country_rule_oct/models.py @@ -1,4 +1,11 @@ -from sqlalchemy import Integer, String, ForeignKey, PrimaryKeyConstraint, ForeignKeyConstraint, UniqueConstraint +from sqlalchemy import ( + Integer, + String, + ForeignKey, + PrimaryKeyConstraint, + ForeignKeyConstraint, + UniqueConstraint, +) from sqlalchemy.orm import Mapped, mapped_column from core.database import Base @@ -6,25 +13,42 @@ from core.database import Base class CountryRuleOct(Base): __tablename__ = "country_rule_oct" __table_args__ = ( - PrimaryKeyConstraint('id', name='country_rule_oct_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_country_rule_oct_tenant'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_country_rule_oct_company'), + PrimaryKeyConstraint("id", name="country_rule_oct_pkey"), ForeignKeyConstraint( - ['tenant_id', 'company_id', 'permission', 'line', 'fraction'], - ['a76.fraction_rule_octave.tenant_id', 'a76.fraction_rule_octave.company_id', 'a76.fraction_rule_octave.permission', 'a76.fraction_rule_octave.line', 'a76.fraction_rule_octave.fraction'], - ondelete="CASCADE", - name='fk_country_rule_oct_frac_octava' - ), - UniqueConstraint('tenant_id', 'company_id', 'permission', 'line', 'fraction', 'country_code', name='uq_country_rule_oct_permission_line_fraction_country'), - {"schema": "a76"} + ["tenant_id"], ["a76.tenants.id"], name="fk_country_rule_oct_tenant" + ), + ForeignKeyConstraint( + ["company_id"], ["a76.company.id"], name="fk_country_rule_oct_company" + ), + ForeignKeyConstraint( + ["tenant_id", "company_id", "permission", "line", "fraction"], + [ + "a76.fraction_rule_octave.tenant_id", + "a76.fraction_rule_octave.company_id", + "a76.fraction_rule_octave.permission", + "a76.fraction_rule_octave.line", + "a76.fraction_rule_octave.fraction", + ], + ondelete="CASCADE", + name="fk_country_rule_oct_frac_octava", + ), + UniqueConstraint( + "tenant_id", + "company_id", + "permission", + "line", + "fraction", + "country_code", + name="uq_country_rule_oct_permission_line_fraction_country", + ), + {"schema": "a76"}, ) id: Mapped[int] = mapped_column(Integer, primary_key=True) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - + permission: Mapped[str] = mapped_column(String(20)) line: Mapped[int] = mapped_column() fraction: Mapped[str] = mapped_column(String(10)) country_code: Mapped[str] = mapped_column(String(3)) - \ No newline at end of file diff --git a/backend/api/v1/modules/a76/country_rule_oct/routes.py b/backend/api/v1/modules/a76/country_rule_oct/routes.py index 3d40c0b9..85a75ec0 100644 --- a/backend/api/v1/modules/a76/country_rule_oct/routes.py +++ b/backend/api/v1/modules/a76/country_rule_oct/routes.py @@ -12,8 +12,7 @@ router = APIRouter(prefix="/country-rule-oct", tags=["CountryRuleOct"]) @router.get("/", response_model=List[CountryRuleOctResponseDTO]) async def list_countries( - db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) ): """ List all CountryRuleOct entries. @@ -28,14 +27,17 @@ async def list_countries( return db.query(CountryRuleOctService).all() -@router.get("/{permission}/{line}/{fraction}/{country_code}", response_model=CountryRuleOctResponseDTO) +@router.get( + "/{permission}/{line}/{fraction}/{country_code}", + response_model=CountryRuleOctResponseDTO, +) async def read_country_rule( permission: str, line: int, fraction: str, country_code: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Get a specific CountryRuleOct by its composite key. @@ -53,11 +55,13 @@ async def read_country_rule( return country -@router.post("/", response_model=CountryRuleOctResponseDTO, status_code=status.HTTP_201_CREATED) +@router.post( + "/", response_model=CountryRuleOctResponseDTO, status_code=status.HTTP_201_CREATED +) async def create_country_rule( country_data: CountryRuleOctCreateDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Create a new CountryRuleOct entry. @@ -65,18 +69,23 @@ async def create_country_rule( return CountryRuleOctService.create_country_rule(db, country_data) -@router.delete("/{permission}/{line}/{fraction}/{country_code}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{permission}/{line}/{fraction}/{country_code}", + status_code=status.HTTP_204_NO_CONTENT, +) async def delete_country_rule( permission: str, line: int, fraction: str, country_code: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Delete a CountryRuleOct by its composite key. """ - country = CountryRuleOctService.delete_country_rule(db, permission, line, fraction, country_code) + country = CountryRuleOctService.delete_country_rule( + db, permission, line, fraction, country_code + ) if not country: - raise HTTPException(status_code=404, detail="CountryRuleOct not found") \ No newline at end of file + raise HTTPException(status_code=404, detail="CountryRuleOct not found") diff --git a/backend/api/v1/modules/a76/country_rule_oct/services.py b/backend/api/v1/modules/a76/country_rule_oct/services.py index 43cc7bd5..1ccb6887 100644 --- a/backend/api/v1/modules/a76/country_rule_oct/services.py +++ b/backend/api/v1/modules/a76/country_rule_oct/services.py @@ -5,15 +5,22 @@ Service layer for CountryRuleOct. from sqlalchemy.orm import Session from . import models, dto + class CountryRuleOctService: @staticmethod - def get_country_by_keys(db: Session, permission: str, line: int, fraction: str, country_code: str): - return db.query(models.CountryRuleOct).filter( - models.CountryRuleOct.permission == permission, - models.CountryRuleOct.line == line, - models.CountryRuleOct.fraction == fraction, - models.CountryRuleOct.country_code == country_code - ).first() + def get_country_by_keys( + db: Session, permission: str, line: int, fraction: str, country_code: str + ): + return ( + db.query(models.CountryRuleOct) + .filter( + models.CountryRuleOct.permission == permission, + models.CountryRuleOct.line == line, + models.CountryRuleOct.fraction == fraction, + models.CountryRuleOct.country_code == country_code, + ) + .first() + ) @staticmethod def create_country_rule(db: Session, country_data: dto.CountryRuleOctCreateDTO): @@ -24,9 +31,13 @@ class CountryRuleOctService: return new_country @staticmethod - def delete_country_rule(db: Session, permission: str, line: int, fraction: str, country_code: str): - country = CountryRuleOctService.get_country_by_keys(db, permission, line, fraction, country_code) + def delete_country_rule( + db: Session, permission: str, line: int, fraction: str, country_code: str + ): + country = CountryRuleOctService.get_country_by_keys( + db, permission, line, fraction, country_code + ) if country: db.delete(country) db.commit() - return country \ No newline at end of file + return country diff --git a/backend/api/v1/modules/a76/exchange_rate/dto.py b/backend/api/v1/modules/a76/exchange_rate/dto.py index ffc520ea..d5ca838d 100644 --- a/backend/api/v1/modules/a76/exchange_rate/dto.py +++ b/backend/api/v1/modules/a76/exchange_rate/dto.py @@ -1,15 +1,18 @@ from pydantic import BaseModel from typing import Optional + class ExchangeRateBaseDTO(BaseModel): date: int value: Optional[float] local_currency: Optional[str] foreign_currency: Optional[str] + class ExchangeRateCreateDTO(ExchangeRateBaseDTO): pass + class ExchangeRateResponseDTO(ExchangeRateBaseDTO): class Config: - from_attributes = True \ No newline at end of file + from_attributes = True diff --git a/backend/api/v1/modules/a76/exchange_rate/models.py b/backend/api/v1/modules/a76/exchange_rate/models.py index b8422fb4..33beb269 100644 --- a/backend/api/v1/modules/a76/exchange_rate/models.py +++ b/backend/api/v1/modules/a76/exchange_rate/models.py @@ -1,6 +1,15 @@ from typing import Optional from decimal import Decimal -from sqlalchemy import Integer, String, DECIMAL, PrimaryKeyConstraint, DateTime, ForeignKeyConstraint, UniqueConstraint, ForeignKey +from sqlalchemy import ( + Integer, + String, + DECIMAL, + PrimaryKeyConstraint, + DateTime, + ForeignKeyConstraint, + UniqueConstraint, + ForeignKey, +) from sqlalchemy.orm import Mapped, mapped_column from core.database import Base @@ -8,18 +17,24 @@ from core.database import Base class ExchangeRate(Base): __tablename__ = "exchange_rate" __table_args__ = ( - PrimaryKeyConstraint('id', name='exchange_rate_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_exchange_rate_tenant'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_exchange_rate_company'), - UniqueConstraint('tenant_id', 'company_id', 'date', name='uq_exchange_rate_date_tenant'), - {"schema": "a76"} + PrimaryKeyConstraint("id", name="exchange_rate_pkey"), + ForeignKeyConstraint( + ["tenant_id"], ["a76.tenants.id"], name="fk_exchange_rate_tenant" + ), + ForeignKeyConstraint( + ["company_id"], ["a76.company.id"], name="fk_exchange_rate_company" + ), + UniqueConstraint( + "tenant_id", "company_id", "date", name="uq_exchange_rate_date_tenant" + ), + {"schema": "a76"}, ) id: Mapped[int] = mapped_column(Integer, primary_key=True) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - + date: Mapped[int] = mapped_column(DateTime) value: Mapped[Optional[Decimal]] = mapped_column(DECIMAL(13, 6)) local_currency: Mapped[Optional[str]] = mapped_column(String(7)) - foreign_currency: Mapped[Optional[str]] = mapped_column(String(7)) \ No newline at end of file + foreign_currency: Mapped[Optional[str]] = mapped_column(String(7)) diff --git a/backend/api/v1/modules/a76/exchange_rate/routes.py b/backend/api/v1/modules/a76/exchange_rate/routes.py index 0446f3c0..51ccaea9 100644 --- a/backend/api/v1/modules/a76/exchange_rate/routes.py +++ b/backend/api/v1/modules/a76/exchange_rate/routes.py @@ -12,8 +12,7 @@ router = APIRouter(prefix="/exchange-rate", tags=["ExchangeRate"]) @router.get("/", response_model=List[ExchangeRateResponseDTO]) async def list_exchange_rates( - db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) ): """ List all ExchangeRate entries. @@ -32,7 +31,7 @@ async def list_exchange_rates( async def read_exchange_rate( date: int, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Get a specific ExchangeRate by its date. @@ -50,11 +49,13 @@ async def read_exchange_rate( return exchange_rate -@router.post("/", response_model=ExchangeRateResponseDTO, status_code=status.HTTP_201_CREATED) +@router.post( + "/", response_model=ExchangeRateResponseDTO, status_code=status.HTTP_201_CREATED +) async def create_exchange_rate( exchange_rate_data: ExchangeRateCreateDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Create a new ExchangeRate entry. @@ -73,7 +74,7 @@ async def create_exchange_rate( async def delete_exchange_rate( date: int, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Delete an ExchangeRate by its date. @@ -87,4 +88,4 @@ async def delete_exchange_rate( exchange_rate = ExchangeRateService.delete_exchange_rate(db, date) if not exchange_rate: - raise HTTPException(status_code=404, detail="ExchangeRate not found") \ No newline at end of file + raise HTTPException(status_code=404, detail="ExchangeRate not found") diff --git a/backend/api/v1/modules/a76/exchange_rate/services.py b/backend/api/v1/modules/a76/exchange_rate/services.py index e833fba0..3ab65bb4 100644 --- a/backend/api/v1/modules/a76/exchange_rate/services.py +++ b/backend/api/v1/modules/a76/exchange_rate/services.py @@ -1,13 +1,20 @@ from sqlalchemy.orm import Session from . import models, dto + class ExchangeRateService: @staticmethod def get_exchange_rate_by_date(db: Session, date: int): - return db.query(models.ExchangeRate).filter(models.ExchangeRate.date == date).first() + return ( + db.query(models.ExchangeRate) + .filter(models.ExchangeRate.date == date) + .first() + ) @staticmethod - def create_exchange_rate(db: Session, exchange_rate_data: dto.ExchangeRateCreateDTO): + def create_exchange_rate( + db: Session, exchange_rate_data: dto.ExchangeRateCreateDTO + ): new_exchange_rate = models.ExchangeRate(**exchange_rate_data.dict()) db.add(new_exchange_rate) db.commit() @@ -20,4 +27,4 @@ class ExchangeRateService: if exchange_rate: db.delete(exchange_rate) db.commit() - return exchange_rate \ No newline at end of file + return exchange_rate diff --git a/backend/api/v1/modules/a76/fraction_rule_octave/dto.py b/backend/api/v1/modules/a76/fraction_rule_octave/dto.py index e5226499..7c312028 100644 --- a/backend/api/v1/modules/a76/fraction_rule_octave/dto.py +++ b/backend/api/v1/modules/a76/fraction_rule_octave/dto.py @@ -5,6 +5,7 @@ DTOs for FractionRuleOctave. from pydantic import BaseModel from typing import Optional + class FractionRuleOctaveBaseDTO(BaseModel): PERMISSION: str LINE: int @@ -16,9 +17,11 @@ class FractionRuleOctaveBaseDTO(BaseModel): UNIT_COST_ME: Optional[float] UNIT_MEASURE: Optional[str] + class FractionRuleOctaveCreateDTO(FractionRuleOctaveBaseDTO): pass + class FractionRuleOctaveResponseDTO(FractionRuleOctaveBaseDTO): class Config: - from_attributes = True \ No newline at end of file + from_attributes = True diff --git a/backend/api/v1/modules/a76/fraction_rule_octave/models.py b/backend/api/v1/modules/a76/fraction_rule_octave/models.py index 61d2ec2c..dba563cf 100644 --- a/backend/api/v1/modules/a76/fraction_rule_octave/models.py +++ b/backend/api/v1/modules/a76/fraction_rule_octave/models.py @@ -1,4 +1,11 @@ -from sqlalchemy import Integer, String, ForeignKey, PrimaryKeyConstraint, ForeignKeyConstraint, UniqueConstraint +from sqlalchemy import ( + Integer, + String, + ForeignKey, + PrimaryKeyConstraint, + ForeignKeyConstraint, + UniqueConstraint, +) from sqlalchemy.orm import Mapped, mapped_column from core.database import Base @@ -6,18 +13,28 @@ from core.database import Base class FractionRuleOctave(Base): __tablename__ = "fraction_rule_octave" __table_args__ = ( - PrimaryKeyConstraint('id', name='fraction_rule_octave_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_fraction_rule_octave_tenant'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_fraction_rule_octave_company'), - UniqueConstraint('tenant_id', 'company_id', 'permission', 'line', 'fraction', name='uq_fraction_rule_octave_permission_line_fraction'), - {"schema": "a76"} + PrimaryKeyConstraint("id", name="fraction_rule_octave_pkey"), + ForeignKeyConstraint( + ["tenant_id"], ["a76.tenants.id"], name="fk_fraction_rule_octave_tenant" + ), + ForeignKeyConstraint( + ["company_id"], ["a76.company.id"], name="fk_fraction_rule_octave_company" + ), + UniqueConstraint( + "tenant_id", + "company_id", + "permission", + "line", + "fraction", + name="uq_fraction_rule_octave_permission_line_fraction", + ), + {"schema": "a76"}, ) - + id: Mapped[int] = mapped_column(Integer, primary_key=True) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - + permission: Mapped[str] = mapped_column(String(20)) line: Mapped[int] = mapped_column(Integer) fraction: Mapped[str] = mapped_column(String(10)) - \ No newline at end of file diff --git a/backend/api/v1/modules/a76/fraction_rule_octave/routes.py b/backend/api/v1/modules/a76/fraction_rule_octave/routes.py index ab5b4363..3a6fc8cc 100644 --- a/backend/api/v1/modules/a76/fraction_rule_octave/routes.py +++ b/backend/api/v1/modules/a76/fraction_rule_octave/routes.py @@ -12,8 +12,7 @@ router = APIRouter(prefix="/fraction_rule_octave", tags=["FractionRuleOctave"]) @router.get("/", response_model=List[FractionRuleOctaveResponseDTO]) async def list_fractions( - db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) ): """ List all FractionRuleOctave entries. @@ -28,13 +27,15 @@ async def list_fractions( return db.query(FractionRuleOctaveService).all() -@router.get("/{permission}/{line}/{fraction}", response_model=FractionRuleOctaveResponseDTO) +@router.get( + "/{permission}/{line}/{fraction}", response_model=FractionRuleOctaveResponseDTO +) async def read_fraction( permission: str, line: int, fraction: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Get a specific FractionRuleOctave by its composite key. @@ -52,11 +53,15 @@ async def read_fraction( return frac -@router.post("/", response_model=FractionRuleOctaveResponseDTO, status_code=status.HTTP_201_CREATED) +@router.post( + "/", + response_model=FractionRuleOctaveResponseDTO, + status_code=status.HTTP_201_CREATED, +) async def create_frac( frac_data: FractionRuleOctaveCreateDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Create a new FractionRuleOctave entry. @@ -71,13 +76,15 @@ async def create_frac( return FractionRuleOctaveService.create_frac(db, frac_data) -@router.delete("/{permission}/{line}/{fraction}", status_code=status.HTTP_204_NO_CONTENT) +@router.delete( + "/{permission}/{line}/{fraction}", status_code=status.HTTP_204_NO_CONTENT +) async def delete_fraction( permission: str, line: int, fraction: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Delete a FractionRuleOctave by its composite key. @@ -91,4 +98,4 @@ async def delete_fraction( frac = FractionRuleOctaveService.delete_fraction(db, permission, line, fraction) if not frac: - raise HTTPException(status_code=404, detail="FractionRuleOctave not found") \ No newline at end of file + raise HTTPException(status_code=404, detail="FractionRuleOctave not found") diff --git a/backend/api/v1/modules/a76/fraction_rule_octave/services.py b/backend/api/v1/modules/a76/fraction_rule_octave/services.py index 0a1f2a16..ed910327 100644 --- a/backend/api/v1/modules/a76/fraction_rule_octave/services.py +++ b/backend/api/v1/modules/a76/fraction_rule_octave/services.py @@ -5,14 +5,21 @@ from . import models, dto Service layer for FractionRuleOctave. """ + class FractionRuleOctaveService: @staticmethod - def get_fraction_by_permission_line(db: Session, permission: str, line: int, fraction: str): - return db.query(models.FractionRuleOctave).filter( - models.FractionRuleOctave.permission == permission, - models.FractionRuleOctave.line == line, - models.FractionRuleOctave.fraction == fraction - ).first() + def get_fraction_by_permission_line( + db: Session, permission: str, line: int, fraction: str + ): + return ( + db.query(models.FractionRuleOctave) + .filter( + models.FractionRuleOctave.permission == permission, + models.FractionRuleOctave.line == line, + models.FractionRuleOctave.fraction == fraction, + ) + .first() + ) @staticmethod def create_frac(db: Session, frac_data: dto.FractionRuleOctaveCreateDTO): @@ -24,8 +31,10 @@ class FractionRuleOctaveService: @staticmethod def delete_fraction(db: Session, permission: str, line: int, fraction: str): - frac = FractionRuleOctaveService.get_fraction_by_permission_line(db, permission, line, fraction) + frac = FractionRuleOctaveService.get_fraction_by_permission_line( + db, permission, line, fraction + ) if frac: db.delete(frac) db.commit() - return frac \ No newline at end of file + return frac diff --git a/backend/api/v1/modules/a76/licenses/__init__.py b/backend/api/v1/modules/a76/licenses/__init__.py index feb458dd..fffdb512 100644 --- a/backend/api/v1/modules/a76/licenses/__init__.py +++ b/backend/api/v1/modules/a76/licenses/__init__.py @@ -1,6 +1,7 @@ """ Módulo de Licenses """ + from .routes import router __all__ = ["router"] diff --git a/backend/api/v1/modules/a76/licenses/dto.py b/backend/api/v1/modules/a76/licenses/dto.py index 9e968356..0e8937f8 100644 --- a/backend/api/v1/modules/a76/licenses/dto.py +++ b/backend/api/v1/modules/a76/licenses/dto.py @@ -1,6 +1,7 @@ """ DTOs para módulo de licencias """ + from pydantic import BaseModel, Field from typing import Optional from datetime import datetime @@ -9,6 +10,7 @@ from enum import Enum class LicensePlanDTO(str, Enum): """Planes de licencia""" + FREE = "free" BASIC = "basic" PROFESSIONAL = "professional" @@ -17,6 +19,7 @@ class LicensePlanDTO(str, Enum): class LicenseStatusDTO(str, Enum): """Estados de licencia""" + ACTIVE = "active" EXPIRED = "expired" SUSPENDED = "suspended" @@ -26,20 +29,25 @@ class LicenseStatusDTO(str, Enum): class LicenseCreateDTO(BaseModel): """DTO para crear una nueva licencia""" + tenant_id: int = Field(..., description="ID del tenant") plan: LicensePlanDTO = Field(..., description="Plan de licencia") max_users: int = Field(default=5, ge=1, description="Número máximo de usuarios") - max_storage_gb: int = Field(default=10, ge=1, description="Almacenamiento máximo en GB") - max_monthly_operations: int = Field(default=1000, ge=1, description="Operaciones mensuales máximas") - + max_storage_gb: int = Field( + default=10, ge=1, description="Almacenamiento máximo en GB" + ) + max_monthly_operations: int = Field( + default=1000, ge=1, description="Operaciones mensuales máximas" + ) + feature_api_access: bool = Field(default=True) feature_advanced_reports: bool = Field(default=False) feature_integrations: bool = Field(default=False) feature_dedicated_support: bool = Field(default=False) - + starts_at: datetime = Field(..., description="Fecha de inicio de vigencia") expires_at: datetime = Field(..., description="Fecha de expiración") - + class Config: json_schema_extra = { "example": { @@ -53,60 +61,63 @@ class LicenseCreateDTO(BaseModel): "feature_integrations": True, "feature_dedicated_support": False, "starts_at": "2025-01-01T00:00:00Z", - "expires_at": "2025-12-31T23:59:59Z" + "expires_at": "2025-12-31T23:59:59Z", } } class LicenseUpdateDTO(BaseModel): """DTO para actualizar una licencia""" + plan: Optional[LicensePlanDTO] = None status: Optional[LicenseStatusDTO] = None max_users: Optional[int] = Field(None, ge=1) max_storage_gb: Optional[int] = Field(None, ge=1) max_monthly_operations: Optional[int] = Field(None, ge=1) - + feature_api_access: Optional[bool] = None feature_advanced_reports: Optional[bool] = None feature_integrations: Optional[bool] = None feature_dedicated_support: Optional[bool] = None - + expires_at: Optional[datetime] = None class LicenseResponseDTO(BaseModel): """DTO para respuesta de licencia""" + id: int tenant_id: int plan: LicensePlanDTO status: LicenseStatusDTO - + max_users: int max_storage_gb: int max_monthly_operations: int - + feature_api_access: bool feature_advanced_reports: bool feature_integrations: bool feature_dedicated_support: bool - + starts_at: datetime expires_at: datetime created_at: datetime updated_at: datetime - + class Config: from_attributes = True class LicenseValidationResponseDTO(BaseModel): """DTO para respuesta de validación de licencia""" + is_valid: bool status: LicenseStatusDTO plan: LicensePlanDTO expires_at: datetime reason: Optional[str] = None - + class Config: json_schema_extra = { "example": { @@ -114,13 +125,14 @@ class LicenseValidationResponseDTO(BaseModel): "status": "active", "plan": "professional", "expires_at": "2025-12-31T23:59:59Z", - "reason": None + "reason": None, } } class LicenseUsageResponseDTO(BaseModel): """DTO para respuesta de uso de licencia""" + tenant_id: int period_start: datetime period_end: datetime @@ -128,16 +140,16 @@ class LicenseUsageResponseDTO(BaseModel): storage_used_gb: int operations_count: int api_calls_count: int - + # Límites actuales max_users: int max_storage_gb: int max_monthly_operations: int - + # Porcentajes de uso users_usage_percent: float storage_usage_percent: float operations_usage_percent: float - + class Config: from_attributes = True diff --git a/backend/api/v1/modules/a76/licenses/models.py b/backend/api/v1/modules/a76/licenses/models.py index d1a6cad4..39c8f189 100644 --- a/backend/api/v1/modules/a76/licenses/models.py +++ b/backend/api/v1/modules/a76/licenses/models.py @@ -1,8 +1,17 @@ """ Modelos ORM para gestión de licencias """ + from datetime import datetime -from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey, Enum as SQLEnum +from sqlalchemy import ( + Column, + Integer, + String, + DateTime, + Boolean, + ForeignKey, + Enum as SQLEnum, +) from sqlalchemy.sql import func from sqlalchemy.orm import relationship from sqlalchemy.orm import Mapped, mapped_column @@ -12,6 +21,7 @@ import enum class LicensePlan(enum.Enum): """Planes de licencia disponibles""" + FREE = "free" BASIC = "basic" PROFESSIONAL = "professional" @@ -20,6 +30,7 @@ class LicensePlan(enum.Enum): class LicenseStatus(enum.Enum): """Estados de licencia""" + ACTIVE = "active" EXPIRED = "expired" SUSPENDED = "suspended" @@ -31,36 +42,45 @@ class License(Base): """ Modelo de Licencia - Control de planes y límites por tenant """ + __tablename__ = "licenses" __table_args__ = {"schema": "a76"} - + id = Column(Integer, primary_key=True, index=True) - tenant_id = Column(Integer, ForeignKey("a76.tenants.id"), nullable=False, unique=True, index=True) - + tenant_id = Column( + Integer, ForeignKey("a76.tenants.id"), nullable=False, unique=True, index=True + ) + # Plan y características plan = Column(SQLEnum(LicensePlan), default=LicensePlan.FREE, nullable=False) - status = Column(SQLEnum(LicenseStatus), default=LicenseStatus.PENDING, nullable=False) - + status = Column( + SQLEnum(LicenseStatus), default=LicenseStatus.PENDING, nullable=False + ) + # Límites del plan max_users = Column(Integer, default=5, nullable=False) max_storage_gb = Column(Integer, default=10, nullable=False) max_monthly_operations = Column(Integer, default=1000, nullable=False) - + # Features habilitadas (booleans) feature_api_access = Column(Boolean, default=True) feature_advanced_reports = Column(Boolean, default=False) feature_integrations = Column(Boolean, default=False) feature_dedicated_support = Column(Boolean, default=False) - + # Vigencia starts_at = Column(DateTime(timezone=True), nullable=False) expires_at = Column(DateTime(timezone=True), nullable=False) - + # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) - updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now(), onupdate=func.now() + ) deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - + def __repr__(self): return f"" @@ -69,25 +89,32 @@ class LicenseUsage(Base): """ Modelo para tracking de uso de licencia """ + __tablename__ = "license_usage" __table_args__ = {"schema": "a76"} - + id = Column(Integer, primary_key=True, index=True) - tenant_id = Column(Integer, ForeignKey("a76.tenants.id"), nullable=False, index=True) - + tenant_id = Column( + Integer, ForeignKey("a76.tenants.id"), nullable=False, index=True + ) + # Métricas de uso period_start = Column(DateTime(timezone=True), nullable=False) period_end = Column(DateTime(timezone=True), nullable=False) - + active_users = Column(Integer, default=0) storage_used_gb = Column(Integer, default=0) operations_count = Column(Integer, default=0) api_calls_count = Column(Integer, default=0) - + # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) - updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now(), onupdate=func.now() + ) deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - + def __repr__(self): return f"" diff --git a/backend/api/v1/modules/a76/licenses/routes.py b/backend/api/v1/modules/a76/licenses/routes.py index 730cbade..b66173cc 100644 --- a/backend/api/v1/modules/a76/licenses/routes.py +++ b/backend/api/v1/modules/a76/licenses/routes.py @@ -1,6 +1,7 @@ """ Endpoints API para gestión de licencias """ + from fastapi import APIRouter, Depends, HTTPException, Request from sqlalchemy.orm import Session @@ -11,7 +12,7 @@ from .dto import ( LicenseUpdateDTO, LicenseResponseDTO, LicenseValidationResponseDTO, - LicenseUsageResponseDTO + LicenseUsageResponseDTO, ) from .service import LicenseService @@ -22,11 +23,11 @@ router = APIRouter(prefix="/licenses") async def create_license( license_data: LicenseCreateDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): """ Crea una nueva licencia para un tenant - + Requiere rol: admin """ service = LicenseService(db) @@ -37,7 +38,7 @@ async def create_license( async def get_license_by_tenant( tenant_id: int, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Obtiene la licencia de un tenant específico @@ -54,11 +55,11 @@ async def update_license( tenant_id: int, license_data: LicenseUpdateDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): """ Actualiza la licencia de un tenant - + Requiere rol: admin """ service = LicenseService(db) @@ -72,7 +73,7 @@ async def update_license( async def validate_license( tenant_id: int, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Valida si la licencia de un tenant está activa y vigente @@ -86,7 +87,7 @@ async def validate_license( async def get_license_usage( tenant_id: int, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Obtiene el uso actual de la licencia de un tenant @@ -102,7 +103,7 @@ async def get_license_usage( async def get_my_license( request: Request, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Obtiene la licencia del tenant del usuario actual @@ -110,7 +111,7 @@ async def get_my_license( tenant_id = getattr(request.state, "tenant_id", None) if not tenant_id: raise HTTPException(status_code=400, detail="Tenant ID not found in request") - + service = LicenseService(db) license = service.get_license_by_tenant(tenant_id) if not license: diff --git a/backend/api/v1/modules/a76/licenses/service.py b/backend/api/v1/modules/a76/licenses/service.py index ef81ded7..8e0a98c9 100644 --- a/backend/api/v1/modules/a76/licenses/service.py +++ b/backend/api/v1/modules/a76/licenses/service.py @@ -1,6 +1,7 @@ """ Servicio de lógica de negocio para licencias """ + from sqlalchemy.orm import Session from sqlalchemy.exc import IntegrityError from fastapi import HTTPException @@ -14,7 +15,7 @@ from .dto import ( LicenseUpdateDTO, LicenseResponseDTO, LicenseValidationResponseDTO, - LicenseUsageResponseDTO + LicenseUsageResponseDTO, ) logger = logging.getLogger(__name__) @@ -22,35 +23,37 @@ logger = logging.getLogger(__name__) class LicenseService: """Servicio para gestión de licencias""" - + def __init__(self, db: Session): self.db = db - + def create_license(self, license_data: LicenseCreateDTO) -> LicenseResponseDTO: """ Crea una nueva licencia para un tenant - + Args: license_data: Datos de la licencia - + Returns: LicenseResponseDTO - + Raises: HTTPException: Si el tenant ya tiene licencia o hay error """ try: # Verificar que el tenant no tenga ya una licencia - existing = self.db.query(License).filter( - License.tenant_id == license_data.tenant_id - ).first() - + existing = ( + self.db.query(License) + .filter(License.tenant_id == license_data.tenant_id) + .first() + ) + if existing: raise HTTPException( status_code=400, - detail=f"Tenant {license_data.tenant_id} already has a license" + detail=f"Tenant {license_data.tenant_id} already has a license", ) - + # Crear licencia db_license = License( tenant_id=license_data.tenant_id, @@ -64,17 +67,17 @@ class LicenseService: feature_integrations=license_data.feature_integrations, feature_dedicated_support=license_data.feature_dedicated_support, starts_at=license_data.starts_at, - expires_at=license_data.expires_at + expires_at=license_data.expires_at, ) - + self.db.add(db_license) self.db.commit() self.db.refresh(db_license) - + logger.info(f"License created for tenant {license_data.tenant_id}") - + return LicenseResponseDTO.model_validate(db_license) - + except IntegrityError as e: self.db.rollback() logger.error(f"IntegrityError creating license: {str(e)}") @@ -85,14 +88,14 @@ class LicenseService: self.db.rollback() logger.error(f"Error creating license: {str(e)}") raise HTTPException(status_code=500, detail="Error creating license") - + def get_license_by_tenant(self, tenant_id: int) -> Optional[LicenseResponseDTO]: """ Obtiene la licencia de un tenant - + Args: tenant_id: ID del tenant - + Returns: LicenseResponseDTO o None si no existe """ @@ -100,22 +103,24 @@ class LicenseService: if not license: return None return LicenseResponseDTO.model_validate(license) - - def update_license(self, tenant_id: int, license_data: LicenseUpdateDTO) -> Optional[LicenseResponseDTO]: + + def update_license( + self, tenant_id: int, license_data: LicenseUpdateDTO + ) -> Optional[LicenseResponseDTO]: """ Actualiza una licencia - + Args: tenant_id: ID del tenant license_data: Datos a actualizar - + Returns: LicenseResponseDTO actualizado o None si no existe """ license = self.db.query(License).filter(License.tenant_id == tenant_id).first() if not license: return None - + # Actualizar campos proporcionados update_data = license_data.model_dump(exclude_unset=True) for field, value in update_data.items(): @@ -123,7 +128,7 @@ class LicenseService: # Convertir enums value = LicensePlan(value) if field == "plan" else LicenseStatus(value) setattr(license, field, value) - + try: self.db.commit() self.db.refresh(license) @@ -133,30 +138,30 @@ class LicenseService: self.db.rollback() logger.error(f"Error updating license for tenant {tenant_id}: {str(e)}") raise HTTPException(status_code=500, detail="Error updating license") - + def validate_license(self, tenant_id: int) -> dict: """ Valida si la licencia de un tenant está activa y vigente - + Args: tenant_id: ID del tenant - + Returns: Dict con información de validación """ license = self.db.query(License).filter(License.tenant_id == tenant_id).first() - + if not license: return { "is_valid": False, "status": "not_found", "plan": None, "expires_at": None, - "reason": "License not found" + "reason": "License not found", } - + now = datetime.now(timezone.utc) - + # Verificar estado if license.status != LicenseStatus.ACTIVE: return { @@ -164,51 +169,54 @@ class LicenseService: "status": license.status.value, "plan": license.plan.value, "expires_at": license.expires_at, - "reason": f"License status is {license.status.value}" + "reason": f"License status is {license.status.value}", } - + # Verificar vigencia if license.expires_at < now: # Auto-actualizar a expirada license.status = LicenseStatus.EXPIRED self.db.commit() - + return { "is_valid": False, "status": "expired", "plan": license.plan.value, "expires_at": license.expires_at, - "reason": "License has expired" + "reason": "License has expired", } - + # Licencia válida return { "is_valid": True, "status": license.status.value, "plan": license.plan.value, "expires_at": license.expires_at, - "reason": None + "reason": None, } - + def get_usage(self, tenant_id: int) -> Optional[LicenseUsageResponseDTO]: """ Obtiene el uso actual de la licencia de un tenant - + Args: tenant_id: ID del tenant - + Returns: LicenseUsageResponseDTO o None """ license = self.db.query(License).filter(License.tenant_id == tenant_id).first() if not license: return None - + # Obtener último registro de uso - usage = self.db.query(LicenseUsage).filter( - LicenseUsage.tenant_id == tenant_id - ).order_by(LicenseUsage.created_at.desc()).first() - + usage = ( + self.db.query(LicenseUsage) + .filter(LicenseUsage.tenant_id == tenant_id) + .order_by(LicenseUsage.created_at.desc()) + .first() + ) + if not usage: # Crear registro inicial si no existe usage = LicenseUsage( @@ -218,14 +226,26 @@ class LicenseService: active_users=0, storage_used_gb=0, operations_count=0, - api_calls_count=0 + api_calls_count=0, ) - + # Calcular porcentajes - users_usage = (usage.active_users / license.max_users * 100) if license.max_users > 0 else 0 - storage_usage = (usage.storage_used_gb / license.max_storage_gb * 100) if license.max_storage_gb > 0 else 0 - operations_usage = (usage.operations_count / license.max_monthly_operations * 100) if license.max_monthly_operations > 0 else 0 - + users_usage = ( + (usage.active_users / license.max_users * 100) + if license.max_users > 0 + else 0 + ) + storage_usage = ( + (usage.storage_used_gb / license.max_storage_gb * 100) + if license.max_storage_gb > 0 + else 0 + ) + operations_usage = ( + (usage.operations_count / license.max_monthly_operations * 100) + if license.max_monthly_operations > 0 + else 0 + ) + return LicenseUsageResponseDTO( tenant_id=tenant_id, period_start=usage.period_start, @@ -239,5 +259,5 @@ class LicenseService: max_monthly_operations=license.max_monthly_operations, users_usage_percent=round(users_usage, 2), storage_usage_percent=round(storage_usage, 2), - operations_usage_percent=round(operations_usage, 2) + operations_usage_percent=round(operations_usage, 2), ) diff --git a/backend/api/v1/modules/a76/package/dto.py b/backend/api/v1/modules/a76/package/dto.py index ba749ccb..e05d91c3 100644 --- a/backend/api/v1/modules/a76/package/dto.py +++ b/backend/api/v1/modules/a76/package/dto.py @@ -5,6 +5,7 @@ DTOs for GBultos. from pydantic import BaseModel from typing import Optional + class GBultoBaseDTO(BaseModel): CODE: str DESCRIPTION: Optional[str] @@ -15,9 +16,11 @@ class GBultoBaseDTO(BaseModel): CODE_ACE: Optional[str] CODE_AAMEX: Optional[str] + class GBultoCreateDTO(GBultoBaseDTO): pass + class GBultoUpdateDTO(BaseModel): DESCRIPTION: Optional[str] DESCRIPTIONI: Optional[str] @@ -27,9 +30,10 @@ class GBultoUpdateDTO(BaseModel): CODE_ACE: Optional[str] CODE_AAMEX: Optional[str] + class GBultoResponseDTO(GBultoBaseDTO): CREATED_AT: Optional[str] UPDATED_AT: Optional[str] class Config: - from_attributes = True \ No newline at end of file + from_attributes = True diff --git a/backend/api/v1/modules/a76/package/models.py b/backend/api/v1/modules/a76/package/models.py index 89326dbc..f24cf07d 100644 --- a/backend/api/v1/modules/a76/package/models.py +++ b/backend/api/v1/modules/a76/package/models.py @@ -1,7 +1,16 @@ from typing import Optional from datetime import datetime from decimal import Decimal -from sqlalchemy import DateTime, Integer, String, DECIMAL, ForeignKey, PrimaryKeyConstraint, ForeignKeyConstraint, UniqueConstraint +from sqlalchemy import ( + DateTime, + Integer, + String, + DECIMAL, + ForeignKey, + PrimaryKeyConstraint, + ForeignKeyConstraint, + UniqueConstraint, +) from sqlalchemy.sql import func from sqlalchemy.orm import Mapped, mapped_column from core.database import Base @@ -10,17 +19,21 @@ from core.database import Base class Package(Base): __tablename__ = "packages" # GBultos __table_args__ = ( - PrimaryKeyConstraint('id', name='packages_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_packages_tenant'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_packages_company'), - UniqueConstraint('tenant_id', 'company_id', 'key', name='packages_key_ukey'), - {"schema": "a76"} + PrimaryKeyConstraint("id", name="packages_pkey"), + ForeignKeyConstraint( + ["tenant_id"], ["a76.tenants.id"], name="fk_packages_tenant" + ), + ForeignKeyConstraint( + ["company_id"], ["a76.company.id"], name="fk_packages_company" + ), + UniqueConstraint("tenant_id", "company_id", "key", name="packages_key_ukey"), + {"schema": "a76"}, ) id: Mapped[int] = mapped_column(Integer, primary_key=True) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - + key: Mapped[str] = mapped_column(String(5)) description_es: Mapped[Optional[str]] = mapped_column(String(40)) description_en: Mapped[Optional[str]] = mapped_column(String(40)) @@ -29,10 +42,12 @@ class Package(Base): plural_in: Mapped[Optional[str]] = mapped_column(String(4)) code_ace: Mapped[Optional[str]] = mapped_column(String(4)) code_aamex: Mapped[Optional[str]] = mapped_column(String(9)) - - # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) - updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) - deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - + # Timestamps + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now(), onupdate=func.now() + ) + deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) diff --git a/backend/api/v1/modules/a76/package/routes.py b/backend/api/v1/modules/a76/package/routes.py index de08247e..24a9c850 100644 --- a/backend/api/v1/modules/a76/package/routes.py +++ b/backend/api/v1/modules/a76/package/routes.py @@ -16,7 +16,7 @@ async def list_bultos( skip: int = 0, limit: int = 100, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ List all GBultos with pagination. @@ -35,7 +35,7 @@ async def list_bultos( async def read_bulto( code: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Get a specific Package by its CODE. @@ -57,7 +57,7 @@ async def read_bulto( async def create_gbulto( bulto_data: GBultoCreateDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Create a new Package. @@ -77,7 +77,7 @@ async def update_bulto( code: str, bulto_data: GBultoUpdateDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Update an existing Package. @@ -99,7 +99,7 @@ async def update_bulto( async def delete_bulto( code: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Delete a Package by its CODE. @@ -113,4 +113,4 @@ async def delete_bulto( bulto = GBultoService.delete_bulto(db, code) if not bulto: - raise HTTPException(status_code=404, detail="Package not found") \ No newline at end of file + raise HTTPException(status_code=404, detail="Package not found") diff --git a/backend/api/v1/modules/a76/package/services.py b/backend/api/v1/modules/a76/package/services.py index 1e22e92c..42559633 100644 --- a/backend/api/v1/modules/a76/package/services.py +++ b/backend/api/v1/modules/a76/package/services.py @@ -1,6 +1,7 @@ from sqlalchemy.orm import Session from . import models, dto + class GBultoService: """ Service layer for GBultos. @@ -34,4 +35,4 @@ class GBultoService: if bulto: db.delete(bulto) db.commit() - return bulto \ No newline at end of file + return bulto diff --git a/backend/api/v1/modules/a76/parts/__init__.py b/backend/api/v1/modules/a76/parts/__init__.py index 11584490..33bfb6c2 100644 --- a/backend/api/v1/modules/a76/parts/__init__.py +++ b/backend/api/v1/modules/a76/parts/__init__.py @@ -1,6 +1,7 @@ """ Módulo de GParts """ + from .routes import router __all__ = ["router"] diff --git a/backend/api/v1/modules/a76/parts/dto.py b/backend/api/v1/modules/a76/parts/dto.py index 00c703af..79156199 100644 --- a/backend/api/v1/modules/a76/parts/dto.py +++ b/backend/api/v1/modules/a76/parts/dto.py @@ -2,6 +2,7 @@ DTOs (Data Transfer Objects) para módulo de partes/componentes Reemplaza schemas.py siguiendo enfoque DDD y estilo NestJS """ + from pydantic import BaseModel, Field from typing import Optional from datetime import datetime @@ -10,43 +11,66 @@ from decimal import Decimal class PartCreateDTO(BaseModel): """DTO para crear una parte""" + client_id: int = Field(..., description="Client key") part_number: str = Field(..., max_length=49, description="Part number") fraction: Optional[str] = Field(None, max_length=10, description="Tariff fraction") - description_spanish: Optional[str] = Field(None, max_length=500, description="Description in Spanish") - description_english: Optional[str] = Field(None, max_length=500, description="Description in English") + description_spanish: Optional[str] = Field( + None, max_length=500, description="Description in Spanish" + ) + description_english: Optional[str] = Field( + None, max_length=500, description="Description in English" + ) part_class: Optional[str] = Field(None, max_length=8, description="Part class") - unit_of_measure: Optional[str] = Field(None, max_length=5, description="Unit of measure") - commercial_part_number: Optional[str] = Field(None, max_length=70, description="Commercial part number") - country_of_origin: Optional[str] = Field(None, max_length=3, description="Country of origin code") - + unit_of_measure: Optional[str] = Field( + None, max_length=5, description="Unit of measure" + ) + commercial_part_number: Optional[str] = Field( + None, max_length=70, description="Commercial part number" + ) + country_of_origin: Optional[str] = Field( + None, max_length=3, description="Country of origin code" + ) + # Pricing and currency unit_cost: Optional[Decimal] = Field(None, description="Unit cost") - currency_type: Optional[str] = Field(None, max_length=2, description="Currency type") + currency_type: Optional[str] = Field( + None, max_length=2, description="Currency type" + ) currency_key: Optional[str] = Field(None, max_length=3, description="Currency key") - + # Weight information unit_weight: Optional[Decimal] = Field(None, description="Unit weight") weight_type: Optional[str] = Field(None, max_length=6, description="Weight type") - + # Classification and regulatory - us_fraction: Optional[str] = Field(None, max_length=16, description="US tariff fraction") + us_fraction: Optional[str] = Field( + None, max_length=16, description="US tariff fraction" + ) fda_key: Optional[str] = Field(None, max_length=20, description="FDA key") fcc_key: Optional[str] = Field(None, max_length=30, description="FCC key") license_code: Optional[str] = Field(None, max_length=3, description="License code") - eccn: Optional[str] = Field(None, max_length=20, description="Export Control Classification Number") + eccn: Optional[str] = Field( + None, max_length=20, description="Export Control Classification Number" + ) export_code: Optional[str] = Field(None, max_length=2, description="Export code") - exclusion_symbol: Optional[str] = Field(None, max_length=19, description="Exclusion symbol") - + exclusion_symbol: Optional[str] = Field( + None, max_length=19, description="Exclusion symbol" + ) + # Additional information supplier: Optional[str] = Field(None, max_length=14, description="Supplier") - alternate_unit_measure: Optional[str] = Field(None, max_length=14, description="Alternate unit of measure") + alternate_unit_measure: Optional[str] = Field( + None, max_length=14, description="Alternate unit of measure" + ) added_value: Optional[Decimal] = Field(None, description="Added value") - + # Status and media enabled_disabled: Optional[int] = Field(None, description="Enabled/Disabled status") creation_date: Optional[int] = Field(None, description="Creation date") - part_photo: Optional[str] = Field(None, max_length=255, description="Part photo URL") + part_photo: Optional[str] = Field( + None, max_length=255, description="Part photo URL" + ) class Config: from_attributes = True @@ -54,40 +78,63 @@ class PartCreateDTO(BaseModel): class PartUpdateDTO(BaseModel): """DTO para actualizar una parte""" + fraction: Optional[str] = Field(None, max_length=10, description="Tariff fraction") - description_spanish: Optional[str] = Field(None, max_length=500, description="Description in Spanish") - description_english: Optional[str] = Field(None, max_length=500, description="Description in English") + description_spanish: Optional[str] = Field( + None, max_length=500, description="Description in Spanish" + ) + description_english: Optional[str] = Field( + None, max_length=500, description="Description in English" + ) part_class: Optional[str] = Field(None, max_length=8, description="Part class") - unit_of_measure: Optional[str] = Field(None, max_length=5, description="Unit of measure") - commercial_part_number: Optional[str] = Field(None, max_length=70, description="Commercial part number") - country_of_origin: Optional[str] = Field(None, max_length=3, description="Country of origin code") - + unit_of_measure: Optional[str] = Field( + None, max_length=5, description="Unit of measure" + ) + commercial_part_number: Optional[str] = Field( + None, max_length=70, description="Commercial part number" + ) + country_of_origin: Optional[str] = Field( + None, max_length=3, description="Country of origin code" + ) + # Pricing and currency unit_cost: Optional[Decimal] = Field(None, description="Unit cost") - currency_type: Optional[str] = Field(None, max_length=2, description="Currency type") + currency_type: Optional[str] = Field( + None, max_length=2, description="Currency type" + ) currency_key: Optional[str] = Field(None, max_length=3, description="Currency key") - + # Weight information unit_weight: Optional[Decimal] = Field(None, description="Unit weight") weight_type: Optional[str] = Field(None, max_length=6, description="Weight type") - + # Classification and regulatory - us_fraction: Optional[str] = Field(None, max_length=16, description="US tariff fraction") + us_fraction: Optional[str] = Field( + None, max_length=16, description="US tariff fraction" + ) fda_key: Optional[str] = Field(None, max_length=20, description="FDA key") fcc_key: Optional[str] = Field(None, max_length=30, description="FCC key") license_code: Optional[str] = Field(None, max_length=3, description="License code") - eccn: Optional[str] = Field(None, max_length=20, description="Export Control Classification Number") + eccn: Optional[str] = Field( + None, max_length=20, description="Export Control Classification Number" + ) export_code: Optional[str] = Field(None, max_length=2, description="Export code") - exclusion_symbol: Optional[str] = Field(None, max_length=19, description="Exclusion symbol") - + exclusion_symbol: Optional[str] = Field( + None, max_length=19, description="Exclusion symbol" + ) + # Additional information supplier: Optional[str] = Field(None, max_length=14, description="Supplier") - alternate_unit_measure: Optional[str] = Field(None, max_length=14, description="Alternate unit of measure") + alternate_unit_measure: Optional[str] = Field( + None, max_length=14, description="Alternate unit of measure" + ) added_value: Optional[Decimal] = Field(None, description="Added value") - + # Status and media enabled_disabled: Optional[int] = Field(None, description="Enabled/Disabled status") - part_photo: Optional[str] = Field(None, max_length=255, description="Part photo URL") + part_photo: Optional[str] = Field( + None, max_length=255, description="Part photo URL" + ) class Config: from_attributes = True @@ -95,6 +142,7 @@ class PartUpdateDTO(BaseModel): class PartResponseDTO(BaseModel): """DTO para respuesta de parte""" + client_id: int part_number: str fraction: Optional[str] = None @@ -104,16 +152,16 @@ class PartResponseDTO(BaseModel): unit_of_measure: Optional[str] = None commercial_part_number: Optional[str] = None country_of_origin: Optional[str] = None - + # Pricing and currency unit_cost: Optional[Decimal] = None currency_type: Optional[str] = None currency_key: Optional[str] = None - + # Weight information unit_weight: Optional[Decimal] = None weight_type: Optional[str] = None - + # Classification and regulatory us_fraction: Optional[str] = None fda_key: Optional[str] = None @@ -122,18 +170,18 @@ class PartResponseDTO(BaseModel): eccn: Optional[str] = None export_code: Optional[str] = None exclusion_symbol: Optional[str] = None - + # Additional information supplier: Optional[str] = None alternate_unit_measure: Optional[str] = None added_value: Optional[Decimal] = None - + # Status and dates enabled_disabled: Optional[int] = None creation_date: Optional[int] = None modification_date: Optional[int] = None modification_date_iso: Optional[datetime] = None - + # Media part_photo: Optional[str] = None @@ -143,6 +191,7 @@ class PartResponseDTO(BaseModel): class PartBasicDTO(BaseModel): """DTO para información básica de parte""" + client_id: int part_number: str description_spanish: Optional[str] = None @@ -158,6 +207,7 @@ class PartBasicDTO(BaseModel): class PartListDTO(BaseModel): """DTO para lista de partes""" + parts: list[PartBasicDTO] total: int page: int @@ -169,6 +219,7 @@ class PartListDTO(BaseModel): class PartSearchDTO(BaseModel): """DTO para búsqueda de partes""" + client_id: Optional[int] = Field(None, description="Filter by client key") part_number: Optional[str] = Field(None, description="Search by part number") description: Optional[str] = Field(None, description="Search in descriptions") @@ -178,5 +229,3 @@ class PartSearchDTO(BaseModel): class Config: from_attributes = True - - diff --git a/backend/api/v1/modules/a76/parts/models.py b/backend/api/v1/modules/a76/parts/models.py index 84c220ba..ea824e61 100644 --- a/backend/api/v1/modules/a76/parts/models.py +++ b/backend/api/v1/modules/a76/parts/models.py @@ -1,10 +1,20 @@ """ Modelos ORM para gestión de partes/componentes """ + from typing import TYPE_CHECKING, Optional from datetime import datetime from decimal import Decimal -from sqlalchemy import Integer, String, Numeric, SmallInteger, ForeignKey, PrimaryKeyConstraint, ForeignKeyConstraint, UniqueConstraint +from sqlalchemy import ( + Integer, + String, + Numeric, + SmallInteger, + ForeignKey, + PrimaryKeyConstraint, + ForeignKeyConstraint, + UniqueConstraint, +) from sqlalchemy.sql import func from sqlalchemy.orm import Mapped, mapped_column, relationship from core.database import Base @@ -19,25 +29,34 @@ class Part(Base): """ Modelo para la tabla GPartes - Información de partes en los sistemas SCAII (N), SCAF (S) Y WINSAAI (W) """ + __tablename__ = "parts" __table_args__ = ( - PrimaryKeyConstraint('id', name='parts_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_parts_tenant'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_parts_company'), - ForeignKeyConstraint(['country_of_origin'], ['public.countries.m3_key'], name='fk_parts_country'), - ForeignKeyConstraint(['currency_key'], ['public.currency_types.code'], name='fk_parts_currency'), - UniqueConstraint('tenant_id', 'company_id', 'part_number', name='client_part_ukey'), - {"schema": "a76"} + PrimaryKeyConstraint("id", name="parts_pkey"), + ForeignKeyConstraint(["tenant_id"], ["a76.tenants.id"], name="fk_parts_tenant"), + ForeignKeyConstraint( + ["company_id"], ["a76.company.id"], name="fk_parts_company" + ), + ForeignKeyConstraint( + ["country_of_origin"], ["public.countries.m3_key"], name="fk_parts_country" + ), + ForeignKeyConstraint( + ["currency_key"], ["public.currency_types.code"], name="fk_parts_currency" + ), + UniqueConstraint( + "tenant_id", "company_id", "part_number", name="client_part_ukey" + ), + {"schema": "a76"}, ) - + id: Mapped[int] = mapped_column(Integer, primary_key=True) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - + # Unique constraint compuesta client_id: Mapped[int] = mapped_column(Integer) part_number: Mapped[str] = mapped_column(String(49)) - + # Basic information fraction: Mapped[Optional[str]] = mapped_column(String(10)) description_spanish: Mapped[Optional[str]] = mapped_column(String(500)) @@ -46,53 +65,59 @@ class Part(Base): unit_of_measure: Mapped[Optional[str]] = mapped_column(String(5)) commercial_part_number: Mapped[Optional[str]] = mapped_column(String(70)) country_of_origin: Mapped[Optional[str]] = mapped_column(String(3)) - + # Pricing and currency unit_cost: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) currency_type: Mapped[Optional[str]] = mapped_column(String(2)) currency_key: Mapped[Optional[str]] = mapped_column(String(3)) - + # Weight information unit_weight: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 8)) weight_type: Mapped[Optional[str]] = mapped_column(String(6)) - + # Classification and regulatory us_fraction: Mapped[Optional[str]] = mapped_column(String(16)) # FRACCIONAME fda_key: Mapped[Optional[str]] = mapped_column(String(20)) fcc_key: Mapped[Optional[str]] = mapped_column(String(30)) license_code: Mapped[Optional[str]] = mapped_column(String(3)) - eccn: Mapped[Optional[str]] = mapped_column(String(20)) # Export Control Classification Number + eccn: Mapped[Optional[str]] = mapped_column( + String(20) + ) # Export Control Classification Number export_code: Mapped[Optional[str]] = mapped_column(String(2)) exclusion_symbol: Mapped[Optional[str]] = mapped_column(String(19)) # SIMBOLOEXCLIC - + # Additional information supplier: Mapped[Optional[str]] = mapped_column(String(14)) alternate_unit_measure: Mapped[Optional[str]] = mapped_column(String(14)) added_value: Mapped[Optional[Decimal]] = mapped_column(Numeric(23, 8)) - + # Status and dates enabled_disabled: Mapped[Optional[int]] = mapped_column(SmallInteger) creation_date: Mapped[Optional[int]] = mapped_column() # FECHACREACIONPARTE modification_date: Mapped[Optional[int]] = mapped_column() # FECHAMODIFICA - modification_date_iso: Mapped[Optional[datetime]] = mapped_column() # FECHAMODIFICA_ISO - + modification_date_iso: Mapped[Optional[datetime]] = ( + mapped_column() + ) # FECHAMODIFICA_ISO + # Media - part_photo: Mapped[Optional[str]] = mapped_column(String(255)) - + part_photo: Mapped[Optional[str]] = mapped_column(String(255)) + # Relationships - country: Mapped[Optional["Country"]] = relationship(foreign_keys=[country_of_origin]) - currency: Mapped[Optional["CurrencyType"]] = relationship(foreign_keys=[currency_key]) - + country: Mapped[Optional["Country"]] = relationship( + foreign_keys=[country_of_origin] + ) + currency: Mapped[Optional["CurrencyType"]] = relationship( + foreign_keys=[currency_key] + ) + # Relationship with Class through composite foreign key # Note: This requires both client_id and part_class to match client_id and class_code in Class part_class_info: Mapped[Optional["Class"]] = relationship( primaryjoin="and_(Part.client_id == Class.client_id, Part.part_class == Class.class_code)", foreign_keys="[Part.client_id, Part.part_class]", viewonly=True, - back_populates="parts" + back_populates="parts", ) def __repr__(self) -> str: return f"" - - diff --git a/backend/api/v1/modules/a76/parts/routes.py b/backend/api/v1/modules/a76/parts/routes.py index 7aab6c88..e7ab6fe8 100644 --- a/backend/api/v1/modules/a76/parts/routes.py +++ b/backend/api/v1/modules/a76/parts/routes.py @@ -1,6 +1,7 @@ """ Endpoints API para gestión de partes/componentes """ + from fastapi import APIRouter, Depends, HTTPException, Query, status from sqlalchemy.orm import Session from typing import List, Optional @@ -9,12 +10,12 @@ from core.database import get_core_db from core.security import get_current_user, has_role from .service import PartService from .dto import ( - PartCreateDTO, - PartUpdateDTO, + PartCreateDTO, + PartUpdateDTO, PartResponseDTO, PartBasicDTO, PartListDTO, - PartSearchDTO + PartSearchDTO, ) router = APIRouter(prefix="/parts") @@ -24,7 +25,7 @@ router = APIRouter(prefix="/parts") async def create_part( part_data: PartCreateDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Create a new part in the system @@ -43,7 +44,9 @@ async def create_part( @router.get("/", response_model=PartListDTO) async def list_parts( skip: int = Query(0, ge=0, description="Number of records to skip"), - limit: int = Query(100, ge=1, le=1000, description="Maximum number of records to return"), + limit: int = Query( + 100, ge=1, le=1000, description="Maximum number of records to return" + ), client_id: Optional[int] = Query(None, description="Filter by client key"), part_number: Optional[str] = Query(None, description="Search by part number"), description: Optional[str] = Query(None, description="Search in descriptions"), @@ -51,7 +54,7 @@ async def list_parts( supplier: Optional[str] = Query(None, description="Filter by supplier"), enabled_only: bool = Query(False, description="Show only enabled parts"), db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ List parts with optional filters and pagination @@ -70,7 +73,7 @@ async def list_parts( description=description, fraction=fraction, supplier=supplier, - enabled_only=enabled_only + enabled_only=enabled_only, ) return service.list_parts(skip, limit, search_params) @@ -81,7 +84,7 @@ async def get_parts_by_client( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Get all parts for a specific client @@ -101,7 +104,7 @@ async def get_parts_by_client( async def search_by_fraction( fraction: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Search parts by tariff fraction @@ -121,7 +124,7 @@ async def search_by_fraction( async def search_by_supplier( supplier: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Search parts by supplier @@ -141,7 +144,7 @@ async def search_by_supplier( async def get_parts_by_country( country_code: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Get parts by country of origin @@ -159,8 +162,7 @@ async def get_parts_by_country( @router.get("/statistics", response_model=dict) async def get_parts_statistics( - db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) ): """ Get basic parts statistics @@ -181,7 +183,7 @@ async def get_part( client_id: int, part_number: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Get part by composite key (client_id + part_number) @@ -197,8 +199,8 @@ async def get_part( part = service.get_part(client_id, part_number) if not part: raise HTTPException( - status_code=404, - detail=f"Part with client_id '{client_id}' and part_number '{part_number}' not found" + status_code=404, + detail=f"Part with client_id '{client_id}' and part_number '{part_number}' not found", ) return part @@ -209,7 +211,7 @@ async def update_part( part_number: str, part_data: PartUpdateDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Update part information @@ -225,8 +227,8 @@ async def update_part( part = service.update_part(client_id, part_number, part_data) if not part: raise HTTPException( - status_code=404, - detail=f"Part with client_id '{client_id}' and part_number '{part_number}' not found" + status_code=404, + detail=f"Part with client_id '{client_id}' and part_number '{part_number}' not found", ) return part @@ -236,11 +238,11 @@ async def delete_part( client_id: int, part_number: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Delete part from the system - + Note: This will completely remove the part from the system. """ # Validate access to the tenant and company @@ -253,17 +255,19 @@ async def delete_part( service = PartService(db) if not service.delete_part(client_id, part_number): raise HTTPException( - status_code=404, - detail=f"Part with client_id '{client_id}' and part_number '{part_number}' not found" + status_code=404, + detail=f"Part with client_id '{client_id}' and part_number '{part_number}' not found", ) -@router.patch("/{client_id}/{part_number}/toggle-status", response_model=PartResponseDTO) +@router.patch( + "/{client_id}/{part_number}/toggle-status", response_model=PartResponseDTO +) async def toggle_part_status( client_id: int, part_number: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Toggle part enabled/disabled status @@ -279,8 +283,8 @@ async def toggle_part_status( part = service.toggle_status(client_id, part_number) if not part: raise HTTPException( - status_code=404, - detail=f"Part with client_id '{client_id}' and part_number '{part_number}' not found" + status_code=404, + detail=f"Part with client_id '{client_id}' and part_number '{part_number}' not found", ) return part @@ -291,7 +295,7 @@ async def get_part_basic_info( client_id: int, part_number: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Get basic information for a part @@ -307,10 +311,10 @@ async def get_part_basic_info( part = service.get_part(client_id, part_number) if not part: raise HTTPException( - status_code=404, - detail=f"Part with client_id '{client_id}' and part_number '{part_number}' not found" + status_code=404, + detail=f"Part with client_id '{client_id}' and part_number '{part_number}' not found", ) - + return PartBasicDTO( client_id=part.client_id, part_number=part.part_number, @@ -319,7 +323,7 @@ async def get_part_basic_info( part_class=part.part_class, unit_cost=part.unit_cost, currency_key=part.currency_key, - enabled_disabled=part.enabled_disabled + enabled_disabled=part.enabled_disabled, ) @@ -328,7 +332,7 @@ async def get_part_regulatory_info( client_id: int, part_number: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Get regulatory information for a part (FDA, FCC, ECCN, etc.) @@ -344,10 +348,10 @@ async def get_part_regulatory_info( part = service.get_part(client_id, part_number) if not part: raise HTTPException( - status_code=404, - detail=f"Part with client_id '{client_id}' and part_number '{part_number}' not found" + status_code=404, + detail=f"Part with client_id '{client_id}' and part_number '{part_number}' not found", ) - + return { "client_id": part.client_id, "part_number": part.part_number, @@ -358,7 +362,5 @@ async def get_part_regulatory_info( "license_code": part.license_code, "eccn": part.eccn, "export_code": part.export_code, - "exclusion_symbol": part.exclusion_symbol + "exclusion_symbol": part.exclusion_symbol, } - - diff --git a/backend/api/v1/modules/a76/parts/service.py b/backend/api/v1/modules/a76/parts/service.py index 31c8c457..e80c15e5 100644 --- a/backend/api/v1/modules/a76/parts/service.py +++ b/backend/api/v1/modules/a76/parts/service.py @@ -1,6 +1,7 @@ """ Capa de servicio para lógica de negocio de partes/componentes """ + from sqlalchemy.orm import Session from sqlalchemy.exc import IntegrityError from sqlalchemy import or_, and_, func @@ -34,7 +35,10 @@ class PartService: except IntegrityError as e: db.rollback() logger.error(f"Error creating part: {e}") - raise HTTPException(status_code=400, detail="Part with this client_id and part_number already exists") + raise HTTPException( + status_code=400, + detail="Part with this client_id and part_number already exists", + ) except Exception as e: db.rollback() logger.error(f"Unexpected error creating part: {e}") @@ -46,55 +50,58 @@ class PartService: Obtener una parte por clave de cliente y número de parte """ try: - return db.query(Part).filter( - and_( - Part.client_id == client_id, - Part.part_number == part_number + return ( + db.query(Part) + .filter( + and_(Part.client_id == client_id, Part.part_number == part_number) ) - ).first() + .first() + ) except Exception as e: logger.error(f"Error getting part: {e}") raise HTTPException(status_code=500, detail="Error retrieving part") @staticmethod def get_parts_paginated( - db: Session, - skip: int = 0, + db: Session, + skip: int = 0, limit: int = 100, search: Optional[str] = None, client_id: Optional[int] = None, fraction: Optional[str] = None, - country_of_origin: Optional[str] = None + country_of_origin: Optional[str] = None, ) -> tuple[List[Part], int]: """ Obtener partes con paginación y filtros """ try: query = db.query(Part) - + # Aplicar filtros if search: - query = query.filter(or_( - Part.description_spanish.ilike(f"%{search}%"), - Part.description_english.ilike(f"%{search}%"), - Part.part_number.ilike(f"%{search}%") - )) - + query = query.filter( + or_( + Part.description_spanish.ilike(f"%{search}%"), + Part.description_english.ilike(f"%{search}%"), + Part.part_number.ilike(f"%{search}%"), + ) + ) + if client_id is not None: query = query.filter(Part.client_id == client_id) - + if fraction: query = query.filter(Part.fraction == fraction) - + if country_of_origin: query = query.filter(Part.country_of_origin == country_of_origin) - + # Contar total total = query.count() - + # Aplicar paginación parts = query.offset(skip).limit(limit).all() - + return parts, total except Exception as e: logger.error(f"Error getting paginated parts: {e}") @@ -117,15 +124,21 @@ class PartService: Buscar partes por fracción arancelaria """ try: - return db.query(Part).filter( - or_( - Part.fraction.ilike(f"%{fraction}%"), - Part.us_fraction.ilike(f"%{fraction}%") + return ( + db.query(Part) + .filter( + or_( + Part.fraction.ilike(f"%{fraction}%"), + Part.us_fraction.ilike(f"%{fraction}%"), + ) ) - ).all() + .all() + ) except Exception as e: logger.error(f"Error searching parts by fraction: {e}") - raise HTTPException(status_code=500, detail="Error searching parts by fraction") + raise HTTPException( + status_code=500, detail="Error searching parts by fraction" + ) @staticmethod def search_parts_by_supplier(db: Session, supplier: str) -> List[Part]: @@ -136,7 +149,9 @@ class PartService: return db.query(Part).filter(Part.supplier.ilike(f"%{supplier}%")).all() except Exception as e: logger.error(f"Error searching parts by supplier: {e}") - raise HTTPException(status_code=500, detail="Error searching parts by supplier") + raise HTTPException( + status_code=500, detail="Error searching parts by supplier" + ) @staticmethod def search_parts_by_country(db: Session, country_code: str) -> List[Part]: @@ -147,10 +162,14 @@ class PartService: return db.query(Part).filter(Part.country_of_origin == country_code).all() except Exception as e: logger.error(f"Error searching parts by country: {e}") - raise HTTPException(status_code=500, detail="Error searching parts by country") + raise HTTPException( + status_code=500, detail="Error searching parts by country" + ) @staticmethod - def update_part(db: Session, client_id: int, part_number: str, part_data: PartUpdateDTO) -> Optional[Part]: + def update_part( + db: Session, client_id: int, part_number: str, part_data: PartUpdateDTO + ) -> Optional[Part]: """ Actualizar una parte existente """ @@ -158,11 +177,11 @@ class PartService: db_part = PartService.get_part(db, client_id, part_number) if not db_part: return None - + # Actualizar campos for field, value in part_data.model_dump(exclude_unset=True).items(): setattr(db_part, field, value) - + db.commit() db.refresh(db_part) return db_part @@ -180,7 +199,7 @@ class PartService: db_part = PartService.get_part(db, client_id, part_number) if not db_part: return False - + db.delete(db_part) db.commit() return True @@ -190,7 +209,9 @@ class PartService: raise HTTPException(status_code=500, detail="Error deleting part") @staticmethod - def toggle_part_status(db: Session, client_id: int, part_number: str) -> Optional[Part]: + def toggle_part_status( + db: Session, client_id: int, part_number: str + ) -> Optional[Part]: """ Cambiar el estado habilitado/deshabilitado de una parte """ @@ -198,10 +219,10 @@ class PartService: db_part = PartService.get_part(db, client_id, part_number) if not db_part: return None - + # Toggle status (assuming 1 = enabled, 0 = disabled) db_part.enabled_disabled = 1 if db_part.enabled_disabled == 0 else 0 - + db.commit() db.refresh(db_part) return db_part @@ -217,37 +238,49 @@ class PartService: """ try: total_parts = db.query(Part).count() - + # Partes por cliente - parts_by_client = db.query( - Part.client_id, - func.count(Part.part_number).label('count') - ).group_by(Part.client_id).all() - + parts_by_client = ( + db.query(Part.client_id, func.count(Part.part_number).label("count")) + .group_by(Part.client_id) + .all() + ) + # Partes por país de origen - parts_by_country = db.query( - Part.country_of_origin, - func.count(Part.part_number).label('count') - ).filter(Part.country_of_origin.isnot(None))\ - .group_by(Part.country_of_origin).all() - + parts_by_country = ( + db.query( + Part.country_of_origin, func.count(Part.part_number).label("count") + ) + .filter(Part.country_of_origin.isnot(None)) + .group_by(Part.country_of_origin) + .all() + ) + # Partes habilitadas vs deshabilitadas enabled_parts = db.query(Part).filter(Part.enabled_disabled == 1).count() disabled_parts = db.query(Part).filter(Part.enabled_disabled == 0).count() - + return { "total_parts": total_parts, "enabled_parts": enabled_parts, "disabled_parts": disabled_parts, - "parts_by_client": [{"client_id": item[0], "count": item[1]} for item in parts_by_client], - "parts_by_country": [{"country": item[0], "count": item[1]} for item in parts_by_country] + "parts_by_client": [ + {"client_id": item[0], "count": item[1]} for item in parts_by_client + ], + "parts_by_country": [ + {"country": item[0], "count": item[1]} for item in parts_by_country + ], } except Exception as e: logger.error(f"Error getting parts statistics: {e}") - raise HTTPException(status_code=500, detail="Error retrieving parts statistics") + raise HTTPException( + status_code=500, detail="Error retrieving parts statistics" + ) @staticmethod - def get_part_regulatory_info(db: Session, client_id: int, part_number: str) -> Optional[dict]: + def get_part_regulatory_info( + db: Session, client_id: int, part_number: str + ) -> Optional[dict]: """ Obtener información regulatoria específica de una parte """ @@ -255,7 +288,7 @@ class PartService: db_part = PartService.get_part(db, client_id, part_number) if not db_part: return None - + return { "client_id": db_part.client_id, "part_number": db_part.part_number, @@ -267,9 +300,10 @@ class PartService: "eccn": db_part.eccn, "export_code": db_part.export_code, "exclusion_symbol": db_part.exclusion_symbol, - "country_of_origin": db_part.country_of_origin + "country_of_origin": db_part.country_of_origin, } except Exception as e: logger.error(f"Error getting part regulatory info: {e}") - raise HTTPException(status_code=500, detail="Error retrieving part regulatory information") - + raise HTTPException( + status_code=500, detail="Error retrieving part regulatory information" + ) diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_additional.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_additional.py index 5e372c0f..d2295e0b 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_additional.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_additional.py @@ -5,23 +5,34 @@ from datetime import datetime class PedimentoConfigAdditionalBase(BaseModel): """Base schema for Pedimento Config Additional""" + pedimento_id: int = Field(..., description="Pedimento ID") tenant_id: int = Field(..., description="Tenant ID") add_po_identifier: Optional[int] = Field(None, description="Add PO identifier") - do_not_exempt_norms_complement_x: Optional[int] = Field(None, description="Do not exempt norms complement X") - manual_pedimento_year: Optional[str] = Field(None, max_length=2, description="Manual pedimento year") - enable_import_invoice_recipient: Optional[int] = Field(None, description="Enable import invoice recipient") - send_502_validation_file_for_consolidated: Optional[int] = Field(None, description="Send 502 validation file for consolidated") + do_not_exempt_norms_complement_x: Optional[int] = Field( + None, description="Do not exempt norms complement X" + ) + manual_pedimento_year: Optional[str] = Field( + None, max_length=2, description="Manual pedimento year" + ) + enable_import_invoice_recipient: Optional[int] = Field( + None, description="Enable import invoice recipient" + ) + send_502_validation_file_for_consolidated: Optional[int] = Field( + None, description="Send 502 validation file for consolidated" + ) add_remove_norms: Optional[int] = Field(None, description="Add/remove norms") class PedimentoConfigAdditionalCreate(PedimentoConfigAdditionalBase): """Schema for creating a new Pedimento Config Additional""" + pass class PedimentoConfigAdditionalUpdate(BaseModel): """Schema for updating a Pedimento Config Additional""" + add_po_identifier: Optional[int] = None do_not_exempt_norms_complement_x: Optional[int] = None manual_pedimento_year: Optional[str] = Field(None, max_length=2) @@ -32,6 +43,7 @@ class PedimentoConfigAdditionalUpdate(BaseModel): class PedimentoConfigAdditionalResponse(PedimentoConfigAdditionalBase): """Schema for Pedimento Config Additional response""" + id: int created_at: datetime diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_calculations.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_calculations.py index a9289d75..9c3a1813 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_calculations.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_calculations.py @@ -5,27 +5,40 @@ from datetime import datetime class PedimentoConfigCalculationsBase(BaseModel): """Base schema for Pedimento Config Calculations""" + pedimento_id: int = Field(..., description="Pedimento ID") tenant_id: int = Field(..., description="Tenant ID") dta_type: Optional[str] = Field(None, max_length=1, description="DTA type") dta_operation: Optional[int] = Field(None, description="DTA operation") dta_vehicle_count: Optional[int] = Field(None, description="DTA vehicle count") - dta_mixed_rate_8permil: Optional[int] = Field(None, description="DTA mixed rate 8 per mil") + dta_mixed_rate_8permil: Optional[int] = Field( + None, description="DTA mixed rate 8 per mil" + ) pays_vat: Optional[int] = Field(None, description="Pays VAT") pays_prevalidation: Optional[int] = Field(None, description="Pays prevalidation") - include_sagar_certificate_fee: Optional[int] = Field(None, description="Include SAGAR certificate fee") - fixed_vehicle_dta_fee: Optional[int] = Field(None, description="Fixed vehicle DTA fee") - additional_fixed_fee: Optional[int] = Field(None, description="Additional fixed fee") - additional_fixed_fee_payment_method: Optional[int] = Field(None, description="Additional fixed fee payment method") + include_sagar_certificate_fee: Optional[int] = Field( + None, description="Include SAGAR certificate fee" + ) + fixed_vehicle_dta_fee: Optional[int] = Field( + None, description="Fixed vehicle DTA fee" + ) + additional_fixed_fee: Optional[int] = Field( + None, description="Additional fixed fee" + ) + additional_fixed_fee_payment_method: Optional[int] = Field( + None, description="Additional fixed fee payment method" + ) class PedimentoConfigCalculationsCreate(PedimentoConfigCalculationsBase): """Schema for creating a new Pedimento Config Calculations""" + pass class PedimentoConfigCalculationsUpdate(BaseModel): """Schema for updating a Pedimento Config Calculations""" + dta_type: Optional[str] = Field(None, max_length=1) dta_operation: Optional[int] = None dta_vehicle_count: Optional[int] = None @@ -40,6 +53,7 @@ class PedimentoConfigCalculationsUpdate(BaseModel): class PedimentoConfigCalculationsResponse(PedimentoConfigCalculationsBase): """Schema for Pedimento Config Calculations response""" + id: int created_at: datetime diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_parameters.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_parameters.py index 3590a24b..e48affa1 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_parameters.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_parameters.py @@ -6,28 +6,43 @@ from datetime import datetime class PedimentoConfigParametersBase(BaseModel): """Base schema for Pedimento Config Parameters""" + pedimento_id: int = Field(..., description="Pedimento ID") tenant_id: int = Field(..., description="Tenant ID") is_embassy: Optional[int] = Field(None, description="Is embassy") embassy_dta: Optional[Decimal] = Field(None, description="Embassy DTA") - rule_3121_section_ii: Optional[int] = Field(None, description="Rule 3.1.21 Section II") + rule_3121_section_ii: Optional[int] = Field( + None, description="Rule 3.1.21 Section II" + ) use_previous_tariff: Optional[int] = Field(None, description="Use previous tariff") use_payment_date_fi: Optional[int] = Field(None, description="Use payment date FI") - add_state_supplier_record_505: Optional[int] = Field(None, description="Add state supplier record 505") - customs_value_calculation: Optional[int] = Field(None, description="Customs value calculation") - two_decimals_unit_value: Optional[int] = Field(None, description="Two decimals unit value") - customs_value_per_item: Optional[int] = Field(None, description="Customs value per item") - is_national_supplier: Optional[int] = Field(None, description="Is national supplier") + add_state_supplier_record_505: Optional[int] = Field( + None, description="Add state supplier record 505" + ) + customs_value_calculation: Optional[int] = Field( + None, description="Customs value calculation" + ) + two_decimals_unit_value: Optional[int] = Field( + None, description="Two decimals unit value" + ) + customs_value_per_item: Optional[int] = Field( + None, description="Customs value per item" + ) + is_national_supplier: Optional[int] = Field( + None, description="Is national supplier" + ) is_consolidated: Optional[int] = Field(None, description="Is consolidated") class PedimentoConfigParametersCreate(PedimentoConfigParametersBase): """Schema for creating a new Pedimento Config Parameters""" + pass class PedimentoConfigParametersUpdate(BaseModel): """Schema for updating a Pedimento Config Parameters""" + is_embassy: Optional[int] = None embassy_dta: Optional[Decimal] = None rule_3121_section_ii: Optional[int] = None @@ -43,6 +58,7 @@ class PedimentoConfigParametersUpdate(BaseModel): class PedimentoConfigParametersResponse(PedimentoConfigParametersBase): """Schema for Pedimento Config Parameters response""" + id: int created_at: datetime diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_surcharges.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_surcharges.py index 36b4e5f4..fa19fe27 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_surcharges.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_surcharges.py @@ -5,6 +5,7 @@ from datetime import datetime class PedimentoConfigSurchargesBase(BaseModel): """Base schema for Pedimento Config Surcharges""" + pedimento_id: int = Field(..., description="Pedimento ID") tenant_id: int = Field(..., description="Tenant ID") surcharge_igi: Optional[int] = Field(None, description="Surcharge IGI") @@ -17,11 +18,13 @@ class PedimentoConfigSurchargesBase(BaseModel): class PedimentoConfigSurchargesCreate(PedimentoConfigSurchargesBase): """Schema for creating a new Pedimento Config Surcharges""" + pass class PedimentoConfigSurchargesUpdate(BaseModel): """Schema for updating a Pedimento Config Surcharges""" + surcharge_igi: Optional[int] = None surcharge_dta: Optional[int] = None surcharge_vat: Optional[int] = None @@ -32,6 +35,7 @@ class PedimentoConfigSurchargesUpdate(BaseModel): class PedimentoConfigSurchargesResponse(PedimentoConfigSurchargesBase): """Schema for Pedimento Config Surcharges response""" + id: int created_at: datetime diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_update_rectification.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_update_rectification.py index 08af0bb9..bfea2b18 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_update_rectification.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_update_rectification.py @@ -5,6 +5,7 @@ from datetime import datetime class PedimentoConfigUpdateRectificationBase(BaseModel): """Base schema for Pedimento Config Update Rectification""" + pedimento_id: int = Field(..., description="Pedimento ID") tenant_id: int = Field(..., description="Tenant ID") update_vat: Optional[int] = Field(None, description="Update VAT") @@ -16,11 +17,13 @@ class PedimentoConfigUpdateRectificationBase(BaseModel): class PedimentoConfigUpdateRectificationCreate(PedimentoConfigUpdateRectificationBase): """Schema for creating a new Pedimento Config Update Rectification""" + pass class PedimentoConfigUpdateRectificationUpdate(BaseModel): """Schema for updating a Pedimento Config Update Rectification""" + update_vat: Optional[int] = None update_advalorem: Optional[int] = None update_cc: Optional[int] = None @@ -28,8 +31,11 @@ class PedimentoConfigUpdateRectificationUpdate(BaseModel): calculate_surcharge: Optional[int] = None -class PedimentoConfigUpdateRectificationResponse(PedimentoConfigUpdateRectificationBase): +class PedimentoConfigUpdateRectificationResponse( + PedimentoConfigUpdateRectificationBase +): """Schema for Pedimento Config Update Rectification response""" + id: int created_at: datetime diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_updates.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_updates.py index a1fda392..f76aeacb 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_updates.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_config_updates.py @@ -5,6 +5,7 @@ from datetime import datetime class PedimentoConfigUpdatesBase(BaseModel): """Base schema for Pedimento Config Updates""" + pedimento_id: int = Field(..., description="Pedimento ID") tenant_id: int = Field(..., description="Tenant ID") update_vat: Optional[int] = Field(None, description="Update VAT") @@ -15,11 +16,13 @@ class PedimentoConfigUpdatesBase(BaseModel): class PedimentoConfigUpdatesCreate(PedimentoConfigUpdatesBase): """Schema for creating a new Pedimento Config Updates""" + pass class PedimentoConfigUpdatesUpdate(BaseModel): """Schema for updating a Pedimento Config Updates""" + update_vat: Optional[int] = None update_advalorem: Optional[int] = None update_cc: Optional[int] = None @@ -28,6 +31,7 @@ class PedimentoConfigUpdatesUpdate(BaseModel): class PedimentoConfigUpdatesResponse(PedimentoConfigUpdatesBase): """Schema for Pedimento Config Updates response""" + id: int created_at: datetime diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_customs_offices.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_customs_offices.py index 108cf979..532541ce 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_customs_offices.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_customs_offices.py @@ -5,25 +5,33 @@ from datetime import datetime class PedimentoCustomsOfficesBase(BaseModel): """Base schema for Pedimento Customs Offices""" + pedimento_id: int = Field(..., description="Pedimento ID") tenant_id: int = Field(..., description="Tenant ID") - dispatch_customs: Optional[str] = Field(None, max_length=3, description="Dispatch customs") - entry_exit_customs: Optional[str] = Field(None, max_length=3, description="Entry/exit customs") + dispatch_customs: Optional[str] = Field( + None, max_length=3, description="Dispatch customs" + ) + entry_exit_customs: Optional[str] = Field( + None, max_length=3, description="Entry/exit customs" + ) class PedimentoCustomsOfficesCreate(PedimentoCustomsOfficesBase): """Schema for creating a new Pedimento Customs Offices""" + pass class PedimentoCustomsOfficesUpdate(BaseModel): """Schema for updating a Pedimento Customs Offices""" + dispatch_customs: Optional[str] = Field(None, max_length=3) entry_exit_customs: Optional[str] = Field(None, max_length=3) class PedimentoCustomsOfficesResponse(PedimentoCustomsOfficesBase): """Schema for Pedimento Customs Offices response""" + id: int created_at: datetime diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_dates.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_dates.py index 98d0c593..db01215e 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_dates.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_dates.py @@ -5,10 +5,13 @@ from datetime import datetime, time class PedimentoDatesBase(BaseModel): """Base schema for Pedimento Dates""" + 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") - rectification_payment_date: Optional[datetime] = Field(None, description="Rectification payment date") + rectification_payment_date: Optional[datetime] = Field( + None, description="Rectification payment date" + ) extraction_date: Optional[datetime] = Field(None, description="Extraction date") submission_date: Optional[datetime] = Field(None, description="Submission date") eucan_date: Optional[datetime] = Field(None, description="EUCAN date") @@ -21,11 +24,13 @@ class PedimentoDatesBase(BaseModel): class PedimentoDatesCreate(PedimentoDatesBase): """Schema for creating a new Pedimento Dates""" + pass class PedimentoDatesUpdate(BaseModel): """Schema for updating a Pedimento Dates""" + entry_date: Optional[datetime] = None pedimento_date: Optional[datetime] = None payment_date: Optional[datetime] = None @@ -42,6 +47,7 @@ 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") diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_decrementables.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_decrementables.py index ec76a40e..199d1983 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_decrementables.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_decrementables.py @@ -6,6 +6,7 @@ from datetime import datetime class PedimentoDecrementablesBase(BaseModel): """Base schema for Pedimento Decrementables""" + pedimento_id: int = Field(..., description="Pedimento ID") tenant_id: int = Field(..., description="Tenant ID") freight: Optional[Decimal] = Field(None, description="Freight") @@ -15,17 +16,23 @@ class PedimentoDecrementablesBase(BaseModel): others: Optional[Decimal] = Field(None, description="Others") currency: Optional[str] = Field(None, max_length=3, description="Currency") currency_factor: Optional[Decimal] = Field(None, description="Currency factor") - not_affect_usd_value: Optional[int] = Field(None, description="Not affect USD value") - not_affect_customs_value: Optional[int] = Field(None, description="Not affect customs value") + not_affect_usd_value: Optional[int] = Field( + None, description="Not affect USD value" + ) + not_affect_customs_value: Optional[int] = Field( + None, description="Not affect customs value" + ) class PedimentoDecrementablesCreate(PedimentoDecrementablesBase): """Schema for creating a new Pedimento Decrementables""" + pass class PedimentoDecrementablesUpdate(BaseModel): """Schema for updating a Pedimento Decrementables""" + freight: Optional[Decimal] = None insurance: Optional[Decimal] = None loading: Optional[Decimal] = None @@ -39,6 +46,7 @@ class PedimentoDecrementablesUpdate(BaseModel): class PedimentoDecrementablesResponse(PedimentoDecrementablesBase): """Schema for Pedimento Decrementables response""" + id: int created_at: datetime diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_incrementables.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_incrementables.py index fd22d50b..e7c347dd 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_incrementables.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_incrementables.py @@ -6,6 +6,7 @@ from datetime import datetime class PedimentoIncrementablesBase(BaseModel): """Base schema for Pedimento Incrementables""" + pedimento_id: int = Field(..., description="Pedimento ID") tenant_id: int = Field(..., description="Tenant ID") insured_value: Optional[Decimal] = Field(None, description="Insured value") @@ -16,17 +17,23 @@ class PedimentoIncrementablesBase(BaseModel): deductibles: Optional[Decimal] = Field(None, description="Deductibles") currency: Optional[str] = Field(None, max_length=3, description="Currency") currency_factor: Optional[Decimal] = Field(None, description="Currency factor") - not_affect_usd_value: Optional[int] = Field(None, description="Not affect USD value") - not_affect_customs_value: Optional[int] = Field(None, description="Not affect customs value") + not_affect_usd_value: Optional[int] = Field( + None, description="Not affect USD value" + ) + not_affect_customs_value: Optional[int] = Field( + None, description="Not affect customs value" + ) class PedimentoIncrementablesCreate(PedimentoIncrementablesBase): """Schema for creating a new Pedimento Incrementables""" + pass class PedimentoIncrementablesUpdate(BaseModel): """Schema for updating a Pedimento Incrementables""" + insured_value: Optional[Decimal] = None freight: Optional[Decimal] = None insurance: Optional[Decimal] = None @@ -41,6 +48,7 @@ class PedimentoIncrementablesUpdate(BaseModel): class PedimentoIncrementablesResponse(PedimentoIncrementablesBase): """Schema for Pedimento Incrementables response""" + id: int created_at: datetime diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_indexes.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_indexes.py index 6dd993c9..4e228798 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_indexes.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_indexes.py @@ -6,20 +6,25 @@ from datetime import datetime class PedimentoIndexesBase(BaseModel): """Base schema for Pedimento Indexes""" + pedimento_id: int = Field(..., description="Pedimento ID") tenant_id: int = Field(..., description="Tenant ID") update_factor_type: Optional[int] = Field(None, description="Update factor type") update_factor: Optional[Decimal] = Field(None, description="Update factor") - manual_update_factor: Optional[int] = Field(None, description="Manual update factor") + manual_update_factor: Optional[int] = Field( + None, description="Manual update factor" + ) class PedimentoIndexesCreate(PedimentoIndexesBase): """Schema for creating a new Pedimento Indexes""" + pass class PedimentoIndexesUpdate(BaseModel): """Schema for updating a Pedimento Indexes""" + update_factor_type: Optional[int] = None update_factor: Optional[Decimal] = None manual_update_factor: Optional[int] = None @@ -27,6 +32,7 @@ class PedimentoIndexesUpdate(BaseModel): class PedimentoIndexesResponse(PedimentoIndexesBase): """Schema for Pedimento Indexes response""" + id: int created_at: datetime diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_payments.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_payments.py index 4421637c..c2897b88 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_payments.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_payments.py @@ -5,10 +5,15 @@ from datetime import datetime, date as Date, time as Time class PedimentoPaymentsBase(BaseModel): """Base schema for Pedimento Payments""" + pedimento_id: int = Field(..., description="Pedimento ID") tenant_id: int = Field(..., description="Tenant ID") - acknowledgment: Optional[str] = Field(None, max_length=20, description="Acknowledgment") - operation_number: Optional[str] = Field(None, max_length=14, description="Operation number") + acknowledgment: Optional[str] = Field( + None, max_length=20, description="Acknowledgment" + ) + operation_number: Optional[str] = Field( + None, max_length=14, description="Operation number" + ) bank_code: Optional[int] = Field(None, description="Bank code") cashier: Optional[str] = Field(None, max_length=2, description="Cashier") date: Optional[Date] = Field(None, description="Date") @@ -23,11 +28,13 @@ class PedimentoPaymentsBase(BaseModel): class PedimentoPaymentsCreate(PedimentoPaymentsBase): """Schema for creating a new Pedimento Payments""" + pass class PedimentoPaymentsUpdate(BaseModel): """Schema for updating a Pedimento Payments""" + acknowledgment: Optional[str] = Field(None, max_length=20) operation_number: Optional[str] = Field(None, max_length=14) bank_code: Optional[int] = None @@ -44,6 +51,7 @@ class PedimentoPaymentsUpdate(BaseModel): class PedimentoPaymentsResponse(PedimentoPaymentsBase): """Schema for Pedimento Payments response""" + id: int created_at: datetime diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_rectification_destination.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_rectification_destination.py index 1f35d1af..6462e0ed 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_rectification_destination.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_rectification_destination.py @@ -4,21 +4,32 @@ from typing import Optional class PedimentoRectificationDestinationBase(BaseModel): """Base schema for Pedimento Rectification Destination""" + pedimento_id: int = Field(..., description="Pedimento ID") tenant_id: int = Field(..., description="Tenant ID") - destination_pedimento_year: Optional[str] = Field(None, max_length=2, description="Destination pedimento year") - destination_customs_office: Optional[str] = Field(None, max_length=3, description="Destination customs office") - destination_license: Optional[str] = Field(None, max_length=4, description="Destination license") - destination_pedimento_number: Optional[str] = Field(None, max_length=7, description="Destination pedimento number") + destination_pedimento_year: Optional[str] = Field( + None, max_length=2, description="Destination pedimento year" + ) + destination_customs_office: Optional[str] = Field( + None, max_length=3, description="Destination customs office" + ) + destination_license: Optional[str] = Field( + None, max_length=4, description="Destination license" + ) + destination_pedimento_number: Optional[str] = Field( + None, max_length=7, description="Destination pedimento number" + ) class PedimentoRectificationDestinationCreate(PedimentoRectificationDestinationBase): """Schema for creating a new Pedimento Rectification Destination""" + pass class PedimentoRectificationDestinationUpdate(BaseModel): """Schema for updating a Pedimento Rectification Destination""" + destination_pedimento_year: Optional[str] = Field(None, max_length=2) destination_customs_office: Optional[str] = Field(None, max_length=3) destination_license: Optional[str] = Field(None, max_length=4) @@ -27,6 +38,7 @@ class PedimentoRectificationDestinationUpdate(BaseModel): class PedimentoRectificationDestinationResponse(PedimentoRectificationDestinationBase): """Schema for Pedimento Rectification Destination response""" + id: int model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_rectification_origin.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_rectification_origin.py index ec8464c0..44e48461 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_rectification_origin.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_rectification_origin.py @@ -5,30 +5,49 @@ from datetime import datetime class PedimentoRectificationOriginBase(BaseModel): """Base schema for Pedimento Rectification Origin""" + pedimento_id: int = Field(..., description="Pedimento ID") tenant_id: int = Field(..., description="Tenant ID") - original_pedimento_year: Optional[str] = Field(None, max_length=2, description="Original pedimento year") - 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_code: Optional[str] = Field(None, max_length=2, description="Original pedimento key") - original_payment_date: Optional[datetime] = Field(None, description="Original payment date") + original_pedimento_year: Optional[str] = Field( + None, max_length=2, description="Original pedimento year" + ) + 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_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") reason: Optional[str] = Field(None, max_length=255, description="Reason") charge_to_client: Optional[int] = Field(None, description="Charge to client") - use_original_payment_date_for_interest_calc: Optional[int] = Field(None, description="Use original payment date for interest calculation") + use_original_payment_date_for_interest_calc: Optional[int] = Field( + None, description="Use original payment date for interest calculation" + ) manual_calculation: Optional[int] = Field(None, description="Manual calculation") - original_pedimento_norms: Optional[int] = Field(None, description="Original pedimento norms") + original_pedimento_norms: Optional[int] = Field( + None, description="Original pedimento norms" + ) class PedimentoRectificationOriginCreate(PedimentoRectificationOriginBase): """Schema for creating a new Pedimento Rectification Origin""" + pass class PedimentoRectificationOriginUpdate(BaseModel): """Schema for updating a Pedimento Rectification Origin""" + original_pedimento_year: Optional[str] = Field(None, max_length=2) original_customs_office: Optional[str] = Field(None, max_length=3) original_license: Optional[str] = Field(None, max_length=4) @@ -46,6 +65,7 @@ class PedimentoRectificationOriginUpdate(BaseModel): class PedimentoRectificationOriginResponse(PedimentoRectificationOriginBase): """Schema for Pedimento Rectification Origin response""" + id: int model_config = ConfigDict(from_attributes=True) diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_transport_means.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_transport_means.py index 3bec5e34..f30b4975 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_transport_means.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_transport_means.py @@ -5,6 +5,7 @@ from datetime import datetime class PedimentoTransportMeansBase(BaseModel): """Base schema for Pedimento Transport Means""" + pedimento_id: int = Field(..., description="Pedimento ID") tenant_id: int = Field(..., description="Tenant ID") destination: Optional[int] = Field(None, description="Destination") @@ -15,11 +16,13 @@ class PedimentoTransportMeansBase(BaseModel): class PedimentoTransportMeansCreate(PedimentoTransportMeansBase): """Schema for creating a new Pedimento Transport Means""" + pass class PedimentoTransportMeansUpdate(BaseModel): """Schema for updating a Pedimento Transport Means""" + destination: Optional[int] = None entry_exit: Optional[str] = Field(None, max_length=2) arrival: Optional[str] = Field(None, max_length=2) @@ -28,6 +31,7 @@ class PedimentoTransportMeansUpdate(BaseModel): class PedimentoTransportMeansResponse(PedimentoTransportMeansBase): """Schema for Pedimento Transport Means response""" + id: int created_at: datetime diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_validation.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_validation.py index 817394e5..e084a61b 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_validation.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimento_validation.py @@ -5,25 +5,36 @@ from datetime import datetime class PedimentoValidationBase(BaseModel): """Base schema for Pedimento Validation""" + pedimento_id: int = Field(..., description="Pedimento ID") tenant_id: int = Field(..., description="Tenant ID") validator: Optional[str] = Field(None, max_length=3, description="Validator") - validation_ack: Optional[str] = Field(None, max_length=8, description="Validation acknowledgment") + validation_ack: Optional[str] = Field( + None, max_length=8, description="Validation acknowledgment" + ) pre_ack: Optional[str] = Field(None, max_length=8, description="Pre-acknowledgment") - line_signature: Optional[str] = Field(None, max_length=50, description="Line signature") - electronic_signature: Optional[str] = Field(None, max_length=999, description="Electronic signature") - certificate_number: Optional[str] = Field(None, max_length=99, description="Certificate number") + line_signature: Optional[str] = Field( + None, max_length=50, description="Line signature" + ) + electronic_signature: Optional[str] = Field( + None, max_length=999, description="Electronic signature" + ) + certificate_number: Optional[str] = Field( + None, max_length=99, description="Certificate number" + ) validator_id: Optional[int] = Field(None, description="Validator ID") responsible_id: Optional[int] = Field(None, description="Responsible ID") class PedimentoValidationCreate(PedimentoValidationBase): """Schema for creating a new Pedimento Validation""" + pass class PedimentoValidationUpdate(BaseModel): """Schema for updating a Pedimento Validation""" + validator: Optional[str] = Field(None, max_length=3) validation_ack: Optional[str] = Field(None, max_length=8) pre_ack: Optional[str] = Field(None, max_length=8) @@ -36,6 +47,7 @@ class PedimentoValidationUpdate(BaseModel): class PedimentoValidationResponse(PedimentoValidationBase): """Schema for Pedimento Validation response""" + id: int created_at: datetime diff --git a/backend/api/v1/modules/a76/pedmientos/dtos/pedimentos.py b/backend/api/v1/modules/a76/pedmientos/dtos/pedimentos.py index d6e7fb9c..31f92fc8 100644 --- a/backend/api/v1/modules/a76/pedmientos/dtos/pedimentos.py +++ b/backend/api/v1/modules/a76/pedmientos/dtos/pedimentos.py @@ -4,20 +4,29 @@ from typing import Optional from decimal import Decimal from datetime import datetime + class OperationType(IntEnum): EXPORTACION = 1 IMPORTACION = 2 + class PedimentosBase(BaseModel): """Base schema for Pedimentos""" + year: Optional[str] = Field(None, max_length=2, description="Year") - customs_office: Optional[str] = Field(None, max_length=2, description="Customs office") + customs_office: Optional[str] = Field( + None, max_length=2, description="Customs office" + ) license: Optional[str] = Field(None, max_length=4, description="License") - pedimento_number: Optional[str] = Field(None, max_length=7, description="Pedimento number") + pedimento_number: Optional[str] = Field( + None, max_length=7, description="Pedimento number" + ) 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_code: 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") @@ -28,11 +37,13 @@ class PedimentosBase(BaseModel): class PedimentosCreate(PedimentosBase): """Schema for creating a new Pedimento""" + pass class PedimentosUpdate(BaseModel): """Schema for updating a Pedimento""" + year: Optional[str] = Field(..., max_length=2) customs_office: Optional[str] = Field(..., max_length=2) license: Optional[str] = Field(..., max_length=4) @@ -51,6 +62,7 @@ class PedimentosUpdate(BaseModel): class PedimentosResponse(PedimentosBase): """Schema for Pedimento response""" + id: int tenant_id: int created_at: datetime diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_additional.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_additional.py index e7d5e3b5..6949395d 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_additional.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_additional.py @@ -1,6 +1,16 @@ from typing import TYPE_CHECKING -from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, String, UniqueConstraint, func, text -from sqlalchemy.orm import Mapped, mapped_column, relationship +from sqlalchemy import ( + DateTime, + ForeignKeyConstraint, + Integer, + PrimaryKeyConstraint, + SmallInteger, + String, + UniqueConstraint, + func, + text, +) +from sqlalchemy.orm import Mapped, mapped_column, relationship from datetime import datetime from core.database import Base @@ -9,31 +19,55 @@ if TYPE_CHECKING: class PedimentoConfigAdditional(Base): - __tablename__ = 'pedimento_config_additional' + __tablename__ = "pedimento_config_additional" __table_args__ = ( - PrimaryKeyConstraint('id', name='pedimento_config_additional_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_config_additional_tenant'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_config_additional_company'), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_config_additional'), - UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_config_additional_pedimento_id_key'), - {'schema': 'a76'} + PrimaryKeyConstraint("id", name="pedimento_config_additional_pkey"), + ForeignKeyConstraint( + ["tenant_id"], + ["a76.tenants.id"], + name="fk_pedimento_config_additional_tenant", + ), + ForeignKeyConstraint( + ["company_id"], + ["a76.company.id"], + name="fk_pedimento_config_additional_company", + ), + ForeignKeyConstraint( + ["pedimento_id"], + ["a76.pedimentos.id"], + ondelete="CASCADE", + name="fk_pedimento_config_additional", + ), + UniqueConstraint( + "tenant_id", + "company_id", + "pedimento_id", + name="pedimento_config_additional_pedimento_id_key", + ), + {"schema": "a76"}, ) - id: Mapped [int] = mapped_column(Integer) - tenant_id: Mapped [int] = mapped_column(Integer, nullable=False, index=True) + id: Mapped[int] = mapped_column(Integer) + tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - pedimento_id: Mapped [int] = mapped_column(Integer, nullable=False) - - add_po_identifier: Mapped [int] = mapped_column(SmallInteger) - do_not_exempt_norms_complement_x: Mapped [int] = mapped_column(SmallInteger) - manual_pedimento_year: Mapped [str] = mapped_column(String(2)) - enable_import_invoice_recipient: Mapped [int] = mapped_column(SmallInteger) - send_502_validation_file_for_consolidated: Mapped [int] = mapped_column(SmallInteger) - add_remove_norms: Mapped [int] = mapped_column(SmallInteger) - + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + + add_po_identifier: Mapped[int] = mapped_column(SmallInteger) + do_not_exempt_norms_complement_x: Mapped[int] = mapped_column(SmallInteger) + manual_pedimento_year: Mapped[str] = mapped_column(String(2)) + enable_import_invoice_recipient: Mapped[int] = mapped_column(SmallInteger) + send_502_validation_file_for_consolidated: Mapped[int] = mapped_column(SmallInteger) + add_remove_norms: Mapped[int] = mapped_column(SmallInteger) + # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) - updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now(), onupdate=func.now() + ) deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_config_additional') \ No newline at end of file + pedimento: Mapped["Pedimentos"] = relationship( + "Pedimentos", back_populates="pedimento_config_additional" + ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_calculations.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_calculations.py index e655fd4d..47a309d3 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_calculations.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_calculations.py @@ -1,5 +1,15 @@ from typing import TYPE_CHECKING -from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, String, UniqueConstraint, func, text +from sqlalchemy import ( + DateTime, + ForeignKeyConstraint, + Integer, + PrimaryKeyConstraint, + SmallInteger, + String, + UniqueConstraint, + func, + text, +) from sqlalchemy.orm import Mapped, mapped_column, relationship from datetime import datetime from core.database import Base @@ -7,22 +17,33 @@ from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos + class PedimentoConfigCalculations(Base): - __tablename__ = 'pedimento_config_calculations' + __tablename__ = "pedimento_config_calculations" __table_args__ = ( - PrimaryKeyConstraint('id', name='pedimento_config_calculations_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), - ForeignKeyConstraint(['company_id'], ['a76.company.id']), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_config_calculations'), - UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_config_calculations_pedimento_id_key'), - {'schema': 'a76'} + PrimaryKeyConstraint("id", name="pedimento_config_calculations_pkey"), + ForeignKeyConstraint(["tenant_id"], ["a76.tenants.id"]), + ForeignKeyConstraint(["company_id"], ["a76.company.id"]), + ForeignKeyConstraint( + ["pedimento_id"], + ["a76.pedimentos.id"], + ondelete="CASCADE", + name="fk_pedimento_config_calculations", + ), + UniqueConstraint( + "tenant_id", + "company_id", + "pedimento_id", + name="pedimento_config_calculations_pedimento_id_key", + ), + {"schema": "a76"}, ) id: Mapped[int] = mapped_column(Integer) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) - + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + dta_type: Mapped[str] = mapped_column(String(1)) dta_operation: Mapped[int] = mapped_column(SmallInteger) dta_vehicle_count: Mapped[int] = mapped_column(SmallInteger) @@ -33,10 +54,16 @@ class PedimentoConfigCalculations(Base): fixed_vehicle_dta_fee: Mapped[int] = mapped_column(SmallInteger) additional_fixed_fee: Mapped[int] = mapped_column(SmallInteger) additional_fixed_fee_payment_method: Mapped[int] = mapped_column(SmallInteger) - + # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) - updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now(), onupdate=func.now() + ) deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_config_calculations') + pedimento: Mapped["Pedimentos"] = relationship( + "Pedimentos", back_populates="pedimento_config_calculations" + ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_parameters.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_parameters.py index fc8046c4..31b85708 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_parameters.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_parameters.py @@ -1,6 +1,16 @@ from decimal import Decimal from typing import TYPE_CHECKING -from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, Numeric, PrimaryKeyConstraint, SmallInteger, UniqueConstraint, func, text +from sqlalchemy import ( + DateTime, + ForeignKeyConstraint, + Integer, + Numeric, + PrimaryKeyConstraint, + SmallInteger, + UniqueConstraint, + func, + text, +) from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm.base import Mapped from datetime import datetime @@ -11,21 +21,39 @@ if TYPE_CHECKING: class PedimentoConfigParameters(Base): - __tablename__ = 'pedimento_config_parameters' + __tablename__ = "pedimento_config_parameters" __table_args__ = ( - PrimaryKeyConstraint('id', name='pedimento_config_parameters_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_config_parameters_tenant'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_config_parameters_company'), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_config_parameters'), - UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_config_parameters_pedimento_id_key'), - {'schema': 'a76'} + PrimaryKeyConstraint("id", name="pedimento_config_parameters_pkey"), + ForeignKeyConstraint( + ["tenant_id"], + ["a76.tenants.id"], + name="fk_pedimento_config_parameters_tenant", + ), + ForeignKeyConstraint( + ["company_id"], + ["a76.company.id"], + name="fk_pedimento_config_parameters_company", + ), + ForeignKeyConstraint( + ["pedimento_id"], + ["a76.pedimentos.id"], + ondelete="CASCADE", + name="fk_pedimento_config_parameters", + ), + UniqueConstraint( + "tenant_id", + "company_id", + "pedimento_id", + name="pedimento_config_parameters_pedimento_id_key", + ), + {"schema": "a76"}, ) id: Mapped[int] = mapped_column(Integer) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) - + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + is_embassy: Mapped[int] = mapped_column(SmallInteger) embassy_dta: Mapped[Decimal] = mapped_column(Numeric(11, 2)) rule_3121_section_ii: Mapped[int] = mapped_column(SmallInteger) @@ -37,10 +65,16 @@ class PedimentoConfigParameters(Base): customs_value_per_item: Mapped[int] = mapped_column(SmallInteger) is_national_supplier: Mapped[int] = mapped_column(SmallInteger) is_consolidated: Mapped[int] = mapped_column(SmallInteger) - + # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) - updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now(), onupdate=func.now() + ) deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_config_parameters') \ No newline at end of file + pedimento: Mapped["Pedimentos"] = relationship( + "Pedimentos", back_populates="pedimento_config_parameters" + ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_surcharges.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_surcharges.py index 845211c2..dd622e79 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_surcharges.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_surcharges.py @@ -1,5 +1,14 @@ from typing import TYPE_CHECKING -from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, UniqueConstraint, func, text +from sqlalchemy import ( + DateTime, + ForeignKeyConstraint, + Integer, + PrimaryKeyConstraint, + SmallInteger, + UniqueConstraint, + func, + text, +) from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm.base import Mapped from datetime import datetime @@ -8,32 +17,57 @@ from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos + class PedimentoConfigSurcharges(Base): - __tablename__ = 'pedimento_config_surcharges' + __tablename__ = "pedimento_config_surcharges" __table_args__ = ( - PrimaryKeyConstraint('id', name='pedimento_config_surcharges_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_config_surcharges_tenant'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_config_surcharges_company'), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_config_surcharges'), - UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_config_surcharges_pedimento_id_key'), - {'schema': 'a76'} + PrimaryKeyConstraint("id", name="pedimento_config_surcharges_pkey"), + ForeignKeyConstraint( + ["tenant_id"], + ["a76.tenants.id"], + name="fk_pedimento_config_surcharges_tenant", + ), + ForeignKeyConstraint( + ["company_id"], + ["a76.company.id"], + name="fk_pedimento_config_surcharges_company", + ), + ForeignKeyConstraint( + ["pedimento_id"], + ["a76.pedimentos.id"], + ondelete="CASCADE", + name="fk_pedimento_config_surcharges", + ), + UniqueConstraint( + "tenant_id", + "company_id", + "pedimento_id", + name="pedimento_config_surcharges_pedimento_id_key", + ), + {"schema": "a76"}, ) id: Mapped[int] = mapped_column(Integer) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) - + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + surcharge_igi: Mapped[int] = mapped_column(SmallInteger) surcharge_dta: Mapped[int] = mapped_column(SmallInteger) surcharge_vat: Mapped[int] = mapped_column(SmallInteger) surcharge_isan: Mapped[int] = mapped_column(SmallInteger) surcharge_ieps: Mapped[int] = mapped_column(SmallInteger) surcharge_cc: Mapped[int] = mapped_column(SmallInteger) - + # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) - updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now(), onupdate=func.now() + ) deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_config_surcharges') \ No newline at end of file + pedimento: Mapped["Pedimentos"] = relationship( + "Pedimentos", back_populates="pedimento_config_surcharges" + ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_update_rectification.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_update_rectification.py index b32e759d..e5535aa2 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_update_rectification.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_update_rectification.py @@ -1,5 +1,14 @@ from typing import TYPE_CHECKING -from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, UniqueConstraint, func, text +from sqlalchemy import ( + DateTime, + ForeignKeyConstraint, + Integer, + PrimaryKeyConstraint, + SmallInteger, + UniqueConstraint, + func, + text, +) from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm.base import Mapped from datetime import datetime @@ -8,31 +17,56 @@ from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos + class PedimentoConfigUpdateRectification(Base): - __tablename__ = 'pedimento_config_update_rectification' + __tablename__ = "pedimento_config_update_rectification" __table_args__ = ( - PrimaryKeyConstraint('id', name='pedimento_config_update_rectification_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_config_update_rectification_tenant'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_config_update_rectification_company'), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_config_update_rectification'), - UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_config_update_rectification_pedimento_id_key'), - {'schema': 'a76'} + PrimaryKeyConstraint("id", name="pedimento_config_update_rectification_pkey"), + ForeignKeyConstraint( + ["tenant_id"], + ["a76.tenants.id"], + name="fk_pedimento_config_update_rectification_tenant", + ), + ForeignKeyConstraint( + ["company_id"], + ["a76.company.id"], + name="fk_pedimento_config_update_rectification_company", + ), + ForeignKeyConstraint( + ["pedimento_id"], + ["a76.pedimentos.id"], + ondelete="CASCADE", + name="fk_pedimento_config_update_rectification", + ), + UniqueConstraint( + "tenant_id", + "company_id", + "pedimento_id", + name="pedimento_config_update_rectification_pedimento_id_key", + ), + {"schema": "a76"}, ) id: Mapped[int] = mapped_column(Integer) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) - + update_vat: Mapped[int] = mapped_column(SmallInteger) update_advalorem: Mapped[int] = mapped_column(SmallInteger) update_cc: Mapped[int] = mapped_column(SmallInteger) update_ieps: Mapped[int] = mapped_column(SmallInteger) calculate_surcharge: Mapped[int] = mapped_column(SmallInteger) - + # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) - updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now(), onupdate=func.now() + ) deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_config_update_rectification') \ No newline at end of file + pedimento: Mapped["Pedimentos"] = relationship( + "Pedimentos", back_populates="pedimento_config_update_rectification" + ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_updates.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_updates.py index a1f93a67..da9aba32 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_updates.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_config_updates.py @@ -1,5 +1,14 @@ from typing import TYPE_CHECKING -from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, UniqueConstraint, func, text +from sqlalchemy import ( + DateTime, + ForeignKeyConstraint, + Integer, + PrimaryKeyConstraint, + SmallInteger, + UniqueConstraint, + func, + text, +) from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm.base import Mapped from datetime import datetime @@ -8,30 +17,53 @@ from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos + class PedimentoConfigUpdates(Base): - __tablename__ = 'pedimento_config_updates' + __tablename__ = "pedimento_config_updates" __table_args__ = ( - PrimaryKeyConstraint('id', name='pedimento_config_updates_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_config_updates_tenant'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_config_updates_company'), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_config_updates'), - UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_config_updates_pedimento_id_key'), - {'schema': 'a76'} + PrimaryKeyConstraint("id", name="pedimento_config_updates_pkey"), + ForeignKeyConstraint( + ["tenant_id"], ["a76.tenants.id"], name="fk_pedimento_config_updates_tenant" + ), + ForeignKeyConstraint( + ["company_id"], + ["a76.company.id"], + name="fk_pedimento_config_updates_company", + ), + ForeignKeyConstraint( + ["pedimento_id"], + ["a76.pedimentos.id"], + ondelete="CASCADE", + name="fk_pedimento_config_updates", + ), + UniqueConstraint( + "tenant_id", + "company_id", + "pedimento_id", + name="pedimento_config_updates_pedimento_id_key", + ), + {"schema": "a76"}, ) id: Mapped[int] = mapped_column(Integer) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) - + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + update_vat: Mapped[int] = mapped_column(SmallInteger) update_advalorem: Mapped[int] = mapped_column(SmallInteger) update_cc: Mapped[int] = mapped_column(SmallInteger) update_ieps: Mapped[int] = mapped_column(SmallInteger) - + # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) - updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now(), onupdate=func.now() + ) deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_config_updates') \ No newline at end of file + pedimento: Mapped["Pedimentos"] = relationship( + "Pedimentos", back_populates="pedimento_config_updates" + ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_customs_offices.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_customs_offices.py index beb1dc1e..c41fdb4d 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_customs_offices.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_customs_offices.py @@ -1,5 +1,14 @@ from typing import TYPE_CHECKING -from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, func, text +from sqlalchemy import ( + DateTime, + ForeignKeyConstraint, + Integer, + PrimaryKeyConstraint, + String, + UniqueConstraint, + func, + text, +) from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm.base import Mapped from datetime import datetime @@ -8,28 +17,53 @@ from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos + class PedimentoCustomsOffices(Base): - __tablename__ = 'pedimento_customs_offices' + __tablename__ = "pedimento_customs_offices" __table_args__ = ( - PrimaryKeyConstraint('id', name='pedimento_customs_offices_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_customs_offices_tenant'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_customs_offices_company'), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_customs_offices'), - UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_customs_offices_pedimento_id_key'), - {'schema': 'a76'} + PrimaryKeyConstraint("id", name="pedimento_customs_offices_pkey"), + ForeignKeyConstraint( + ["tenant_id"], + ["a76.tenants.id"], + name="fk_pedimento_customs_offices_tenant", + ), + ForeignKeyConstraint( + ["company_id"], + ["a76.company.id"], + name="fk_pedimento_customs_offices_company", + ), + ForeignKeyConstraint( + ["pedimento_id"], + ["a76.pedimentos.id"], + ondelete="CASCADE", + name="fk_pedimento_customs_offices", + ), + UniqueConstraint( + "tenant_id", + "company_id", + "pedimento_id", + name="pedimento_customs_offices_pedimento_id_key", + ), + {"schema": "a76"}, ) id: Mapped[int] = mapped_column(Integer) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) - + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + dispatch_customs: Mapped[str] = mapped_column(String(3)) entry_exit_customs: Mapped[str] = mapped_column(String(3)) - + # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) - updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now(), onupdate=func.now() + ) deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_customs_offices') \ No newline at end of file + pedimento: Mapped["Pedimentos"] = relationship( + "Pedimentos", back_populates="pedimento_customs_offices" + ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_dates.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_dates.py index 61e87d3c..0ea9147a 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_dates.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_dates.py @@ -1,5 +1,15 @@ from typing import TYPE_CHECKING -from sqlalchemy import DateTime, ForeignKeyConstraint, Index, Integer, PrimaryKeyConstraint, Time, UniqueConstraint, func, text +from sqlalchemy import ( + DateTime, + ForeignKeyConstraint, + Index, + Integer, + PrimaryKeyConstraint, + Time, + UniqueConstraint, + func, + text, +) from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm.base import Mapped from datetime import datetime, time as datetime_time @@ -8,23 +18,38 @@ from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos + class PedimentoDates(Base): - __tablename__ = 'pedimento_dates' + __tablename__ = "pedimento_dates" __table_args__ = ( - PrimaryKeyConstraint('id', name='pedimento_dates_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_dates_tenant'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_dates_company'), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_dates'), - UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_dates_pedimento_id_key'), - Index('idx_pedimento_dates_pedimento_id', 'pedimento_id'), - {'schema': 'a76'} + PrimaryKeyConstraint("id", name="pedimento_dates_pkey"), + ForeignKeyConstraint( + ["tenant_id"], ["a76.tenants.id"], name="fk_pedimento_dates_tenant" + ), + ForeignKeyConstraint( + ["company_id"], ["a76.company.id"], name="fk_pedimento_dates_company" + ), + ForeignKeyConstraint( + ["pedimento_id"], + ["a76.pedimentos.id"], + ondelete="CASCADE", + name="fk_pedimento_dates", + ), + UniqueConstraint( + "tenant_id", + "company_id", + "pedimento_id", + name="pedimento_dates_pedimento_id_key", + ), + Index("idx_pedimento_dates_pedimento_id", "pedimento_id"), + {"schema": "a76"}, ) id: Mapped[int] = mapped_column(Integer) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) - + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + entry_date: Mapped[datetime] = mapped_column(DateTime) pedimento_date: Mapped[datetime] = mapped_column(DateTime) payment_date: Mapped[datetime] = mapped_column(DateTime) @@ -37,10 +62,16 @@ class PedimentoDates(Base): end_date: Mapped[datetime] = mapped_column(DateTime) capture_date: Mapped[datetime] = mapped_column(DateTime) capture_time: Mapped[datetime_time] = mapped_column(Time) - + # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) - updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now(), onupdate=func.now() + ) deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_dates') \ No newline at end of file + pedimento: Mapped["Pedimentos"] = relationship( + "Pedimentos", back_populates="pedimento_dates" + ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_decrementables.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_decrementables.py index 69aa462a..d806a0fc 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_decrementables.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_decrementables.py @@ -1,6 +1,17 @@ from decimal import Decimal from typing import TYPE_CHECKING -from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, Numeric, PrimaryKeyConstraint, SmallInteger, String, UniqueConstraint, func, text +from sqlalchemy import ( + DateTime, + ForeignKeyConstraint, + Integer, + Numeric, + PrimaryKeyConstraint, + SmallInteger, + String, + UniqueConstraint, + func, + text, +) from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm.base import Mapped from datetime import datetime @@ -9,22 +20,39 @@ from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos + class PedimentoDecrementables(Base): - __tablename__ = 'pedimento_decrementables' + __tablename__ = "pedimento_decrementables" __table_args__ = ( - PrimaryKeyConstraint('id', name='pedimento_decrementables_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_decrementables_tenant'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_decrementables_company'), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_decrementables'), - UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_decrementables_pedimento_id_key'), - {'schema': 'a76'} + PrimaryKeyConstraint("id", name="pedimento_decrementables_pkey"), + ForeignKeyConstraint( + ["tenant_id"], ["a76.tenants.id"], name="fk_pedimento_decrementables_tenant" + ), + ForeignKeyConstraint( + ["company_id"], + ["a76.company.id"], + name="fk_pedimento_decrementables_company", + ), + ForeignKeyConstraint( + ["pedimento_id"], + ["a76.pedimentos.id"], + ondelete="CASCADE", + name="fk_pedimento_decrementables", + ), + UniqueConstraint( + "tenant_id", + "company_id", + "pedimento_id", + name="pedimento_decrementables_pedimento_id_key", + ), + {"schema": "a76"}, ) id: Mapped[int] = mapped_column(Integer) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) - + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + freight: Mapped[Decimal] = mapped_column(Numeric(13, 2)) insurance: Mapped[Decimal] = mapped_column(Numeric(13, 2)) loading: Mapped[Decimal] = mapped_column(Numeric(13, 2)) @@ -34,10 +62,16 @@ class PedimentoDecrementables(Base): currency_factor: Mapped[Decimal] = mapped_column(Numeric(15, 8)) not_affect_usd_value: Mapped[int] = mapped_column(SmallInteger) not_affect_customs_value: Mapped[int] = mapped_column(SmallInteger) - + # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) - updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now(), onupdate=func.now() + ) deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_decrementables') \ No newline at end of file + pedimento: Mapped["Pedimentos"] = relationship( + "Pedimentos", back_populates="pedimento_decrementables" + ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_incrementables.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_incrementables.py index b9023aa8..3bb0112b 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_incrementables.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_incrementables.py @@ -1,6 +1,17 @@ from decimal import Decimal from typing import TYPE_CHECKING -from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, Numeric, PrimaryKeyConstraint, SmallInteger, String, UniqueConstraint, func, text +from sqlalchemy import ( + DateTime, + ForeignKeyConstraint, + Integer, + Numeric, + PrimaryKeyConstraint, + SmallInteger, + String, + UniqueConstraint, + func, + text, +) from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm.base import Mapped from datetime import datetime @@ -9,22 +20,39 @@ from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos + class PedimentoIncrementables(Base): - __tablename__ = 'pedimento_incrementables' + __tablename__ = "pedimento_incrementables" __table_args__ = ( - PrimaryKeyConstraint('id', name='pedimento_incrementables_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_incrementables_tenant'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_incrementables_company'), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_incrementables'), - UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_incrementables_pedimento_id_key'), - {'schema': 'a76'} + PrimaryKeyConstraint("id", name="pedimento_incrementables_pkey"), + ForeignKeyConstraint( + ["tenant_id"], ["a76.tenants.id"], name="fk_pedimento_incrementables_tenant" + ), + ForeignKeyConstraint( + ["company_id"], + ["a76.company.id"], + name="fk_pedimento_incrementables_company", + ), + ForeignKeyConstraint( + ["pedimento_id"], + ["a76.pedimentos.id"], + ondelete="CASCADE", + name="fk_pedimento_incrementables", + ), + UniqueConstraint( + "tenant_id", + "company_id", + "pedimento_id", + name="pedimento_incrementables_pedimento_id_key", + ), + {"schema": "a76"}, ) id: Mapped[int] = mapped_column(Integer) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) - + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + insured_value: Mapped[Decimal] = mapped_column(Numeric(13, 2)) freight: Mapped[Decimal] = mapped_column(Numeric(13, 2)) insurance: Mapped[Decimal] = mapped_column(Numeric(13, 2)) @@ -35,10 +63,16 @@ class PedimentoIncrementables(Base): currency_factor: Mapped[Decimal] = mapped_column(Numeric(15, 8)) not_affect_usd_value: Mapped[int] = mapped_column(SmallInteger) not_affect_customs_value: Mapped[int] = mapped_column(SmallInteger) - + # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) - updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now(), onupdate=func.now() + ) deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_incrementables') \ No newline at end of file + pedimento: Mapped["Pedimentos"] = relationship( + "Pedimentos", back_populates="pedimento_incrementables" + ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_indexes.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_indexes.py index 66d8478a..73133176 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_indexes.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_indexes.py @@ -1,6 +1,16 @@ from decimal import Decimal from typing import TYPE_CHECKING -from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, Numeric, PrimaryKeyConstraint, SmallInteger, UniqueConstraint, func, text +from sqlalchemy import ( + DateTime, + ForeignKeyConstraint, + Integer, + Numeric, + PrimaryKeyConstraint, + SmallInteger, + UniqueConstraint, + func, + text, +) from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm.base import Mapped from datetime import datetime @@ -9,29 +19,50 @@ from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos + class PedimentoIndexes(Base): - __tablename__ = 'pedimento_indexes' + __tablename__ = "pedimento_indexes" __table_args__ = ( - PrimaryKeyConstraint('id', name='pedimento_indexes_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_indexes_tenant'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_indexes_company'), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_indexes'), - UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_indexes_pedimento_id_key'), - {'schema': 'a76'} + PrimaryKeyConstraint("id", name="pedimento_indexes_pkey"), + ForeignKeyConstraint( + ["tenant_id"], ["a76.tenants.id"], name="fk_pedimento_indexes_tenant" + ), + ForeignKeyConstraint( + ["company_id"], ["a76.company.id"], name="fk_pedimento_indexes_company" + ), + ForeignKeyConstraint( + ["pedimento_id"], + ["a76.pedimentos.id"], + ondelete="CASCADE", + name="fk_pedimento_indexes", + ), + UniqueConstraint( + "tenant_id", + "company_id", + "pedimento_id", + name="pedimento_indexes_pedimento_id_key", + ), + {"schema": "a76"}, ) id: Mapped[int] = mapped_column(Integer) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) - + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + update_factor_type: Mapped[int] = mapped_column(SmallInteger) update_factor: Mapped[Decimal] = mapped_column(Numeric(7, 4)) manual_update_factor: Mapped[int] = mapped_column(SmallInteger) - + # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) - updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now(), onupdate=func.now() + ) deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_indexes') \ No newline at end of file + pedimento: Mapped["Pedimentos"] = relationship( + "Pedimentos", back_populates="pedimento_indexes" + ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_payments.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_payments.py index b7fdfd62..8f1c3780 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_payments.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_payments.py @@ -1,5 +1,18 @@ from typing import TYPE_CHECKING -from sqlalchemy import Date, DateTime, ForeignKeyConstraint, Index, Integer, PrimaryKeyConstraint, SmallInteger, String, Time, UniqueConstraint, func, text +from sqlalchemy import ( + Date, + DateTime, + ForeignKeyConstraint, + Index, + Integer, + PrimaryKeyConstraint, + SmallInteger, + String, + Time, + UniqueConstraint, + func, + text, +) from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm.base import Mapped from datetime import datetime, time as Time2, date as Date2 @@ -8,24 +21,39 @@ from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos + class PedimentoPayments(Base): - __tablename__ = 'pedimento_payments' + __tablename__ = "pedimento_payments" __table_args__ = ( - PrimaryKeyConstraint('id', name='pedimento_payments_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_payments_tenant'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_payments_company'), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_payments'), - UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_payments_pedimento_id_key'), - Index('idx_pedimento_payments_pedimento_id', 'pedimento_id'), - {'schema': 'a76'} + PrimaryKeyConstraint("id", name="pedimento_payments_pkey"), + ForeignKeyConstraint( + ["tenant_id"], ["a76.tenants.id"], name="fk_pedimento_payments_tenant" + ), + ForeignKeyConstraint( + ["company_id"], ["a76.company.id"], name="fk_pedimento_payments_company" + ), + ForeignKeyConstraint( + ["pedimento_id"], + ["a76.pedimentos.id"], + ondelete="CASCADE", + name="fk_pedimento_payments", + ), + UniqueConstraint( + "tenant_id", + "company_id", + "pedimento_id", + name="pedimento_payments_pedimento_id_key", + ), + Index("idx_pedimento_payments_pedimento_id", "pedimento_id"), + {"schema": "a76"}, ) id: Mapped[int] = mapped_column(Integer) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) payment_id: Mapped[int] = mapped_column(Integer) - + acknowledgment: Mapped[str] = mapped_column(String(20)) operation_number: Mapped[str] = mapped_column(String(14)) bank_code: Mapped[int] = mapped_column(Integer) @@ -36,11 +64,17 @@ class PedimentoPayments(Base): total_cash_paid: Mapped[int] = mapped_column(Integer) total_contributions: Mapped[int] = mapped_column(Integer) counter_payment: Mapped[int] = mapped_column(SmallInteger) - pece_code: Mapped[str] = mapped_column(String(5)) - + pece_code: Mapped[str] = mapped_column(String(5)) + # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) - updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now(), onupdate=func.now() + ) deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_payments') \ No newline at end of file + pedimento: Mapped["Pedimentos"] = relationship( + "Pedimentos", back_populates="pedimento_payments" + ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_destination.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_destination.py index ee643464..8e68a7c1 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_destination.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_destination.py @@ -1,6 +1,14 @@ from datetime import datetime from typing import TYPE_CHECKING -from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, func +from sqlalchemy import ( + DateTime, + ForeignKeyConstraint, + Integer, + PrimaryKeyConstraint, + String, + UniqueConstraint, + func, +) from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm.base import Mapped from core.database import Base @@ -8,30 +16,55 @@ from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos + class PedimentoRectificationDestination(Base): - __tablename__ = 'pedimento_rectification_destination' + __tablename__ = "pedimento_rectification_destination" __table_args__ = ( - PrimaryKeyConstraint('id', name='pedimento_rectification_destination_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_rectification_destination_tenant'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_rectification_destination_company'), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_rectification_destination'), - UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_rectification_destination_pedimento_id_key'), - {'schema': 'a76'} + PrimaryKeyConstraint("id", name="pedimento_rectification_destination_pkey"), + ForeignKeyConstraint( + ["tenant_id"], + ["a76.tenants.id"], + name="fk_pedimento_rectification_destination_tenant", + ), + ForeignKeyConstraint( + ["company_id"], + ["a76.company.id"], + name="fk_pedimento_rectification_destination_company", + ), + ForeignKeyConstraint( + ["pedimento_id"], + ["a76.pedimentos.id"], + ondelete="CASCADE", + name="fk_pedimento_rectification_destination", + ), + UniqueConstraint( + "tenant_id", + "company_id", + "pedimento_id", + name="pedimento_rectification_destination_pedimento_id_key", + ), + {"schema": "a76"}, ) id: Mapped[int] = mapped_column(Integer) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) - + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + destination_pedimento_year: Mapped[str] = mapped_column(String(2)) destination_customs_office: Mapped[str] = mapped_column(String(3)) destination_license: Mapped[str] = mapped_column(String(4)) destination_pedimento_number: Mapped[str] = mapped_column(String(7)) - + # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) - updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now(), onupdate=func.now() + ) deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_rectification_destination') \ No newline at end of file + pedimento: Mapped["Pedimentos"] = relationship( + "Pedimentos", back_populates="pedimento_rectification_destination" + ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_origin.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_origin.py index c0898eda..be6ab24d 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_origin.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_rectification_origin.py @@ -1,6 +1,15 @@ from datetime import datetime from typing import TYPE_CHECKING -from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, String, UniqueConstraint, func +from sqlalchemy import ( + DateTime, + ForeignKeyConstraint, + Integer, + PrimaryKeyConstraint, + SmallInteger, + String, + UniqueConstraint, + func, +) from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm.base import Mapped from core.database import Base @@ -8,22 +17,41 @@ from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos + class PedimentoRectificationOrigin(Base): - __tablename__ = 'pedimento_rectification_origin' + __tablename__ = "pedimento_rectification_origin" __table_args__ = ( - PrimaryKeyConstraint('id', name='pedimento_rectification_origin_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_rectification_origin_tenant'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_rectification_origin_company'), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_rectification_origin'), - UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_rectification_origin_pedimento_id_key'), - {'schema': 'a76'} + PrimaryKeyConstraint("id", name="pedimento_rectification_origin_pkey"), + ForeignKeyConstraint( + ["tenant_id"], + ["a76.tenants.id"], + name="fk_pedimento_rectification_origin_tenant", + ), + ForeignKeyConstraint( + ["company_id"], + ["a76.company.id"], + name="fk_pedimento_rectification_origin_company", + ), + ForeignKeyConstraint( + ["pedimento_id"], + ["a76.pedimentos.id"], + ondelete="CASCADE", + name="fk_pedimento_rectification_origin", + ), + UniqueConstraint( + "tenant_id", + "company_id", + "pedimento_id", + name="pedimento_rectification_origin_pedimento_id_key", + ), + {"schema": "a76"}, ) id: Mapped[int] = mapped_column(Integer) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) - + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + original_pedimento_year: Mapped[str] = mapped_column(String(2)) original_customs_office: Mapped[str] = mapped_column(String(3)) original_license: Mapped[str] = mapped_column(String(4)) @@ -34,13 +62,21 @@ class PedimentoRectificationOrigin(Base): total_others: Mapped[int] = mapped_column(Integer) reason: Mapped[str] = mapped_column(String(255)) charge_to_client: Mapped[int] = mapped_column(SmallInteger) - use_original_payment_date_for_interest_calc: Mapped[int] = mapped_column(SmallInteger) + use_original_payment_date_for_interest_calc: Mapped[int] = mapped_column( + SmallInteger + ) manual_calculation: Mapped[int] = mapped_column(SmallInteger) original_pedimento_norms: Mapped[int] = mapped_column(SmallInteger) - + # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) - updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now(), onupdate=func.now() + ) deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_rectification_origin') \ No newline at end of file + pedimento: Mapped["Pedimentos"] = relationship( + "Pedimentos", back_populates="pedimento_rectification_origin" + ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_transport_means.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_transport_means.py index 05146158..6fad41e5 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_transport_means.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_transport_means.py @@ -1,6 +1,15 @@ from datetime import datetime from typing import TYPE_CHECKING -from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, SmallInteger, String, UniqueConstraint, text +from sqlalchemy import ( + DateTime, + ForeignKeyConstraint, + Integer, + PrimaryKeyConstraint, + SmallInteger, + String, + UniqueConstraint, + text, +) from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm.base import Mapped from core.database import Base @@ -8,26 +17,49 @@ from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos + class PedimentoTransportMeans(Base): - __tablename__ = 'pedimento_transport_means' + __tablename__ = "pedimento_transport_means" __table_args__ = ( - PrimaryKeyConstraint('id', name='pedimento_transport_means_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimento_transport_means_tenant'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimento_transport_means_company'), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_transport_means'), - UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_transport_means_pedimento_id_key'), - {'schema': 'a76'} + PrimaryKeyConstraint("id", name="pedimento_transport_means_pkey"), + ForeignKeyConstraint( + ["tenant_id"], + ["a76.tenants.id"], + name="fk_pedimento_transport_means_tenant", + ), + ForeignKeyConstraint( + ["company_id"], + ["a76.company.id"], + name="fk_pedimento_transport_means_company", + ), + ForeignKeyConstraint( + ["pedimento_id"], + ["a76.pedimentos.id"], + ondelete="CASCADE", + name="fk_pedimento_transport_means", + ), + UniqueConstraint( + "tenant_id", + "company_id", + "pedimento_id", + name="pedimento_transport_means_pedimento_id_key", + ), + {"schema": "a76"}, ) id: Mapped[int] = mapped_column(Integer) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) - + destination: Mapped[int] = mapped_column(SmallInteger) entry_exit: Mapped[str] = mapped_column(String(2)) arrival: Mapped[str] = mapped_column(String(2)) departure: Mapped[str] = mapped_column(String(2)) - created_at: Mapped[datetime] = mapped_column(DateTime, server_default=text('CURRENT_TIMESTAMP')) + created_at: Mapped[datetime] = mapped_column( + DateTime, server_default=text("CURRENT_TIMESTAMP") + ) - pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_transport_means') \ No newline at end of file + pedimento: Mapped["Pedimentos"] = relationship( + "Pedimentos", back_populates="pedimento_transport_means" + ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimento_validation.py b/backend/api/v1/modules/a76/pedmientos/models/pedimento_validation.py index d0cf0229..bd7239a4 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimento_validation.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimento_validation.py @@ -1,5 +1,14 @@ from typing import TYPE_CHECKING -from sqlalchemy import DateTime, ForeignKeyConstraint, Integer, PrimaryKeyConstraint, String, UniqueConstraint, func, text +from sqlalchemy import ( + DateTime, + ForeignKeyConstraint, + Integer, + PrimaryKeyConstraint, + String, + UniqueConstraint, + func, + text, +) from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm.base import Mapped from datetime import datetime @@ -8,35 +17,50 @@ from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos + class PedimentoValidation(Base): - __tablename__ = 'pedimento_validation' #PedimentoValidacion + __tablename__ = "pedimento_validation" # PedimentoValidacion __table_args__ = ( - PrimaryKeyConstraint('id', name='pedimento_validation_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), - ForeignKeyConstraint(['pedimento_id'], ['a76.pedimentos.id'], ondelete='CASCADE', name='fk_pedimento_validation'), - UniqueConstraint('tenant_id', 'company_id', 'pedimento_id', name='pedimento_validation_pedimento_id_key'), - {'schema': 'a76'} + PrimaryKeyConstraint("id", name="pedimento_validation_pkey"), + ForeignKeyConstraint(["tenant_id"], ["a76.tenants.id"]), + ForeignKeyConstraint( + ["pedimento_id"], + ["a76.pedimentos.id"], + ondelete="CASCADE", + name="fk_pedimento_validation", + ), + UniqueConstraint( + "tenant_id", + "company_id", + "pedimento_id", + name="pedimento_validation_pedimento_id_key", + ), + {"schema": "a76"}, ) id: Mapped[int] = mapped_column(Integer) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) - - validator: Mapped[str] = mapped_column(String(3)) #validador - validation_ack: Mapped[str] = mapped_column(String(8)) #acuse_validacion - pre_ack: Mapped[str] = mapped_column(String(8)) #acuse_previo - line_signature: Mapped[str] = mapped_column(String(50)) #firma_linea_captura - electronic_signature: Mapped[str] = mapped_column(String(999)) #firma_electronica - certificate_number: Mapped[str] = mapped_column(String(99)) #numero_certificado + pedimento_id: Mapped[int] = mapped_column(Integer, nullable=False) + + validator: Mapped[str] = mapped_column(String(3)) # validador + validation_ack: Mapped[str] = mapped_column(String(8)) # acuse_validacion + pre_ack: Mapped[str] = mapped_column(String(8)) # acuse_previo + line_signature: Mapped[str] = mapped_column(String(50)) # firma_linea_captura + electronic_signature: Mapped[str] = mapped_column(String(999)) # firma_electronica + certificate_number: Mapped[str] = mapped_column(String(99)) # numero_certificado validator_id: Mapped[int] = mapped_column(Integer) responsible_id: Mapped[int] = mapped_column(Integer) - + # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) - updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now(), onupdate=func.now() + ) deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - - - pedimento: Mapped['Pedimentos'] = relationship('Pedimentos', back_populates='pedimento_validation') \ No newline at end of file + pedimento: Mapped["Pedimentos"] = relationship( + "Pedimentos", back_populates="pedimento_validation" + ) diff --git a/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py b/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py index 6663917b..95c6b00c 100644 --- a/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py +++ b/backend/api/v1/modules/a76/pedmientos/models/pedimentos.py @@ -1,50 +1,110 @@ from decimal import Decimal from typing import TYPE_CHECKING, Optional -from sqlalchemy import DateTime, ForeignKeyConstraint, Index, Integer, Numeric, PrimaryKeyConstraint, String, UniqueConstraint, func, text +from sqlalchemy import ( + DateTime, + ForeignKeyConstraint, + Index, + Integer, + Numeric, + PrimaryKeyConstraint, + String, + UniqueConstraint, + func, + text, +) from sqlalchemy.orm import Mapped, mapped_column, relationship from sqlalchemy.orm.base import Mapped from datetime import datetime from enum import IntEnum from core.database import Base -if TYPE_CHECKING: - from api.v1.modules.a76.pedmientos.models.pedimento_config_additional import PedimentoConfigAdditional - from api.v1.modules.a76.pedmientos.models.pedimento_config_calculations import PedimentoConfigCalculations - from api.v1.modules.a76.pedmientos.models.pedimento_config_parameters import PedimentoConfigParameters - from api.v1.modules.a76.pedmientos.models.pedimento_config_surcharges import PedimentoConfigSurcharges - from api.v1.modules.a76.pedmientos.models.pedimento_config_update_rectification import PedimentoConfigUpdateRectification - from api.v1.modules.a76.pedmientos.models.pedimento_config_updates import PedimentoConfigUpdates - from api.v1.modules.a76.pedmientos.models.pedimento_customs_offices import PedimentoCustomsOffices +if TYPE_CHECKING: + from api.v1.modules.a76.pedmientos.models.pedimento_config_additional import ( + PedimentoConfigAdditional, + ) + from api.v1.modules.a76.pedmientos.models.pedimento_config_calculations import ( + PedimentoConfigCalculations, + ) + from api.v1.modules.a76.pedmientos.models.pedimento_config_parameters import ( + PedimentoConfigParameters, + ) + from api.v1.modules.a76.pedmientos.models.pedimento_config_surcharges import ( + PedimentoConfigSurcharges, + ) + from api.v1.modules.a76.pedmientos.models.pedimento_config_update_rectification import ( + PedimentoConfigUpdateRectification, + ) + from api.v1.modules.a76.pedmientos.models.pedimento_config_updates import ( + PedimentoConfigUpdates, + ) + from api.v1.modules.a76.pedmientos.models.pedimento_customs_offices import ( + PedimentoCustomsOffices, + ) from api.v1.modules.a76.pedmientos.models.pedimento_dates import PedimentoDates - from api.v1.modules.a76.pedmientos.models.pedimento_decrementables import PedimentoDecrementables - from api.v1.modules.a76.pedmientos.models.pedimento_incrementables import PedimentoIncrementables + from api.v1.modules.a76.pedmientos.models.pedimento_decrementables import ( + PedimentoDecrementables, + ) + from api.v1.modules.a76.pedmientos.models.pedimento_incrementables import ( + PedimentoIncrementables, + ) from api.v1.modules.a76.pedmientos.models.pedimento_indexes import PedimentoIndexes - from api.v1.modules.a76.pedmientos.models.pedimento_payments import PedimentoPayments - from api.v1.modules.a76.pedmientos.models.pedimento_rectification_destination import PedimentoRectificationDestination - from api.v1.modules.a76.pedmientos.models.pedimento_rectification_origin import PedimentoRectificationOrigin - from api.v1.modules.a76.pedmientos.models.pedimento_transport_means import PedimentoTransportMeans - from api.v1.modules.a76.pedmientos.models.pedimento_validation import PedimentoValidation + from api.v1.modules.a76.pedmientos.models.pedimento_payments import ( + PedimentoPayments, + ) + from api.v1.modules.a76.pedmientos.models.pedimento_rectification_destination import ( + PedimentoRectificationDestination, + ) + from api.v1.modules.a76.pedmientos.models.pedimento_rectification_origin import ( + PedimentoRectificationOrigin, + ) + from api.v1.modules.a76.pedmientos.models.pedimento_transport_means import ( + PedimentoTransportMeans, + ) + from api.v1.modules.a76.pedmientos.models.pedimento_validation import ( + PedimentoValidation, + ) + class Pedimentos(Base): - __tablename__ = 'pedimentos' + __tablename__ = "pedimentos" __table_args__ = ( - PrimaryKeyConstraint('id', name='pedimentos_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_pedimentos_tenant'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_pedimentos_company'), - ForeignKeyConstraint(['client_id'], ['a76.client_provider.id'], name='fk_pedimentos_client'), - ForeignKeyConstraint(['regime'], ['public.pedimento_regimens.code'], name='fk_pedimentos_regime'), - ForeignKeyConstraint(['pedimento_code'], ['public.pedimento_codes.code'], name='fk_pedimentos_code'), - UniqueConstraint('tenant_id', 'company_id', 'year', 'customs_office', 'license', 'pedimento_number', name='pedimentos_unique_key'), - Index('idx_pedimentos_client_id', 'client_id'), - Index('idx_pedimentos_created_at', 'created_at'), - Index('idx_pedimentos_status', 'status'), - {'schema': 'a76'} + PrimaryKeyConstraint("id", name="pedimentos_pkey"), + ForeignKeyConstraint( + ["tenant_id"], ["a76.tenants.id"], name="fk_pedimentos_tenant" + ), + ForeignKeyConstraint( + ["company_id"], ["a76.company.id"], name="fk_pedimentos_company" + ), + ForeignKeyConstraint( + ["client_id"], ["a76.client_provider.id"], name="fk_pedimentos_client" + ), + ForeignKeyConstraint( + ["regime"], ["public.pedimento_regimens.code"], name="fk_pedimentos_regime" + ), + ForeignKeyConstraint( + ["pedimento_code"], + ["public.pedimento_codes.code"], + name="fk_pedimentos_code", + ), + UniqueConstraint( + "tenant_id", + "company_id", + "year", + "customs_office", + "license", + "pedimento_number", + name="pedimentos_unique_key", + ), + Index("idx_pedimentos_client_id", "client_id"), + Index("idx_pedimentos_created_at", "created_at"), + Index("idx_pedimentos_status", "status"), + {"schema": "a76"}, ) id: Mapped[int] = mapped_column(Integer) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - + year: Mapped[str] = mapped_column(String(2)) customs_office: Mapped[str] = mapped_column(String(2)) license: Mapped[str] = mapped_column(String(4)) @@ -59,26 +119,69 @@ class Pedimentos(Base): paid_price: Mapped[Optional[Decimal]] = mapped_column(Numeric(17, 6)) gross_weight: Mapped[Optional[Decimal]] = mapped_column(Numeric(19, 3)) exchange_rate: Mapped[Optional[Decimal]] = mapped_column(Numeric(9, 5)) - + # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) - updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now(), onupdate=func.now() + ) deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - pedimento_config_additional: Mapped['PedimentoConfigAdditional'] = relationship('PedimentoConfigAdditional', uselist=False, back_populates='pedimento') - pedimento_config_calculations: Mapped['PedimentoConfigCalculations'] = relationship('PedimentoConfigCalculations', uselist=False, back_populates='pedimento') - pedimento_config_parameters: Mapped['PedimentoConfigParameters'] = relationship('PedimentoConfigParameters', uselist=False, back_populates='pedimento') - pedimento_config_surcharges: Mapped['PedimentoConfigSurcharges'] = relationship('PedimentoConfigSurcharges', uselist=False, back_populates='pedimento') - pedimento_config_update_rectification: Mapped['PedimentoConfigUpdateRectification'] = relationship('PedimentoConfigUpdateRectification', uselist=False, back_populates='pedimento') - pedimento_config_updates: Mapped['PedimentoConfigUpdates'] = relationship('PedimentoConfigUpdates', uselist=False, back_populates='pedimento') - pedimento_customs_offices: Mapped['PedimentoCustomsOffices'] = relationship('PedimentoCustomsOffices', uselist=False, back_populates='pedimento') - pedimento_dates: Mapped['PedimentoDates'] = relationship('PedimentoDates', uselist=False, back_populates='pedimento') - pedimento_decrementables: Mapped['PedimentoDecrementables'] = relationship('PedimentoDecrementables', uselist=False, back_populates='pedimento') - pedimento_incrementables: Mapped['PedimentoIncrementables'] = relationship('PedimentoIncrementables', uselist=False, back_populates='pedimento') - pedimento_indexes: Mapped['PedimentoIndexes'] = relationship('PedimentoIndexes', uselist=False, back_populates='pedimento') - pedimento_payments: Mapped['PedimentoPayments'] = relationship('PedimentoPayments', uselist=False, back_populates='pedimento') - pedimento_rectification_destination: Mapped['PedimentoRectificationDestination'] = relationship('PedimentoRectificationDestination', uselist=False, back_populates='pedimento') - pedimento_rectification_origin: Mapped['PedimentoRectificationOrigin'] = relationship('PedimentoRectificationOrigin', uselist=False, back_populates='pedimento') - pedimento_transport_means: Mapped['PedimentoTransportMeans'] = relationship('PedimentoTransportMeans', uselist=False, back_populates='pedimento') - pedimento_validation: Mapped['PedimentoValidation'] = relationship('PedimentoValidation', uselist=False, back_populates='pedimento') - + pedimento_config_additional: Mapped["PedimentoConfigAdditional"] = relationship( + "PedimentoConfigAdditional", uselist=False, back_populates="pedimento" + ) + pedimento_config_calculations: Mapped["PedimentoConfigCalculations"] = relationship( + "PedimentoConfigCalculations", uselist=False, back_populates="pedimento" + ) + pedimento_config_parameters: Mapped["PedimentoConfigParameters"] = relationship( + "PedimentoConfigParameters", uselist=False, back_populates="pedimento" + ) + pedimento_config_surcharges: Mapped["PedimentoConfigSurcharges"] = relationship( + "PedimentoConfigSurcharges", uselist=False, back_populates="pedimento" + ) + pedimento_config_update_rectification: Mapped[ + "PedimentoConfigUpdateRectification" + ] = relationship( + "PedimentoConfigUpdateRectification", uselist=False, back_populates="pedimento" + ) + pedimento_config_updates: Mapped["PedimentoConfigUpdates"] = relationship( + "PedimentoConfigUpdates", uselist=False, back_populates="pedimento" + ) + pedimento_customs_offices: Mapped["PedimentoCustomsOffices"] = relationship( + "PedimentoCustomsOffices", uselist=False, back_populates="pedimento" + ) + pedimento_dates: Mapped["PedimentoDates"] = relationship( + "PedimentoDates", uselist=False, back_populates="pedimento" + ) + pedimento_decrementables: Mapped["PedimentoDecrementables"] = relationship( + "PedimentoDecrementables", uselist=False, back_populates="pedimento" + ) + pedimento_incrementables: Mapped["PedimentoIncrementables"] = relationship( + "PedimentoIncrementables", uselist=False, back_populates="pedimento" + ) + pedimento_indexes: Mapped["PedimentoIndexes"] = relationship( + "PedimentoIndexes", uselist=False, back_populates="pedimento" + ) + pedimento_payments: Mapped["PedimentoPayments"] = relationship( + "PedimentoPayments", uselist=False, back_populates="pedimento" + ) + pedimento_rectification_destination: Mapped["PedimentoRectificationDestination"] = ( + relationship( + "PedimentoRectificationDestination", + uselist=False, + back_populates="pedimento", + ) + ) + pedimento_rectification_origin: Mapped["PedimentoRectificationOrigin"] = ( + relationship( + "PedimentoRectificationOrigin", uselist=False, back_populates="pedimento" + ) + ) + pedimento_transport_means: Mapped["PedimentoTransportMeans"] = relationship( + "PedimentoTransportMeans", uselist=False, back_populates="pedimento" + ) + pedimento_validation: Mapped["PedimentoValidation"] = relationship( + "PedimentoValidation", uselist=False, back_populates="pedimento" + ) diff --git a/backend/api/v1/modules/a76/pedmientos/router.py b/backend/api/v1/modules/a76/pedmientos/router.py index 07084d4f..f294cba4 100644 --- a/backend/api/v1/modules/a76/pedmientos/router.py +++ b/backend/api/v1/modules/a76/pedmientos/router.py @@ -1,39 +1,119 @@ from fastapi import APIRouter -from .routes.pedimento_config_additional import router as pedimento_config_additional_router -from .routes.pedimento_config_calculations import router as pedimento_config_calculations_router -from .routes.pedimento_config_parameters import router as pedimento_config_parameters_router -from .routes.pedimento_config_surcharges import router as pedimento_config_surcharges_router -from .routes.pedimento_config_update_rectification import router as pedimento_config_update_rectification_router +from .routes.pedimento_config_additional import ( + router as pedimento_config_additional_router, +) +from .routes.pedimento_config_calculations import ( + router as pedimento_config_calculations_router, +) +from .routes.pedimento_config_parameters import ( + router as pedimento_config_parameters_router, +) +from .routes.pedimento_config_surcharges import ( + router as pedimento_config_surcharges_router, +) +from .routes.pedimento_config_update_rectification import ( + router as pedimento_config_update_rectification_router, +) from .routes.pedimento_config_updates import router as pedimento_config_updates_router from .routes.pedimento_customs_offices import router as pedimento_customs_offices_router from .routes.pedimento_dates import router as pedimento_dates_router from .routes.pedimento_decrementables import router as pedimento_decrementables_router -from .routes.pedimento_incrementables import router as pedimento_incrementables_router +from .routes.pedimento_incrementables import router as pedimento_incrementables_router from .routes.pedimento_indexes import router as pedimento_indexes_router from .routes.pedimento_payments import router as pedimento_payments_router -from .routes.pedimento_rectification_destination import router as pedimento_rectification_destination_router -from .routes.pedimento_rectification_origin import router as pedimento_rectification_origin_router +from .routes.pedimento_rectification_destination import ( + router as pedimento_rectification_destination_router, +) +from .routes.pedimento_rectification_origin import ( + router as pedimento_rectification_origin_router, +) from .routes.pedimento_transport_means import router as pedimento_transport_means_router from .routes.pedimento_validation import router as pedimento_validation_router from .routes.pedimentos import router as pedimentos_router router = APIRouter() -router.include_router(pedimento_config_additional_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_config_additional"]) -router.include_router(pedimento_config_calculations_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_config_calculations"]) -router.include_router(pedimento_config_parameters_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_config_parameters"]) -router.include_router(pedimento_config_surcharges_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_config_surcharges"]) -router.include_router(pedimento_config_update_rectification_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_config_update_rectification"]) -router.include_router(pedimento_config_updates_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_config_updates"]) -router.include_router(pedimento_customs_offices_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_customs_offices"]) -router.include_router(pedimento_dates_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_dates"]) -router.include_router(pedimento_decrementables_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_decrementables"]) -router.include_router(pedimento_incrementables_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_incrementables"]) -router.include_router(pedimento_indexes_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_indexes"]) -router.include_router(pedimento_payments_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_payments"]) -router.include_router(pedimento_rectification_destination_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_rectification_destination"]) -router.include_router(pedimento_rectification_origin_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_rectification_origin"]) -router.include_router(pedimento_transport_means_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_transport_means"]) -router.include_router(pedimento_validation_router, prefix="/pedimentos", tags=["a76 / pedimentos / pedimento_validation"]) -router.include_router(pedimentos_router, prefix="/pedimentos", tags=["a76 / pedimentos"]) \ No newline at end of file +router.include_router( + pedimento_config_additional_router, + prefix="/pedimentos", + tags=["a76 / pedimentos / pedimento_config_additional"], +) +router.include_router( + pedimento_config_calculations_router, + prefix="/pedimentos", + tags=["a76 / pedimentos / pedimento_config_calculations"], +) +router.include_router( + pedimento_config_parameters_router, + prefix="/pedimentos", + tags=["a76 / pedimentos / pedimento_config_parameters"], +) +router.include_router( + pedimento_config_surcharges_router, + prefix="/pedimentos", + tags=["a76 / pedimentos / pedimento_config_surcharges"], +) +router.include_router( + pedimento_config_update_rectification_router, + prefix="/pedimentos", + tags=["a76 / pedimentos / pedimento_config_update_rectification"], +) +router.include_router( + pedimento_config_updates_router, + prefix="/pedimentos", + tags=["a76 / pedimentos / pedimento_config_updates"], +) +router.include_router( + pedimento_customs_offices_router, + prefix="/pedimentos", + tags=["a76 / pedimentos / pedimento_customs_offices"], +) +router.include_router( + pedimento_dates_router, + prefix="/pedimentos", + tags=["a76 / pedimentos / pedimento_dates"], +) +router.include_router( + pedimento_decrementables_router, + prefix="/pedimentos", + tags=["a76 / pedimentos / pedimento_decrementables"], +) +router.include_router( + pedimento_incrementables_router, + prefix="/pedimentos", + tags=["a76 / pedimentos / pedimento_incrementables"], +) +router.include_router( + pedimento_indexes_router, + prefix="/pedimentos", + tags=["a76 / pedimentos / pedimento_indexes"], +) +router.include_router( + pedimento_payments_router, + prefix="/pedimentos", + tags=["a76 / pedimentos / pedimento_payments"], +) +router.include_router( + pedimento_rectification_destination_router, + prefix="/pedimentos", + tags=["a76 / pedimentos / pedimento_rectification_destination"], +) +router.include_router( + pedimento_rectification_origin_router, + prefix="/pedimentos", + tags=["a76 / pedimentos / pedimento_rectification_origin"], +) +router.include_router( + pedimento_transport_means_router, + prefix="/pedimentos", + tags=["a76 / pedimentos / pedimento_transport_means"], +) +router.include_router( + pedimento_validation_router, + prefix="/pedimentos", + tags=["a76 / pedimentos / pedimento_validation"], +) +router.include_router( + pedimentos_router, prefix="/pedimentos", tags=["a76 / pedimentos"] +) diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_additional.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_additional.py index e9589c93..d882663d 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_additional.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_additional.py @@ -1,16 +1,18 @@ """ Routes for PedimentoConfigAdditional CRUD operations """ + +from typing import Dict, Any from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import validate_access_to_resource +from core.security import validate_access_to_resource, get_current_user from ..services.pedimento_config_additional import PedimentoConfigAdditionalService from ..dtos.pedimento_config_additional import ( PedimentoConfigAdditionalCreate, PedimentoConfigAdditionalUpdate, - PedimentoConfigAdditionalResponse + PedimentoConfigAdditionalResponse, ) @@ -22,14 +24,17 @@ async def get_config_additional( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Get config additional by pedimento ID""" - tenant_id = validate_access_to_resource(company_id) - - config = PedimentoConfigAdditionalService.get_by_pedimento_id(db, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + config = PedimentoConfigAdditionalService.get_by_pedimento_id( + db, pedimento_id, tenant_id, company_id + ) if not config: raise HTTPException(status_code=404, detail="Config additional not found") - + return config @@ -39,14 +44,15 @@ async def create_config_additional( data: PedimentoConfigAdditionalCreate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Create config additional""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + # Ensure pedimento_id and company_id match if data.pedimento_id != pedimento_id: raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - + config = PedimentoConfigAdditionalService.create(db, data, tenant_id, company_id) return config @@ -57,14 +63,17 @@ async def update_config_additional( data: PedimentoConfigAdditionalUpdate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Update config additional""" - tenant_id = validate_access_to_resource(company_id) - - config = PedimentoConfigAdditionalService.update(db, pedimento_id, tenant_id, company_id, data) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + config = PedimentoConfigAdditionalService.update( + db, pedimento_id, tenant_id, company_id, data + ) if not config: raise HTTPException(status_code=404, detail="Config additional not found") - + return config @@ -73,12 +82,15 @@ async def delete_config_additional( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Delete config additional""" - tenant_id = validate_access_to_resource(company_id) - - success = PedimentoConfigAdditionalService.delete(db, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + success = PedimentoConfigAdditionalService.delete( + db, pedimento_id, tenant_id, company_id + ) if not success: raise HTTPException(status_code=404, detail="Config additional not found") - + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_calculations.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_calculations.py index 62bda5dd..33af3f06 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_calculations.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_calculations.py @@ -1,16 +1,18 @@ """ Routes for PedimentoConfigCalculations CRUD operations """ + +from typing import Dict, Any from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import validate_access_to_resource +from core.security import validate_access_to_resource, get_current_user from ..services.pedimento_config_calculations import PedimentoConfigCalculationsService from ..dtos.pedimento_config_calculations import ( PedimentoConfigCalculationsCreate, PedimentoConfigCalculationsUpdate, - PedimentoConfigCalculationsResponse + PedimentoConfigCalculationsResponse, ) @@ -22,14 +24,17 @@ async def get_config_calculations( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Get config calculations by pedimento ID""" - tenant_id = validate_access_to_resource(company_id) - - config = PedimentoConfigCalculationsService.get_by_pedimento_id(db, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + config = PedimentoConfigCalculationsService.get_by_pedimento_id( + db, pedimento_id, tenant_id, company_id + ) if not config: raise HTTPException(status_code=404, detail="Config calculations not found") - + return config @@ -39,14 +44,15 @@ async def create_config_calculations( data: PedimentoConfigCalculationsCreate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Create config calculations""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + # Ensure pedimento_id matches if data.pedimento_id != pedimento_id: raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - + config = PedimentoConfigCalculationsService.create(db, data, tenant_id, company_id) return config @@ -57,14 +63,17 @@ async def update_config_calculations( data: PedimentoConfigCalculationsUpdate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Update config calculations""" - tenant_id = validate_access_to_resource(company_id) - - config = PedimentoConfigCalculationsService.update(db, pedimento_id, tenant_id, company_id, data) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + config = PedimentoConfigCalculationsService.update( + db, pedimento_id, tenant_id, company_id, data + ) if not config: raise HTTPException(status_code=404, detail="Config calculations not found") - + return config @@ -73,12 +82,13 @@ async def delete_config_calculations( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Delete config calculations""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + success = PedimentoConfigCalculationsService.delete(db, pedimento_id, company_id) if not success: raise HTTPException(status_code=404, detail="Config calculations not found") - + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_parameters.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_parameters.py index 147b7f45..9c3e5743 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_parameters.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_parameters.py @@ -1,16 +1,18 @@ """ Routes for PedimentoConfigParameters CRUD operations """ + +from typing import Dict, Any from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import validate_access_to_resource +from core.security import validate_access_to_resource, get_current_user from ..services.pedimento_config_parameters import PedimentoConfigParametersService from ..dtos.pedimento_config_parameters import ( PedimentoConfigParametersCreate, PedimentoConfigParametersUpdate, - PedimentoConfigParametersResponse + PedimentoConfigParametersResponse, ) @@ -22,14 +24,17 @@ async def get_config_parameters( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Get config parameters by pedimento ID""" - tenant_id = validate_access_to_resource(company_id) - - config = PedimentoConfigParametersService.get_by_pedimento_id(db, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + config = PedimentoConfigParametersService.get_by_pedimento_id( + db, pedimento_id, tenant_id, company_id + ) if not config: raise HTTPException(status_code=404, detail="Config parameters not found") - + return config @@ -39,14 +44,15 @@ async def create_config_parameters( data: PedimentoConfigParametersCreate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Create config parameters""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + # Ensure pedimento_id matches if data.pedimento_id != pedimento_id: raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - + config = PedimentoConfigParametersService.create(db, data, tenant_id, company_id) return config @@ -57,14 +63,17 @@ async def update_config_parameters( data: PedimentoConfigParametersUpdate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Update config parameters""" - tenant_id = validate_access_to_resource(company_id) - - config = PedimentoConfigParametersService.update(db, pedimento_id, tenant_id, company_id, data) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + config = PedimentoConfigParametersService.update( + db, pedimento_id, tenant_id, company_id, data + ) if not config: raise HTTPException(status_code=404, detail="Config parameters not found") - + return config @@ -73,12 +82,15 @@ async def delete_config_parameters( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Delete config parameters""" - tenant_id = validate_access_to_resource(company_id) - - success = PedimentoConfigParametersService.delete(db, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + success = PedimentoConfigParametersService.delete( + db, pedimento_id, tenant_id, company_id + ) if not success: raise HTTPException(status_code=404, detail="Config parameters not found") - + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_surcharges.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_surcharges.py index ffcb5109..0fd6bea6 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_surcharges.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_surcharges.py @@ -1,16 +1,18 @@ """ Routes for PedimentoConfigSurcharges CRUD operations """ + +from typing import Dict, Any from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import validate_access_to_resource +from core.security import validate_access_to_resource, get_current_user from ..services.pedimento_config_surcharges import PedimentoConfigSurchargesService from ..dtos.pedimento_config_surcharges import ( PedimentoConfigSurchargesCreate, PedimentoConfigSurchargesUpdate, - PedimentoConfigSurchargesResponse + PedimentoConfigSurchargesResponse, ) @@ -22,14 +24,17 @@ async def get_config_surcharges( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Get config surcharges by pedimento ID""" - tenant_id = validate_access_to_resource(company_id) - - config = PedimentoConfigSurchargesService.get_by_pedimento_id(db, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + config = PedimentoConfigSurchargesService.get_by_pedimento_id( + db, pedimento_id, tenant_id, company_id + ) if not config: raise HTTPException(status_code=404, detail="Config surcharges not found") - + return config @@ -39,14 +44,15 @@ async def create_config_surcharges( data: PedimentoConfigSurchargesCreate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Create config surcharges""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + # Ensure pedimento_id matches if data.pedimento_id != pedimento_id: raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - + config = PedimentoConfigSurchargesService.create(db, data, tenant_id, company_id) return config @@ -57,14 +63,17 @@ async def update_config_surcharges( data: PedimentoConfigSurchargesUpdate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Update config surcharges""" - tenant_id = validate_access_to_resource(company_id) - - config = PedimentoConfigSurchargesService.update(db, pedimento_id, tenant_id, company_id, data) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + config = PedimentoConfigSurchargesService.update( + db, pedimento_id, tenant_id, company_id, data + ) if not config: raise HTTPException(status_code=404, detail="Config surcharges not found") - + return config @@ -73,12 +82,15 @@ async def delete_config_surcharges( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Delete config surcharges""" - tenant_id = validate_access_to_resource(company_id) - - success = PedimentoConfigSurchargesService.delete(db, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + success = PedimentoConfigSurchargesService.delete( + db, pedimento_id, tenant_id, company_id + ) if not success: raise HTTPException(status_code=404, detail="Config surcharges not found") - + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_update_rectification.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_update_rectification.py index 248e186b..8343ce16 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_update_rectification.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_update_rectification.py @@ -1,16 +1,20 @@ """ Routes for PedimentoConfigUpdateRectification CRUD operations """ + +from typing import Dict, Any from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import validate_access_to_resource +from core.security import validate_access_to_resource, get_current_user -from ..services.pedimento_config_update_rectification import PedimentoConfigUpdateRectificationService +from ..services.pedimento_config_update_rectification import ( + PedimentoConfigUpdateRectificationService, +) from ..dtos.pedimento_config_update_rectification import ( PedimentoConfigUpdateRectificationCreate, PedimentoConfigUpdateRectificationUpdate, - PedimentoConfigUpdateRectificationResponse + PedimentoConfigUpdateRectificationResponse, ) @@ -22,32 +26,42 @@ async def get_config_update_rectification( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Get config update rectification by pedimento ID""" - tenant_id = validate_access_to_resource(company_id) - - config = PedimentoConfigUpdateRectificationService.get_by_pedimento_id(db, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + config = PedimentoConfigUpdateRectificationService.get_by_pedimento_id( + db, pedimento_id, tenant_id, company_id + ) if not config: - raise HTTPException(status_code=404, detail="Config update rectification not found") - + raise HTTPException( + status_code=404, detail="Config update rectification not found" + ) + return config -@router.post("/", response_model=PedimentoConfigUpdateRectificationResponse, status_code=201) +@router.post( + "/", response_model=PedimentoConfigUpdateRectificationResponse, status_code=201 +) async def create_config_update_rectification( pedimento_id: int, data: PedimentoConfigUpdateRectificationCreate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Create config update rectification""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + # Ensure pedimento_id matches if data.pedimento_id != pedimento_id: raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - - config = PedimentoConfigUpdateRectificationService.create(db, data, tenant_id, company_id) + + config = PedimentoConfigUpdateRectificationService.create( + db, data, tenant_id, company_id + ) return config @@ -57,14 +71,19 @@ async def update_config_update_rectification( data: PedimentoConfigUpdateRectificationUpdate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Update config update rectification""" - tenant_id = validate_access_to_resource(company_id) - - config = PedimentoConfigUpdateRectificationService.update(db, pedimento_id, tenant_id, company_id, data) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + config = PedimentoConfigUpdateRectificationService.update( + db, pedimento_id, tenant_id, company_id, data + ) if not config: - raise HTTPException(status_code=404, detail="Config update rectification not found") - + raise HTTPException( + status_code=404, detail="Config update rectification not found" + ) + return config @@ -73,12 +92,17 @@ async def delete_config_update_rectification( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Delete config update rectification""" - tenant_id = validate_access_to_resource(company_id) - - success = PedimentoConfigUpdateRectificationService.delete(db, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + success = PedimentoConfigUpdateRectificationService.delete( + db, pedimento_id, tenant_id, company_id + ) if not success: - raise HTTPException(status_code=404, detail="Config update rectification not found") - + raise HTTPException( + status_code=404, detail="Config update rectification not found" + ) + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_updates.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_updates.py index e1616cbb..e46c25b8 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_updates.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_config_updates.py @@ -1,16 +1,18 @@ """ Routes for PedimentoConfigUpdates CRUD operations """ + +from typing import Dict, Any from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import validate_access_to_resource +from core.security import validate_access_to_resource, get_current_user from ..services.pedimento_config_updates import PedimentoConfigUpdatesService from ..dtos.pedimento_config_updates import ( PedimentoConfigUpdatesCreate, PedimentoConfigUpdatesUpdate, - PedimentoConfigUpdatesResponse + PedimentoConfigUpdatesResponse, ) @@ -22,14 +24,17 @@ async def get_config_updates( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Get config updates by pedimento ID""" - tenant_id = validate_access_to_resource(company_id) - - config = PedimentoConfigUpdatesService.get_by_pedimento_id(db, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + config = PedimentoConfigUpdatesService.get_by_pedimento_id( + db, pedimento_id, tenant_id, company_id + ) if not config: raise HTTPException(status_code=404, detail="Config updates not found") - + return config @@ -38,15 +43,16 @@ async def create_config_updates( pedimento_id: int, data: PedimentoConfigUpdatesCreate, company_id: int = Query(..., description="Company ID"), - db: Session = Depends(get_core_db) + db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Create config updates""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + # Ensure pedimento_id matches if data.pedimento_id != pedimento_id: raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - + config = PedimentoConfigUpdatesService.create(db, data, tenant_id, company_id) return config @@ -57,14 +63,17 @@ async def update_config_updates( data: PedimentoConfigUpdatesUpdate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Update config updates""" - tenant_id = validate_access_to_resource(company_id) - - config = PedimentoConfigUpdatesService.update(db, pedimento_id, tenant_id, company_id, data) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + config = PedimentoConfigUpdatesService.update( + db, pedimento_id, tenant_id, company_id, data + ) if not config: raise HTTPException(status_code=404, detail="Config updates not found") - + return config @@ -73,12 +82,15 @@ async def delete_config_updates( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Delete config updates""" - tenant_id = validate_access_to_resource(company_id) - - success = PedimentoConfigUpdatesService.delete(db, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + success = PedimentoConfigUpdatesService.delete( + db, pedimento_id, tenant_id, company_id + ) if not success: raise HTTPException(status_code=404, detail="Config updates not found") - + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_customs_offices.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_customs_offices.py index 3c843c84..92bb05a4 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_customs_offices.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_customs_offices.py @@ -1,16 +1,17 @@ """ Routes for PedimentoCustomsOffices CRUD operations """ + from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session -from typing import List +from typing import Dict, Any, List from core.database import get_core_db -from core.security import validate_access_to_resource +from core.security import validate_access_to_resource, get_current_user from ..services.pedimento_customs_offices import PedimentoCustomsOfficesService from ..dtos.pedimento_customs_offices import ( PedimentoCustomsOfficesCreate, PedimentoCustomsOfficesUpdate, - PedimentoCustomsOfficesResponse + PedimentoCustomsOfficesResponse, ) @@ -22,11 +23,14 @@ async def list_customs_offices( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Get all customs offices for a pedimento""" - tenant_id = validate_access_to_resource(company_id) - - offices = PedimentoCustomsOfficesService.get_by_pedimento_id(db, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + offices = PedimentoCustomsOfficesService.get_by_pedimento_id( + db, pedimento_id, tenant_id, company_id + ) return offices @@ -36,14 +40,17 @@ async def get_customs_office( office_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Get a specific customs office by ID""" - tenant_id = validate_access_to_resource(company_id) - - office = PedimentoCustomsOfficesService.get_by_id(db, office_id, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + office = PedimentoCustomsOfficesService.get_by_id( + db, office_id, pedimento_id, tenant_id, company_id + ) if not office: raise HTTPException(status_code=404, detail="Customs office not found") - + return office @@ -53,14 +60,15 @@ async def create_customs_office( data: PedimentoCustomsOfficesCreate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Create a new customs office""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + # Ensure pedimento_id matches if data.pedimento_id != pedimento_id: raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - + office = PedimentoCustomsOfficesService.create(db, data, tenant_id, company_id) return office @@ -72,14 +80,17 @@ async def update_customs_office( data: PedimentoCustomsOfficesUpdate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Update a customs office""" - tenant_id = validate_access_to_resource(company_id) - - office = PedimentoCustomsOfficesService.update(db, office_id, pedimento_id, tenant_id, company_id, data) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + office = PedimentoCustomsOfficesService.update( + db, office_id, pedimento_id, tenant_id, company_id, data + ) if not office: raise HTTPException(status_code=404, detail="Customs office not found") - + return office @@ -89,12 +100,15 @@ async def delete_customs_office( office_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Delete a customs office""" - tenant_id = validate_access_to_resource(company_id) - - success = PedimentoCustomsOfficesService.delete(db, office_id, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + success = PedimentoCustomsOfficesService.delete( + db, office_id, pedimento_id, tenant_id, company_id + ) if not success: raise HTTPException(status_code=404, detail="Customs office not found") - + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_dates.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_dates.py index 78ab8b08..9e641747 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_dates.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_dates.py @@ -1,48 +1,55 @@ """ Routes for PedimentoDates CRUD operations """ + import logging +from typing import Dict, Any from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import validate_access_to_resource +from core.security import validate_access_to_resource, get_current_user from ..services.pedimento_dates import PedimentoDatesService from ..dtos.pedimento_dates import ( PedimentoDatesCreate, PedimentoDatesUpdate, - PedimentoDatesResponse + PedimentoDatesResponse, ) router = APIRouter(prefix="/{pedimento_id}/dates") logger = logging.getLogger(__name__) + @router.get("/", response_model=PedimentoDatesResponse) async def get_dates( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Get dates by pedimento ID""" - tenant_id = validate_access_to_resource(company_id) - - dates = PedimentoDatesService.get_by_pedimento_id(db, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + dates = PedimentoDatesService.get_by_pedimento_id( + db, pedimento_id, tenant_id, company_id + ) if not dates: raise HTTPException(status_code=404, detail="Pedimento dates not found") - + return dates @router.post("/", response_model=PedimentoDatesResponse, status_code=201) -async def create_dates( +async def create_dates( data: PedimentoDatesCreate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Create pedimento dates""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + dates = PedimentoDatesService.create(db, data, tenant_id, company_id) return dates @@ -53,14 +60,15 @@ async def update_dates( data: PedimentoDatesUpdate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Update pedimento dates""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + dates = PedimentoDatesService.update(db, pedimento_id, tenant_id, company_id, data) if not dates: raise HTTPException(status_code=404, detail="Pedimento dates not found") - + return dates @@ -69,12 +77,13 @@ async def delete_dates( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Delete pedimento dates""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + success = PedimentoDatesService.delete(db, pedimento_id, tenant_id, company_id) if not success: raise HTTPException(status_code=404, detail="Pedimento dates not found") - + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_decrementables.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_decrementables.py index fa8871f6..d658c479 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_decrementables.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_decrementables.py @@ -1,17 +1,18 @@ """ Routes for PedimentoDecrementables CRUD operations """ + from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session -from typing import List +from typing import Dict, Any, List from core.database import get_core_db -from core.security import validate_access_to_resource +from core.security import validate_access_to_resource, get_current_user from ..services.pedimento_decrementables import PedimentoDecrementablesService from ..dtos.pedimento_decrementables import ( PedimentoDecrementablesCreate, PedimentoDecrementablesUpdate, - PedimentoDecrementablesResponse + PedimentoDecrementablesResponse, ) @@ -23,11 +24,14 @@ async def list_decrementables( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Get all decrementables for a pedimento""" - tenant_id = validate_access_to_resource(company_id) - - decrementables = PedimentoDecrementablesService.get_by_pedimento_id(db, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + decrementables = PedimentoDecrementablesService.get_by_pedimento_id( + db, pedimento_id, tenant_id, company_id + ) return decrementables @@ -37,14 +41,17 @@ async def get_decrementable( decrementable_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Get a specific decrementable by ID""" - tenant_id = validate_access_to_resource(company_id) - - decrementable = PedimentoDecrementablesService.get_by_id(db, decrementable_id, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + decrementable = PedimentoDecrementablesService.get_by_id( + db, decrementable_id, pedimento_id, tenant_id, company_id + ) if not decrementable: raise HTTPException(status_code=404, detail="Decrementable not found") - + return decrementable @@ -54,15 +61,18 @@ async def create_decrementable( data: PedimentoDecrementablesCreate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Create a new decrementable""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + # Ensure pedimento_id matches if data.pedimento_id != pedimento_id: raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - - decrementable = PedimentoDecrementablesService.create(db, data, tenant_id, company_id) + + decrementable = PedimentoDecrementablesService.create( + db, data, tenant_id, company_id + ) return decrementable @@ -73,14 +83,17 @@ async def update_decrementable( data: PedimentoDecrementablesUpdate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Update a decrementable""" - tenant_id = validate_access_to_resource(company_id) - - decrementable = PedimentoDecrementablesService.update(db, decrementable_id, pedimento_id, tenant_id, company_id, data) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + decrementable = PedimentoDecrementablesService.update( + db, decrementable_id, pedimento_id, tenant_id, company_id, data + ) if not decrementable: raise HTTPException(status_code=404, detail="Decrementable not found") - + return decrementable @@ -90,12 +103,15 @@ async def delete_decrementable( decrementable_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Delete a decrementable""" - tenant_id = validate_access_to_resource(company_id) - - success = PedimentoDecrementablesService.delete(db, decrementable_id, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + success = PedimentoDecrementablesService.delete( + db, decrementable_id, pedimento_id, tenant_id, company_id + ) if not success: raise HTTPException(status_code=404, detail="Decrementable not found") - + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_incrementables.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_incrementables.py index b99d317b..6fa3ecbb 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_incrementables.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_incrementables.py @@ -1,17 +1,18 @@ """ Routes for PedimentoIncrementables CRUD operations """ + from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session -from typing import List +from typing import Dict, Any, List from core.database import get_core_db -from core.security import validate_access_to_resource +from core.security import validate_access_to_resource, get_current_user from ..services.pedimento_incrementables import PedimentoIncrementablesService from ..dtos.pedimento_incrementables import ( PedimentoIncrementablesCreate, PedimentoIncrementablesUpdate, - PedimentoIncrementablesResponse + PedimentoIncrementablesResponse, ) @@ -23,11 +24,14 @@ async def list_incrementables( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Get all incrementables for a pedimento""" - tenant_id = validate_access_to_resource(company_id) - - incrementables = PedimentoIncrementablesService.get_by_pedimento_id(db, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + incrementables = PedimentoIncrementablesService.get_by_pedimento_id( + db, pedimento_id, tenant_id, company_id + ) return incrementables @@ -37,14 +41,17 @@ async def get_incrementable( incrementable_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Get a specific incrementable by ID""" - tenant_id = validate_access_to_resource(company_id) - - incrementable = PedimentoIncrementablesService.get_by_id(db, incrementable_id, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + incrementable = PedimentoIncrementablesService.get_by_id( + db, incrementable_id, pedimento_id, tenant_id, company_id + ) if not incrementable: raise HTTPException(status_code=404, detail="Incrementable not found") - + return incrementable @@ -54,15 +61,18 @@ async def create_incrementable( data: PedimentoIncrementablesCreate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Create a new incrementable""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + # Ensure pedimento_id matches if data.pedimento_id != pedimento_id: raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - - incrementable = PedimentoIncrementablesService.create(db, data, tenant_id, company_id) + + incrementable = PedimentoIncrementablesService.create( + db, data, tenant_id, company_id + ) return incrementable @@ -73,14 +83,17 @@ async def update_incrementable( data: PedimentoIncrementablesUpdate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Update an incrementable""" - tenant_id = validate_access_to_resource(company_id) - - incrementable = PedimentoIncrementablesService.update(db, incrementable_id, pedimento_id, tenant_id, company_id, data) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + incrementable = PedimentoIncrementablesService.update( + db, incrementable_id, pedimento_id, tenant_id, company_id, data + ) if not incrementable: raise HTTPException(status_code=404, detail="Incrementable not found") - + return incrementable @@ -90,12 +103,15 @@ async def delete_incrementable( incrementable_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Delete an incrementable""" - tenant_id = validate_access_to_resource(company_id) - - success = PedimentoIncrementablesService.delete(db, incrementable_id, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + success = PedimentoIncrementablesService.delete( + db, incrementable_id, pedimento_id, tenant_id, company_id + ) if not success: raise HTTPException(status_code=404, detail="Incrementable not found") - + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_indexes.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_indexes.py index bc89cf02..8e1a3030 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_indexes.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_indexes.py @@ -1,16 +1,18 @@ """ Routes for PedimentoIndexes CRUD operations """ + +from typing import Dict, Any from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import validate_access_to_resource +from core.security import validate_access_to_resource, get_current_user from ..services.pedimento_indexes import PedimentoIndexesService from ..dtos.pedimento_indexes import ( PedimentoIndexesCreate, PedimentoIndexesUpdate, - PedimentoIndexesResponse + PedimentoIndexesResponse, ) @@ -22,14 +24,17 @@ async def get_indexes( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Get indexes by pedimento ID""" - tenant_id = validate_access_to_resource(company_id) - - indexes = PedimentoIndexesService.get_by_pedimento_id(db, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + indexes = PedimentoIndexesService.get_by_pedimento_id( + db, pedimento_id, tenant_id, company_id + ) if not indexes: raise HTTPException(status_code=404, detail="Pedimento indexes not found") - + return indexes @@ -39,14 +44,15 @@ async def create_indexes( data: PedimentoIndexesCreate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Create pedimento indexes""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + # Ensure pedimento_id matches if data.pedimento_id != pedimento_id: raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - + indexes = PedimentoIndexesService.create(db, data, tenant_id, company_id) return indexes @@ -57,14 +63,17 @@ async def update_indexes( data: PedimentoIndexesUpdate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Update pedimento indexes""" - tenant_id = validate_access_to_resource(company_id) - - indexes = PedimentoIndexesService.update(db, pedimento_id, tenant_id, company_id, data) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + indexes = PedimentoIndexesService.update( + db, pedimento_id, tenant_id, company_id, data + ) if not indexes: raise HTTPException(status_code=404, detail="Pedimento indexes not found") - + return indexes @@ -73,12 +82,13 @@ async def delete_indexes( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Delete pedimento indexes""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + success = PedimentoIndexesService.delete(db, pedimento_id, tenant_id, company_id) if not success: raise HTTPException(status_code=404, detail="Pedimento indexes not found") - + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_payments.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_payments.py index d553d6d3..fe863d7f 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_payments.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_payments.py @@ -1,17 +1,18 @@ """ Routes for PedimentoPayments CRUD operations """ + from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session -from typing import List +from typing import Dict, Any, List from core.database import get_core_db -from core.security import validate_access_to_resource +from core.security import validate_access_to_resource, get_current_user from ..services.pedimento_payments import PedimentoPaymentsService from ..dtos.pedimento_payments import ( PedimentoPaymentsCreate, PedimentoPaymentsUpdate, - PedimentoPaymentsResponse + PedimentoPaymentsResponse, ) @@ -23,11 +24,14 @@ async def list_payments( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Get all payments for a pedimento""" - tenant_id = validate_access_to_resource(company_id) - - payments = PedimentoPaymentsService.get_by_pedimento_id(db, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + payments = PedimentoPaymentsService.get_by_pedimento_id( + db, pedimento_id, tenant_id, company_id + ) return payments @@ -37,14 +41,17 @@ async def get_payment( payment_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Get a specific payment by ID""" - tenant_id = validate_access_to_resource(company_id) - - payment = PedimentoPaymentsService.get_by_id(db, payment_id, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + payment = PedimentoPaymentsService.get_by_id( + db, payment_id, pedimento_id, tenant_id, company_id + ) if not payment: raise HTTPException(status_code=404, detail="Payment not found") - + return payment @@ -54,14 +61,15 @@ async def create_payment( data: PedimentoPaymentsCreate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Create a new payment""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + # Ensure pedimento_id matches if data.pedimento_id != pedimento_id: raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - + payment = PedimentoPaymentsService.create(db, data, tenant_id, company_id) return payment @@ -73,14 +81,17 @@ async def update_payment( data: PedimentoPaymentsUpdate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Update a payment""" - tenant_id = validate_access_to_resource(company_id) - - payment = PedimentoPaymentsService.update(db, payment_id, pedimento_id, tenant_id, company_id, data) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + payment = PedimentoPaymentsService.update( + db, payment_id, pedimento_id, tenant_id, company_id, data + ) if not payment: raise HTTPException(status_code=404, detail="Payment not found") - + return payment @@ -90,12 +101,15 @@ async def delete_payment( payment_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Delete a payment""" - tenant_id = validate_access_to_resource(company_id) - - success = PedimentoPaymentsService.delete(db, payment_id, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + success = PedimentoPaymentsService.delete( + db, payment_id, pedimento_id, tenant_id, company_id + ) if not success: raise HTTPException(status_code=404, detail="Payment not found") - + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_rectification_destination.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_rectification_destination.py index 336d1fdc..4a54af28 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_rectification_destination.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_rectification_destination.py @@ -1,16 +1,20 @@ """ Routes for PedimentoRectificationDestination CRUD operations """ + +from typing import Dict, Any from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import validate_access_to_resource +from core.security import validate_access_to_resource, get_current_user -from ..services.pedimento_rectification_destination import PedimentoRectificationDestinationService +from ..services.pedimento_rectification_destination import ( + PedimentoRectificationDestinationService, +) from ..dtos.pedimento_rectification_destination import ( PedimentoRectificationDestinationCreate, PedimentoRectificationDestinationUpdate, - PedimentoRectificationDestinationResponse + PedimentoRectificationDestinationResponse, ) @@ -22,32 +26,42 @@ async def get_rectification_destination( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Get rectification destination by pedimento ID""" - tenant_id = validate_access_to_resource(company_id) - - rectification = PedimentoRectificationDestinationService.get_by_pedimento_id(db, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + rectification = PedimentoRectificationDestinationService.get_by_pedimento_id( + db, pedimento_id, tenant_id, company_id + ) if not rectification: - raise HTTPException(status_code=404, detail="Rectification destination not found") - + raise HTTPException( + status_code=404, detail="Rectification destination not found" + ) + return rectification -@router.post("/", response_model=PedimentoRectificationDestinationResponse, status_code=201) +@router.post( + "/", response_model=PedimentoRectificationDestinationResponse, status_code=201 +) async def create_rectification_destination( pedimento_id: int, data: PedimentoRectificationDestinationCreate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Create rectification destination""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + # Ensure pedimento_id matches if data.pedimento_id != pedimento_id: raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - - rectification = PedimentoRectificationDestinationService.create(db, data, tenant_id, company_id) + + rectification = PedimentoRectificationDestinationService.create( + db, data, tenant_id, company_id + ) return rectification @@ -57,14 +71,19 @@ async def update_rectification_destination( data: PedimentoRectificationDestinationUpdate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Update rectification destination""" - tenant_id = validate_access_to_resource(company_id) - - rectification = PedimentoRectificationDestinationService.update(db, pedimento_id, tenant_id, company_id, data) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + rectification = PedimentoRectificationDestinationService.update( + db, pedimento_id, tenant_id, company_id, data + ) if not rectification: - raise HTTPException(status_code=404, detail="Rectification destination not found") - + raise HTTPException( + status_code=404, detail="Rectification destination not found" + ) + return rectification @@ -73,12 +92,17 @@ async def delete_rectification_destination( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Delete rectification destination""" - tenant_id = validate_access_to_resource(company_id) - - success = PedimentoRectificationDestinationService.delete(db, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + success = PedimentoRectificationDestinationService.delete( + db, pedimento_id, tenant_id, company_id + ) if not success: - raise HTTPException(status_code=404, detail="Rectification destination not found") - + raise HTTPException( + status_code=404, detail="Rectification destination not found" + ) + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_rectification_origin.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_rectification_origin.py index 8d8b2799..0539c945 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_rectification_origin.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_rectification_origin.py @@ -1,16 +1,20 @@ """ Routes for PedimentoRectificationOrigin CRUD operations """ + +from typing import Dict, Any from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import validate_access_to_resource +from core.security import validate_access_to_resource, get_current_user -from ..services.pedimento_rectification_origin import PedimentoRectificationOriginService +from ..services.pedimento_rectification_origin import ( + PedimentoRectificationOriginService, +) from ..dtos.pedimento_rectification_origin import ( PedimentoRectificationOriginCreate, PedimentoRectificationOriginUpdate, - PedimentoRectificationOriginResponse + PedimentoRectificationOriginResponse, ) @@ -22,14 +26,17 @@ async def get_rectification_origin( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Get rectification origin by pedimento ID""" - tenant_id = validate_access_to_resource(company_id) - - rectification = PedimentoRectificationOriginService.get_by_pedimento_id(db, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + rectification = PedimentoRectificationOriginService.get_by_pedimento_id( + db, pedimento_id, tenant_id, company_id + ) if not rectification: raise HTTPException(status_code=404, detail="Rectification origin not found") - + return rectification @@ -39,15 +46,18 @@ async def create_rectification_origin( data: PedimentoRectificationOriginCreate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Create rectification origin""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + # Ensure pedimento_id matches if data.pedimento_id != pedimento_id: raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - - rectification = PedimentoRectificationOriginService.create(db, data, tenant_id, company_id) + + rectification = PedimentoRectificationOriginService.create( + db, data, tenant_id, company_id + ) return rectification @@ -57,14 +67,17 @@ async def update_rectification_origin( data: PedimentoRectificationOriginUpdate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Update rectification origin""" - tenant_id = validate_access_to_resource(company_id) - - rectification = PedimentoRectificationOriginService.update(db, pedimento_id, tenant_id, company_id, data) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + rectification = PedimentoRectificationOriginService.update( + db, pedimento_id, tenant_id, company_id, data + ) if not rectification: raise HTTPException(status_code=404, detail="Rectification origin not found") - + return rectification @@ -73,12 +86,15 @@ async def delete_rectification_origin( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Delete rectification origin""" - tenant_id = validate_access_to_resource(company_id) - - success = PedimentoRectificationOriginService.delete(db, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + success = PedimentoRectificationOriginService.delete( + db, pedimento_id, tenant_id, company_id + ) if not success: raise HTTPException(status_code=404, detail="Rectification origin not found") - + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_transport_means.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_transport_means.py index ad6b7d60..bbf27928 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_transport_means.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_transport_means.py @@ -1,17 +1,18 @@ """ Routes for PedimentoTransportMeans CRUD operations """ + from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session -from typing import List +from typing import Dict, Any, List from core.database import get_core_db -from core.security import validate_access_to_resource +from core.security import validate_access_to_resource, get_current_user from ..services.pedimento_transport_means import PedimentoTransportMeansService from ..dtos.pedimento_transport_means import ( PedimentoTransportMeansCreate, PedimentoTransportMeansUpdate, - PedimentoTransportMeansResponse + PedimentoTransportMeansResponse, ) @@ -23,11 +24,14 @@ async def list_transport_means( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Get all transport means for a pedimento""" - tenant_id = validate_access_to_resource(company_id) - - transport_means = PedimentoTransportMeansService.get_by_pedimento_id(db, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + transport_means = PedimentoTransportMeansService.get_by_pedimento_id( + db, pedimento_id, tenant_id, company_id + ) return transport_means @@ -37,14 +41,17 @@ async def get_transport_mean( transport_mean_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Get a specific transport mean by ID""" - tenant_id = validate_access_to_resource(company_id) - - transport_mean = PedimentoTransportMeansService.get_by_id(db, transport_mean_id, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + transport_mean = PedimentoTransportMeansService.get_by_id( + db, transport_mean_id, pedimento_id, tenant_id, company_id + ) if not transport_mean: raise HTTPException(status_code=404, detail="Transport mean not found") - + return transport_mean @@ -54,15 +61,18 @@ async def create_transport_mean( data: PedimentoTransportMeansCreate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Create a new transport mean""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + # Ensure pedimento_id matches if data.pedimento_id != pedimento_id: raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - - transport_mean = PedimentoTransportMeansService.create(db, data, tenant_id, company_id) + + transport_mean = PedimentoTransportMeansService.create( + db, data, tenant_id, company_id + ) return transport_mean @@ -73,14 +83,17 @@ async def update_transport_mean( data: PedimentoTransportMeansUpdate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Update a transport mean""" - tenant_id = validate_access_to_resource(company_id) - - transport_mean = PedimentoTransportMeansService.update(db, transport_mean_id, pedimento_id, tenant_id, company_id, data) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + transport_mean = PedimentoTransportMeansService.update( + db, transport_mean_id, pedimento_id, tenant_id, company_id, data + ) if not transport_mean: raise HTTPException(status_code=404, detail="Transport mean not found") - + return transport_mean @@ -90,12 +103,15 @@ async def delete_transport_mean( transport_mean_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Delete a transport mean""" - tenant_id = validate_access_to_resource(company_id) - - success = PedimentoTransportMeansService.delete(db, transport_mean_id, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + success = PedimentoTransportMeansService.delete( + db, transport_mean_id, pedimento_id, tenant_id, company_id + ) if not success: raise HTTPException(status_code=404, detail="Transport mean not found") - + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_validation.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_validation.py index dfbe7589..5f78c617 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimento_validation.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimento_validation.py @@ -1,16 +1,18 @@ """ Routes for PedimentoValidation CRUD operations """ + +from typing import Dict, Any from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db -from core.security import validate_access_to_resource +from core.security import validate_access_to_resource, get_current_user from ..services.pedimento_validation import PedimentoValidationService from ..dtos.pedimento_validation import ( PedimentoValidationCreate, PedimentoValidationUpdate, - PedimentoValidationResponse + PedimentoValidationResponse, ) @@ -22,14 +24,17 @@ async def get_validation( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Get validation by pedimento ID""" - tenant_id = validate_access_to_resource(company_id) - - validation = PedimentoValidationService.get_by_pedimento_id(db, pedimento_id, tenant_id, company_id) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + validation = PedimentoValidationService.get_by_pedimento_id( + db, pedimento_id, tenant_id, company_id + ) if not validation: raise HTTPException(status_code=404, detail="Pedimento validation not found") - + return validation @@ -39,14 +44,15 @@ async def create_validation( data: PedimentoValidationCreate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Create pedimento validation""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + # Ensure pedimento_id matches if data.pedimento_id != pedimento_id: raise HTTPException(status_code=400, detail="Pedimento ID mismatch") - + validation = PedimentoValidationService.create(db, data, tenant_id, company_id) return validation @@ -57,14 +63,17 @@ async def update_validation( data: PedimentoValidationUpdate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Update pedimento validation""" - tenant_id = validate_access_to_resource(company_id) - - validation = PedimentoValidationService.update(db, pedimento_id, tenant_id, company_id, data) + tenant_id = validate_access_to_resource(db, company_id, current_user) + + validation = PedimentoValidationService.update( + db, pedimento_id, tenant_id, company_id, data + ) if not validation: raise HTTPException(status_code=404, detail="Pedimento validation not found") - + return validation @@ -73,12 +82,13 @@ async def delete_validation( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Delete pedimento validation""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + success = PedimentoValidationService.delete(db, pedimento_id, tenant_id, company_id) if not success: raise HTTPException(status_code=404, detail="Pedimento validation not found") - + return None diff --git a/backend/api/v1/modules/a76/pedmientos/routes/pedimentos.py b/backend/api/v1/modules/a76/pedmientos/routes/pedimentos.py index 65aafb4e..005d219a 100644 --- a/backend/api/v1/modules/a76/pedmientos/routes/pedimentos.py +++ b/backend/api/v1/modules/a76/pedmientos/routes/pedimentos.py @@ -1,11 +1,12 @@ """ Routes for Pedimentos CRUD operations """ + from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from typing import Dict, Any, Optional from core.database import get_core_db -from core.security import validate_access_to_resource +from core.security import validate_access_to_resource, get_current_user from ..services.pedimentos import PedimentosService from ..dtos.pedimentos import PedimentosCreate, PedimentosUpdate, PedimentosResponse @@ -23,10 +24,11 @@ async def list_pedimentos( client_id: Optional[int] = Query(None, description="Filter by client ID"), year: Optional[str] = Query(None, description="Filter by year"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Get all pedimentos with pagination and filters""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + filters = {} if status: filters["status"] = status @@ -34,15 +36,17 @@ async def list_pedimentos( filters["client_id"] = client_id if year: filters["year"] = year - + skip = (page - 1) * page_size - items, total = PedimentosService.get_all(db, tenant_id, company_id, skip, page_size, filters) - + items, total = PedimentosService.get_all( + db, tenant_id, company_id, skip, page_size, filters + ) + return { "items": [PedimentosResponse.model_validate(item) for item in items], "total": total, "page": page, - "page_size": page_size + "page_size": page_size, } @@ -51,14 +55,15 @@ async def get_pedimento( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Get a pedimento by ID""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + pedimento = PedimentosService.get_by_id(db, pedimento_id, tenant_id, company_id) if not pedimento: raise HTTPException(status_code=404, detail="Pedimento not found") - + return pedimento @@ -67,10 +72,11 @@ async def create_pedimento( data: PedimentosCreate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Create a new pedimento""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + pedimento = PedimentosService.create(db, data, tenant_id, company_id) return pedimento @@ -81,14 +87,15 @@ async def update_pedimento( data: PedimentosUpdate, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Update a pedimento""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + pedimento = PedimentosService.update(db, pedimento_id, tenant_id, company_id, data) if not pedimento: raise HTTPException(status_code=404, detail="Pedimento not found") - + return pedimento @@ -97,12 +104,13 @@ async def delete_pedimento( pedimento_id: int, company_id: int = Query(..., description="Company ID"), db: Session = Depends(get_core_db), + current_user: Dict[str, Any] = Depends(get_current_user), ): """Delete a pedimento""" - tenant_id = validate_access_to_resource(company_id) - + tenant_id = validate_access_to_resource(db, company_id, current_user) + success = PedimentosService.delete(db, pedimento_id, tenant_id, company_id) if not success: raise HTTPException(status_code=404, detail="Pedimento not found") - + return None diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_additional.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_additional.py index f29b1428..fbb2c1f5 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_additional.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_additional.py @@ -1,32 +1,47 @@ """ Service layer for PedimentoConfigAdditional CRUD operations """ + from typing import Optional from sqlalchemy.orm import Session from ..models.pedimento_config_additional import PedimentoConfigAdditional -from ..dtos.pedimento_config_additional import PedimentoConfigAdditionalCreate, PedimentoConfigAdditionalUpdate +from ..dtos.pedimento_config_additional import ( + PedimentoConfigAdditionalCreate, + PedimentoConfigAdditionalUpdate, +) class PedimentoConfigAdditionalService: """Service class for PedimentoConfigAdditional business logic""" @staticmethod - def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int, company_id: int) -> Optional[PedimentoConfigAdditional]: + def get_by_pedimento_id( + db: Session, pedimento_id: int, tenant_id: int, company_id: int + ) -> Optional[PedimentoConfigAdditional]: """Get config by pedimento ID""" - return db.query(PedimentoConfigAdditional).filter( - PedimentoConfigAdditional.pedimento_id == pedimento_id, - PedimentoConfigAdditional.tenant_id == tenant_id, - PedimentoConfigAdditional.company_id == company_id - ).first() + return ( + db.query(PedimentoConfigAdditional) + .filter( + PedimentoConfigAdditional.pedimento_id == pedimento_id, + PedimentoConfigAdditional.tenant_id == tenant_id, + PedimentoConfigAdditional.company_id == company_id, + ) + .first() + ) @staticmethod - def create(db: Session, config_data: PedimentoConfigAdditionalCreate, tenant_id: int, company_id: int) -> PedimentoConfigAdditional: + def create( + db: Session, + config_data: PedimentoConfigAdditionalCreate, + tenant_id: int, + company_id: int, + ) -> PedimentoConfigAdditional: """Create a new config""" config = PedimentoConfigAdditional(**config_data.model_dump()) config.tenant_id = tenant_id config.company_id = company_id - + db.add(config) db.commit() db.refresh(config) @@ -38,17 +53,19 @@ class PedimentoConfigAdditionalService: pedimento_id: int, tenant_id: int, company_id: int, - config_data: PedimentoConfigAdditionalUpdate + config_data: PedimentoConfigAdditionalUpdate, ) -> Optional[PedimentoConfigAdditional]: """Update config""" - config = PedimentoConfigAdditionalService.get_by_pedimento_id(db, pedimento_id, tenant_id, company_id) + config = PedimentoConfigAdditionalService.get_by_pedimento_id( + db, pedimento_id, tenant_id, company_id + ) if not config: return None - + update_data = config_data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(config, field, value) - + db.commit() db.refresh(config) return config @@ -56,10 +73,12 @@ class PedimentoConfigAdditionalService: @staticmethod def delete(db: Session, pedimento_id: int, tenant_id: int, company_id: int) -> bool: """Delete config""" - config = PedimentoConfigAdditionalService.get_by_pedimento_id(db, pedimento_id, tenant_id, company_id) + config = PedimentoConfigAdditionalService.get_by_pedimento_id( + db, pedimento_id, tenant_id, company_id + ) if not config: return False - + db.delete(config) db.commit() return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_calculations.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_calculations.py index 1de5979a..9b9ce5a5 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_calculations.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_calculations.py @@ -1,32 +1,47 @@ """ Service layer for PedimentoConfigCalculations CRUD operations """ + from typing import Optional from sqlalchemy.orm import Session from ..models.pedimento_config_calculations import PedimentoConfigCalculations -from ..dtos.pedimento_config_calculations import PedimentoConfigCalculationsCreate, PedimentoConfigCalculationsUpdate +from ..dtos.pedimento_config_calculations import ( + PedimentoConfigCalculationsCreate, + PedimentoConfigCalculationsUpdate, +) class PedimentoConfigCalculationsService: """Service class for PedimentoConfigCalculations business logic""" @staticmethod - def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int, company_id: int) -> Optional[PedimentoConfigCalculations]: + def get_by_pedimento_id( + db: Session, pedimento_id: int, tenant_id: int, company_id: int + ) -> Optional[PedimentoConfigCalculations]: """Get config by pedimento ID""" - return db.query(PedimentoConfigCalculations).filter( - PedimentoConfigCalculations.pedimento_id == pedimento_id, - PedimentoConfigCalculations.tenant_id == tenant_id, - PedimentoConfigCalculations.company_id == company_id - ).first() + return ( + db.query(PedimentoConfigCalculations) + .filter( + PedimentoConfigCalculations.pedimento_id == pedimento_id, + PedimentoConfigCalculations.tenant_id == tenant_id, + PedimentoConfigCalculations.company_id == company_id, + ) + .first() + ) @staticmethod - def create(db: Session, config_data: PedimentoConfigCalculationsCreate, tenant_id: int, company_id: int) -> PedimentoConfigCalculations: + def create( + db: Session, + config_data: PedimentoConfigCalculationsCreate, + tenant_id: int, + company_id: int, + ) -> PedimentoConfigCalculations: """Create a new config""" config = PedimentoConfigCalculations(**config_data.model_dump()) config.tenant_id = tenant_id config.company_id = company_id - + db.add(config) db.commit() db.refresh(config) @@ -38,17 +53,19 @@ class PedimentoConfigCalculationsService: pedimento_id: int, tenant_id: int, company_id: int, - config_data: PedimentoConfigCalculationsUpdate + config_data: PedimentoConfigCalculationsUpdate, ) -> Optional[PedimentoConfigCalculations]: """Update config""" - config = PedimentoConfigCalculationsService.get_by_pedimento_id(db, pedimento_id, tenant_id, company_id) + config = PedimentoConfigCalculationsService.get_by_pedimento_id( + db, pedimento_id, tenant_id, company_id + ) if not config: return None - + update_data = config_data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(config, field, value) - + db.commit() db.refresh(config) return config @@ -56,10 +73,12 @@ class PedimentoConfigCalculationsService: @staticmethod def delete(db: Session, pedimento_id: int, tenant_id: int, company_id: int) -> bool: """Delete config""" - config = PedimentoConfigCalculationsService.get_by_pedimento_id(db, pedimento_id, tenant_id, company_id) + config = PedimentoConfigCalculationsService.get_by_pedimento_id( + db, pedimento_id, tenant_id, company_id + ) if not config: return False - + db.delete(config) db.commit() return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_parameters.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_parameters.py index e3ed7f3c..6cb35de3 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_parameters.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_parameters.py @@ -1,26 +1,38 @@ """ Service layer for PedimentoConfigParameters CRUD operations """ + from typing import Optional from sqlalchemy.orm import Session from ..models.pedimento_config_parameters import PedimentoConfigParameters -from ..dtos.pedimento_config_parameters import PedimentoConfigParametersCreate, PedimentoConfigParametersUpdate +from ..dtos.pedimento_config_parameters import ( + PedimentoConfigParametersCreate, + PedimentoConfigParametersUpdate, +) class PedimentoConfigParametersService: """Service class for PedimentoConfigParameters business logic""" @staticmethod - def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoConfigParameters]: + def get_by_pedimento_id( + db: Session, pedimento_id: int, tenant_id: int + ) -> Optional[PedimentoConfigParameters]: """Get config by pedimento ID""" - return db.query(PedimentoConfigParameters).filter( - PedimentoConfigParameters.pedimento_id == pedimento_id, - PedimentoConfigParameters.tenant_id == tenant_id - ).first() + return ( + db.query(PedimentoConfigParameters) + .filter( + PedimentoConfigParameters.pedimento_id == pedimento_id, + PedimentoConfigParameters.tenant_id == tenant_id, + ) + .first() + ) @staticmethod - def create(db: Session, config_data: PedimentoConfigParametersCreate) -> PedimentoConfigParameters: + def create( + db: Session, config_data: PedimentoConfigParametersCreate + ) -> PedimentoConfigParameters: """Create a new config""" config = PedimentoConfigParameters(**config_data.model_dump()) db.add(config) @@ -33,17 +45,19 @@ class PedimentoConfigParametersService: db: Session, pedimento_id: int, tenant_id: int, - config_data: PedimentoConfigParametersUpdate + config_data: PedimentoConfigParametersUpdate, ) -> Optional[PedimentoConfigParameters]: """Update config""" - config = PedimentoConfigParametersService.get_by_pedimento_id(db, pedimento_id, tenant_id) + config = PedimentoConfigParametersService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not config: return None - + update_data = config_data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(config, field, value) - + db.commit() db.refresh(config) return config @@ -51,10 +65,12 @@ class PedimentoConfigParametersService: @staticmethod def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: """Delete config""" - config = PedimentoConfigParametersService.get_by_pedimento_id(db, pedimento_id, tenant_id) + config = PedimentoConfigParametersService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not config: return False - + db.delete(config) db.commit() return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_surcharges.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_surcharges.py index af7b9283..5e837314 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_surcharges.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_surcharges.py @@ -1,26 +1,38 @@ """ Service layer for PedimentoConfigSurcharges CRUD operations """ + from typing import Optional from sqlalchemy.orm import Session from ..models.pedimento_config_surcharges import PedimentoConfigSurcharges -from ..dtos.pedimento_config_surcharges import PedimentoConfigSurchargesCreate, PedimentoConfigSurchargesUpdate +from ..dtos.pedimento_config_surcharges import ( + PedimentoConfigSurchargesCreate, + PedimentoConfigSurchargesUpdate, +) class PedimentoConfigSurchargesService: """Service class for PedimentoConfigSurcharges business logic""" @staticmethod - def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoConfigSurcharges]: + def get_by_pedimento_id( + db: Session, pedimento_id: int, tenant_id: int + ) -> Optional[PedimentoConfigSurcharges]: """Get config by pedimento ID""" - return db.query(PedimentoConfigSurcharges).filter( - PedimentoConfigSurcharges.pedimento_id == pedimento_id, - PedimentoConfigSurcharges.tenant_id == tenant_id - ).first() + return ( + db.query(PedimentoConfigSurcharges) + .filter( + PedimentoConfigSurcharges.pedimento_id == pedimento_id, + PedimentoConfigSurcharges.tenant_id == tenant_id, + ) + .first() + ) @staticmethod - def create(db: Session, config_data: PedimentoConfigSurchargesCreate) -> PedimentoConfigSurcharges: + def create( + db: Session, config_data: PedimentoConfigSurchargesCreate + ) -> PedimentoConfigSurcharges: """Create a new config""" config = PedimentoConfigSurcharges(**config_data.model_dump()) db.add(config) @@ -33,17 +45,19 @@ class PedimentoConfigSurchargesService: db: Session, pedimento_id: int, tenant_id: int, - config_data: PedimentoConfigSurchargesUpdate + config_data: PedimentoConfigSurchargesUpdate, ) -> Optional[PedimentoConfigSurcharges]: """Update config""" - config = PedimentoConfigSurchargesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + config = PedimentoConfigSurchargesService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not config: return None - + update_data = config_data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(config, field, value) - + db.commit() db.refresh(config) return config @@ -51,10 +65,12 @@ class PedimentoConfigSurchargesService: @staticmethod def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: """Delete config""" - config = PedimentoConfigSurchargesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + config = PedimentoConfigSurchargesService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not config: return False - + db.delete(config) db.commit() return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_update_rectification.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_update_rectification.py index a9566f49..4d31443d 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_update_rectification.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_update_rectification.py @@ -1,26 +1,40 @@ """ Service layer for PedimentoConfigUpdateRectification CRUD operations """ + from typing import Optional from sqlalchemy.orm import Session -from ..models.pedimento_config_update_rectification import PedimentoConfigUpdateRectification -from ..dtos.pedimento_config_update_rectification import PedimentoConfigUpdateRectificationCreate, PedimentoConfigUpdateRectificationUpdate +from ..models.pedimento_config_update_rectification import ( + PedimentoConfigUpdateRectification, +) +from ..dtos.pedimento_config_update_rectification import ( + PedimentoConfigUpdateRectificationCreate, + PedimentoConfigUpdateRectificationUpdate, +) class PedimentoConfigUpdateRectificationService: """Service class for PedimentoConfigUpdateRectification business logic""" @staticmethod - def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoConfigUpdateRectification]: + def get_by_pedimento_id( + db: Session, pedimento_id: int, tenant_id: int + ) -> Optional[PedimentoConfigUpdateRectification]: """Get config by pedimento ID""" - return db.query(PedimentoConfigUpdateRectification).filter( - PedimentoConfigUpdateRectification.pedimento_id == pedimento_id, - PedimentoConfigUpdateRectification.tenant_id == tenant_id - ).first() + return ( + db.query(PedimentoConfigUpdateRectification) + .filter( + PedimentoConfigUpdateRectification.pedimento_id == pedimento_id, + PedimentoConfigUpdateRectification.tenant_id == tenant_id, + ) + .first() + ) @staticmethod - def create(db: Session, config_data: PedimentoConfigUpdateRectificationCreate) -> PedimentoConfigUpdateRectification: + def create( + db: Session, config_data: PedimentoConfigUpdateRectificationCreate + ) -> PedimentoConfigUpdateRectification: """Create a new config""" config = PedimentoConfigUpdateRectification(**config_data.model_dump()) db.add(config) @@ -33,17 +47,19 @@ class PedimentoConfigUpdateRectificationService: db: Session, pedimento_id: int, tenant_id: int, - config_data: PedimentoConfigUpdateRectificationUpdate + config_data: PedimentoConfigUpdateRectificationUpdate, ) -> Optional[PedimentoConfigUpdateRectification]: """Update config""" - config = PedimentoConfigUpdateRectificationService.get_by_pedimento_id(db, pedimento_id, tenant_id) + config = PedimentoConfigUpdateRectificationService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not config: return None - + update_data = config_data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(config, field, value) - + db.commit() db.refresh(config) return config @@ -51,10 +67,12 @@ class PedimentoConfigUpdateRectificationService: @staticmethod def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: """Delete config""" - config = PedimentoConfigUpdateRectificationService.get_by_pedimento_id(db, pedimento_id, tenant_id) + config = PedimentoConfigUpdateRectificationService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not config: return False - + db.delete(config) db.commit() return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_updates.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_updates.py index bf8a4b97..3e0c4364 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_updates.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_config_updates.py @@ -1,26 +1,38 @@ """ Service layer for PedimentoConfigUpdates CRUD operations """ + from typing import Optional from sqlalchemy.orm import Session from ..models.pedimento_config_updates import PedimentoConfigUpdates -from ..dtos.pedimento_config_updates import PedimentoConfigUpdatesCreate, PedimentoConfigUpdatesUpdate +from ..dtos.pedimento_config_updates import ( + PedimentoConfigUpdatesCreate, + PedimentoConfigUpdatesUpdate, +) class PedimentoConfigUpdatesService: """Service class for PedimentoConfigUpdates business logic""" @staticmethod - def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoConfigUpdates]: + def get_by_pedimento_id( + db: Session, pedimento_id: int, tenant_id: int + ) -> Optional[PedimentoConfigUpdates]: """Get config by pedimento ID""" - return db.query(PedimentoConfigUpdates).filter( - PedimentoConfigUpdates.pedimento_id == pedimento_id, - PedimentoConfigUpdates.tenant_id == tenant_id - ).first() + return ( + db.query(PedimentoConfigUpdates) + .filter( + PedimentoConfigUpdates.pedimento_id == pedimento_id, + PedimentoConfigUpdates.tenant_id == tenant_id, + ) + .first() + ) @staticmethod - def create(db: Session, config_data: PedimentoConfigUpdatesCreate) -> PedimentoConfigUpdates: + def create( + db: Session, config_data: PedimentoConfigUpdatesCreate + ) -> PedimentoConfigUpdates: """Create a new config""" config = PedimentoConfigUpdates(**config_data.model_dump()) db.add(config) @@ -33,17 +45,19 @@ class PedimentoConfigUpdatesService: db: Session, pedimento_id: int, tenant_id: int, - config_data: PedimentoConfigUpdatesUpdate + config_data: PedimentoConfigUpdatesUpdate, ) -> Optional[PedimentoConfigUpdates]: """Update config""" - config = PedimentoConfigUpdatesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + config = PedimentoConfigUpdatesService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not config: return None - + update_data = config_data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(config, field, value) - + db.commit() db.refresh(config) return config @@ -51,10 +65,12 @@ class PedimentoConfigUpdatesService: @staticmethod def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: """Delete config""" - config = PedimentoConfigUpdatesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + config = PedimentoConfigUpdatesService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not config: return False - + db.delete(config) db.commit() return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_customs_offices.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_customs_offices.py index 521cb5ce..ac8d865d 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_customs_offices.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_customs_offices.py @@ -1,26 +1,38 @@ """ Service layer for PedimentoCustomsOffices CRUD operations """ + from typing import Optional from sqlalchemy.orm import Session from ..models.pedimento_customs_offices import PedimentoCustomsOffices -from ..dtos.pedimento_customs_offices import PedimentoCustomsOfficesCreate, PedimentoCustomsOfficesUpdate +from ..dtos.pedimento_customs_offices import ( + PedimentoCustomsOfficesCreate, + PedimentoCustomsOfficesUpdate, +) class PedimentoCustomsOfficesService: """Service class for PedimentoCustomsOffices business logic""" @staticmethod - def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoCustomsOffices]: + def get_by_pedimento_id( + db: Session, pedimento_id: int, tenant_id: int + ) -> Optional[PedimentoCustomsOffices]: """Get customs offices by pedimento ID""" - return db.query(PedimentoCustomsOffices).filter( - PedimentoCustomsOffices.pedimento_id == pedimento_id, - PedimentoCustomsOffices.tenant_id == tenant_id - ).first() + return ( + db.query(PedimentoCustomsOffices) + .filter( + PedimentoCustomsOffices.pedimento_id == pedimento_id, + PedimentoCustomsOffices.tenant_id == tenant_id, + ) + .first() + ) @staticmethod - def create(db: Session, data: PedimentoCustomsOfficesCreate) -> PedimentoCustomsOffices: + def create( + db: Session, data: PedimentoCustomsOfficesCreate + ) -> PedimentoCustomsOffices: """Create new customs offices""" offices = PedimentoCustomsOffices(**data.model_dump()) db.add(offices) @@ -33,17 +45,19 @@ class PedimentoCustomsOfficesService: db: Session, pedimento_id: int, tenant_id: int, - data: PedimentoCustomsOfficesUpdate + data: PedimentoCustomsOfficesUpdate, ) -> Optional[PedimentoCustomsOffices]: """Update customs offices""" - offices = PedimentoCustomsOfficesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + offices = PedimentoCustomsOfficesService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not offices: return None - + update_data = data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(offices, field, value) - + db.commit() db.refresh(offices) return offices @@ -51,10 +65,12 @@ class PedimentoCustomsOfficesService: @staticmethod def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: """Delete customs offices""" - offices = PedimentoCustomsOfficesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + offices = PedimentoCustomsOfficesService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not offices: return False - + db.delete(offices) db.commit() return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_dates.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_dates.py index 2e928978..06c31e87 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_dates.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_dates.py @@ -1,6 +1,7 @@ """ Service layer for PedimentoDates CRUD operations """ + import logging from typing import Optional from sqlalchemy.orm import Session @@ -10,16 +11,23 @@ from ..dtos.pedimento_dates import PedimentoDatesCreate, PedimentoDatesUpdate logger = logging.getLogger(__name__) + class PedimentoDatesService: """Service class for PedimentoDates business logic""" @staticmethod - def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoDates]: + def get_by_pedimento_id( + db: Session, pedimento_id: int, tenant_id: int + ) -> Optional[PedimentoDates]: """Get dates by pedimento ID""" - return db.query(PedimentoDates).filter( - PedimentoDates.pedimento_id == pedimento_id, - PedimentoDates.tenant_id == tenant_id - ).first() + return ( + db.query(PedimentoDates) + .filter( + PedimentoDates.pedimento_id == pedimento_id, + PedimentoDates.tenant_id == tenant_id, + ) + .first() + ) @staticmethod def create(db: Session, data: PedimentoDatesCreate) -> PedimentoDates: @@ -32,20 +40,17 @@ class PedimentoDatesService: @staticmethod def update( - db: Session, - pedimento_id: int, - tenant_id: int, - data: PedimentoDatesUpdate + db: Session, pedimento_id: int, tenant_id: int, data: PedimentoDatesUpdate ) -> Optional[PedimentoDates]: """Update pedimento dates""" dates = PedimentoDatesService.get_by_pedimento_id(db, pedimento_id, tenant_id) if not dates: return None - + update_data = data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(dates, field, value) - + db.commit() db.refresh(dates) return dates @@ -56,7 +61,7 @@ class PedimentoDatesService: dates = PedimentoDatesService.get_by_pedimento_id(db, pedimento_id, tenant_id) if not dates: return False - + db.delete(dates) db.commit() return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_decrementables.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_decrementables.py index f9eb2893..39f8754a 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_decrementables.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_decrementables.py @@ -1,26 +1,38 @@ """ Service layer for PedimentoDecrementables CRUD operations """ + from typing import Optional from sqlalchemy.orm import Session from ..models.pedimento_decrementables import PedimentoDecrementables -from ..dtos.pedimento_decrementables import PedimentoDecrementablesCreate, PedimentoDecrementablesUpdate +from ..dtos.pedimento_decrementables import ( + PedimentoDecrementablesCreate, + PedimentoDecrementablesUpdate, +) class PedimentoDecrementablesService: """Service class for PedimentoDecrementables business logic""" @staticmethod - def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoDecrementables]: + def get_by_pedimento_id( + db: Session, pedimento_id: int, tenant_id: int + ) -> Optional[PedimentoDecrementables]: """Get decrementables by pedimento ID""" - return db.query(PedimentoDecrementables).filter( - PedimentoDecrementables.pedimento_id == pedimento_id, - PedimentoDecrementables.tenant_id == tenant_id - ).first() + return ( + db.query(PedimentoDecrementables) + .filter( + PedimentoDecrementables.pedimento_id == pedimento_id, + PedimentoDecrementables.tenant_id == tenant_id, + ) + .first() + ) @staticmethod - def create(db: Session, data: PedimentoDecrementablesCreate) -> PedimentoDecrementables: + def create( + db: Session, data: PedimentoDecrementablesCreate + ) -> PedimentoDecrementables: """Create new decrementables""" decrementables = PedimentoDecrementables(**data.model_dump()) db.add(decrementables) @@ -33,17 +45,19 @@ class PedimentoDecrementablesService: db: Session, pedimento_id: int, tenant_id: int, - data: PedimentoDecrementablesUpdate + data: PedimentoDecrementablesUpdate, ) -> Optional[PedimentoDecrementables]: """Update decrementables""" - decrementables = PedimentoDecrementablesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + decrementables = PedimentoDecrementablesService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not decrementables: return None - + update_data = data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(decrementables, field, value) - + db.commit() db.refresh(decrementables) return decrementables @@ -51,10 +65,12 @@ class PedimentoDecrementablesService: @staticmethod def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: """Delete decrementables""" - decrementables = PedimentoDecrementablesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + decrementables = PedimentoDecrementablesService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not decrementables: return False - + db.delete(decrementables) db.commit() return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_incrementables.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_incrementables.py index 40bd6c81..9d99b3a1 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_incrementables.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_incrementables.py @@ -1,26 +1,38 @@ """ Service layer for PedimentoIncrementables CRUD operations """ + from typing import Optional from sqlalchemy.orm import Session from ..models.pedimento_incrementables import PedimentoIncrementables -from ..dtos.pedimento_incrementables import PedimentoIncrementablesCreate, PedimentoIncrementablesUpdate +from ..dtos.pedimento_incrementables import ( + PedimentoIncrementablesCreate, + PedimentoIncrementablesUpdate, +) class PedimentoIncrementablesService: """Service class for PedimentoIncrementables business logic""" @staticmethod - def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoIncrementables]: + def get_by_pedimento_id( + db: Session, pedimento_id: int, tenant_id: int + ) -> Optional[PedimentoIncrementables]: """Get incrementables by pedimento ID""" - return db.query(PedimentoIncrementables).filter( - PedimentoIncrementables.pedimento_id == pedimento_id, - PedimentoIncrementables.tenant_id == tenant_id - ).first() + return ( + db.query(PedimentoIncrementables) + .filter( + PedimentoIncrementables.pedimento_id == pedimento_id, + PedimentoIncrementables.tenant_id == tenant_id, + ) + .first() + ) @staticmethod - def create(db: Session, data: PedimentoIncrementablesCreate) -> PedimentoIncrementables: + def create( + db: Session, data: PedimentoIncrementablesCreate + ) -> PedimentoIncrementables: """Create new incrementables""" incrementables = PedimentoIncrementables(**data.model_dump()) db.add(incrementables) @@ -33,17 +45,19 @@ class PedimentoIncrementablesService: db: Session, pedimento_id: int, tenant_id: int, - data: PedimentoIncrementablesUpdate + data: PedimentoIncrementablesUpdate, ) -> Optional[PedimentoIncrementables]: """Update incrementables""" - incrementables = PedimentoIncrementablesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + incrementables = PedimentoIncrementablesService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not incrementables: return None - + update_data = data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(incrementables, field, value) - + db.commit() db.refresh(incrementables) return incrementables @@ -51,10 +65,12 @@ class PedimentoIncrementablesService: @staticmethod def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: """Delete incrementables""" - incrementables = PedimentoIncrementablesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + incrementables = PedimentoIncrementablesService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not incrementables: return False - + db.delete(incrementables) db.commit() return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_indexes.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_indexes.py index dbd8fdd5..c702dff3 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_indexes.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_indexes.py @@ -1,6 +1,7 @@ """ Service layer for PedimentoIndexes CRUD operations """ + from typing import Optional from sqlalchemy.orm import Session @@ -12,12 +13,18 @@ class PedimentoIndexesService: """Service class for PedimentoIndexes business logic""" @staticmethod - def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoIndexes]: + def get_by_pedimento_id( + db: Session, pedimento_id: int, tenant_id: int + ) -> Optional[PedimentoIndexes]: """Get indexes by pedimento ID""" - return db.query(PedimentoIndexes).filter( - PedimentoIndexes.pedimento_id == pedimento_id, - PedimentoIndexes.tenant_id == tenant_id - ).first() + return ( + db.query(PedimentoIndexes) + .filter( + PedimentoIndexes.pedimento_id == pedimento_id, + PedimentoIndexes.tenant_id == tenant_id, + ) + .first() + ) @staticmethod def create(db: Session, data: PedimentoIndexesCreate) -> PedimentoIndexes: @@ -30,20 +37,19 @@ class PedimentoIndexesService: @staticmethod def update( - db: Session, - pedimento_id: int, - tenant_id: int, - data: PedimentoIndexesUpdate + db: Session, pedimento_id: int, tenant_id: int, data: PedimentoIndexesUpdate ) -> Optional[PedimentoIndexes]: """Update indexes""" - indexes = PedimentoIndexesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + indexes = PedimentoIndexesService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not indexes: return None - + update_data = data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(indexes, field, value) - + db.commit() db.refresh(indexes) return indexes @@ -51,10 +57,12 @@ class PedimentoIndexesService: @staticmethod def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: """Delete indexes""" - indexes = PedimentoIndexesService.get_by_pedimento_id(db, pedimento_id, tenant_id) + indexes = PedimentoIndexesService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not indexes: return False - + db.delete(indexes) db.commit() return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_payments.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_payments.py index 81dc5af1..95471291 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_payments.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_payments.py @@ -1,6 +1,7 @@ """ Service layer for PedimentoPayments CRUD operations """ + from typing import Optional from sqlalchemy.orm import Session @@ -12,12 +13,18 @@ class PedimentoPaymentsService: """Service class for PedimentoPayments business logic""" @staticmethod - def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoPayments]: + def get_by_pedimento_id( + db: Session, pedimento_id: int, tenant_id: int + ) -> Optional[PedimentoPayments]: """Get payments by pedimento ID""" - return db.query(PedimentoPayments).filter( - PedimentoPayments.pedimento_id == pedimento_id, - PedimentoPayments.tenant_id == tenant_id - ).first() + return ( + db.query(PedimentoPayments) + .filter( + PedimentoPayments.pedimento_id == pedimento_id, + PedimentoPayments.tenant_id == tenant_id, + ) + .first() + ) @staticmethod def create(db: Session, data: PedimentoPaymentsCreate) -> PedimentoPayments: @@ -30,20 +37,19 @@ class PedimentoPaymentsService: @staticmethod def update( - db: Session, - pedimento_id: int, - tenant_id: int, - data: PedimentoPaymentsUpdate + db: Session, pedimento_id: int, tenant_id: int, data: PedimentoPaymentsUpdate ) -> Optional[PedimentoPayments]: """Update payments""" - payments = PedimentoPaymentsService.get_by_pedimento_id(db, pedimento_id, tenant_id) + payments = PedimentoPaymentsService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not payments: return None - + update_data = data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(payments, field, value) - + db.commit() db.refresh(payments) return payments @@ -51,10 +57,12 @@ class PedimentoPaymentsService: @staticmethod def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: """Delete payments""" - payments = PedimentoPaymentsService.get_by_pedimento_id(db, pedimento_id, tenant_id) + payments = PedimentoPaymentsService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not payments: return False - + db.delete(payments) db.commit() return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_rectification_destination.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_rectification_destination.py index 19dc8715..1790d1e1 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_rectification_destination.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_rectification_destination.py @@ -1,26 +1,40 @@ """ Service layer for PedimentoRectificationDestination CRUD operations """ + from typing import Optional from sqlalchemy.orm import Session -from ..models.pedimento_rectification_destination import PedimentoRectificationDestination -from ..dtos.pedimento_rectification_destination import PedimentoRectificationDestinationCreate, PedimentoRectificationDestinationUpdate +from ..models.pedimento_rectification_destination import ( + PedimentoRectificationDestination, +) +from ..dtos.pedimento_rectification_destination import ( + PedimentoRectificationDestinationCreate, + PedimentoRectificationDestinationUpdate, +) class PedimentoRectificationDestinationService: """Service class for PedimentoRectificationDestination business logic""" @staticmethod - def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoRectificationDestination]: + def get_by_pedimento_id( + db: Session, pedimento_id: int, tenant_id: int + ) -> Optional[PedimentoRectificationDestination]: """Get rectification destination by pedimento ID""" - return db.query(PedimentoRectificationDestination).filter( - PedimentoRectificationDestination.pedimento_id == pedimento_id, - PedimentoRectificationDestination.tenant_id == tenant_id - ).first() + return ( + db.query(PedimentoRectificationDestination) + .filter( + PedimentoRectificationDestination.pedimento_id == pedimento_id, + PedimentoRectificationDestination.tenant_id == tenant_id, + ) + .first() + ) @staticmethod - def create(db: Session, data: PedimentoRectificationDestinationCreate) -> PedimentoRectificationDestination: + def create( + db: Session, data: PedimentoRectificationDestinationCreate + ) -> PedimentoRectificationDestination: """Create new rectification destination""" rectification = PedimentoRectificationDestination(**data.model_dump()) db.add(rectification) @@ -33,17 +47,19 @@ class PedimentoRectificationDestinationService: db: Session, pedimento_id: int, tenant_id: int, - data: PedimentoRectificationDestinationUpdate + data: PedimentoRectificationDestinationUpdate, ) -> Optional[PedimentoRectificationDestination]: """Update rectification destination""" - rectification = PedimentoRectificationDestinationService.get_by_pedimento_id(db, pedimento_id, tenant_id) + rectification = PedimentoRectificationDestinationService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not rectification: return None - + update_data = data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(rectification, field, value) - + db.commit() db.refresh(rectification) return rectification @@ -51,10 +67,12 @@ class PedimentoRectificationDestinationService: @staticmethod def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: """Delete rectification destination""" - rectification = PedimentoRectificationDestinationService.get_by_pedimento_id(db, pedimento_id, tenant_id) + rectification = PedimentoRectificationDestinationService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not rectification: return False - + db.delete(rectification) db.commit() return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_rectification_origin.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_rectification_origin.py index 1206b137..74ca4be4 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_rectification_origin.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_rectification_origin.py @@ -1,26 +1,38 @@ """ Service layer for PedimentoRectificationOrigin CRUD operations """ + from typing import Optional from sqlalchemy.orm import Session from ..models.pedimento_rectification_origin import PedimentoRectificationOrigin -from ..dtos.pedimento_rectification_origin import PedimentoRectificationOriginCreate, PedimentoRectificationOriginUpdate +from ..dtos.pedimento_rectification_origin import ( + PedimentoRectificationOriginCreate, + PedimentoRectificationOriginUpdate, +) class PedimentoRectificationOriginService: """Service class for PedimentoRectificationOrigin business logic""" @staticmethod - def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoRectificationOrigin]: + def get_by_pedimento_id( + db: Session, pedimento_id: int, tenant_id: int + ) -> Optional[PedimentoRectificationOrigin]: """Get rectification origin by pedimento ID""" - return db.query(PedimentoRectificationOrigin).filter( - PedimentoRectificationOrigin.pedimento_id == pedimento_id, - PedimentoRectificationOrigin.tenant_id == tenant_id - ).first() + return ( + db.query(PedimentoRectificationOrigin) + .filter( + PedimentoRectificationOrigin.pedimento_id == pedimento_id, + PedimentoRectificationOrigin.tenant_id == tenant_id, + ) + .first() + ) @staticmethod - def create(db: Session, data: PedimentoRectificationOriginCreate) -> PedimentoRectificationOrigin: + def create( + db: Session, data: PedimentoRectificationOriginCreate + ) -> PedimentoRectificationOrigin: """Create new rectification origin""" rectification = PedimentoRectificationOrigin(**data.model_dump()) db.add(rectification) @@ -33,17 +45,19 @@ class PedimentoRectificationOriginService: db: Session, pedimento_id: int, tenant_id: int, - data: PedimentoRectificationOriginUpdate + data: PedimentoRectificationOriginUpdate, ) -> Optional[PedimentoRectificationOrigin]: """Update rectification origin""" - rectification = PedimentoRectificationOriginService.get_by_pedimento_id(db, pedimento_id, tenant_id) + rectification = PedimentoRectificationOriginService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not rectification: return None - + update_data = data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(rectification, field, value) - + db.commit() db.refresh(rectification) return rectification @@ -51,10 +65,12 @@ class PedimentoRectificationOriginService: @staticmethod def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: """Delete rectification origin""" - rectification = PedimentoRectificationOriginService.get_by_pedimento_id(db, pedimento_id, tenant_id) + rectification = PedimentoRectificationOriginService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not rectification: return False - + db.delete(rectification) db.commit() return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_transport_means.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_transport_means.py index 8384523f..d5d395c5 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_transport_means.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_transport_means.py @@ -1,26 +1,38 @@ """ Service layer for PedimentoTransportMeans CRUD operations """ + from typing import Optional from sqlalchemy.orm import Session from ..models.pedimento_transport_means import PedimentoTransportMeans -from ..dtos.pedimento_transport_means import PedimentoTransportMeansCreate, PedimentoTransportMeansUpdate +from ..dtos.pedimento_transport_means import ( + PedimentoTransportMeansCreate, + PedimentoTransportMeansUpdate, +) class PedimentoTransportMeansService: """Service class for PedimentoTransportMeans business logic""" @staticmethod - def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoTransportMeans]: + def get_by_pedimento_id( + db: Session, pedimento_id: int, tenant_id: int + ) -> Optional[PedimentoTransportMeans]: """Get transport means by pedimento ID""" - return db.query(PedimentoTransportMeans).filter( - PedimentoTransportMeans.pedimento_id == pedimento_id, - PedimentoTransportMeans.tenant_id == tenant_id - ).first() + return ( + db.query(PedimentoTransportMeans) + .filter( + PedimentoTransportMeans.pedimento_id == pedimento_id, + PedimentoTransportMeans.tenant_id == tenant_id, + ) + .first() + ) @staticmethod - def create(db: Session, data: PedimentoTransportMeansCreate) -> PedimentoTransportMeans: + def create( + db: Session, data: PedimentoTransportMeansCreate + ) -> PedimentoTransportMeans: """Create new transport means""" transport = PedimentoTransportMeans(**data.model_dump()) db.add(transport) @@ -33,17 +45,19 @@ class PedimentoTransportMeansService: db: Session, pedimento_id: int, tenant_id: int, - data: PedimentoTransportMeansUpdate + data: PedimentoTransportMeansUpdate, ) -> Optional[PedimentoTransportMeans]: """Update transport means""" - transport = PedimentoTransportMeansService.get_by_pedimento_id(db, pedimento_id, tenant_id) + transport = PedimentoTransportMeansService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not transport: return None - + update_data = data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(transport, field, value) - + db.commit() db.refresh(transport) return transport @@ -51,10 +65,12 @@ class PedimentoTransportMeansService: @staticmethod def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: """Delete transport means""" - transport = PedimentoTransportMeansService.get_by_pedimento_id(db, pedimento_id, tenant_id) + transport = PedimentoTransportMeansService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not transport: return False - + db.delete(transport) db.commit() return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimento_validation.py b/backend/api/v1/modules/a76/pedmientos/services/pedimento_validation.py index 8db22ede..4ebae004 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimento_validation.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimento_validation.py @@ -1,23 +1,33 @@ """ Service layer for PedimentoValidation CRUD operations """ + from typing import Optional from sqlalchemy.orm import Session from ..models.pedimento_validation import PedimentoValidation -from ..dtos.pedimento_validation import PedimentoValidationCreate, PedimentoValidationUpdate +from ..dtos.pedimento_validation import ( + PedimentoValidationCreate, + PedimentoValidationUpdate, +) class PedimentoValidationService: """Service class for PedimentoValidation business logic""" @staticmethod - def get_by_pedimento_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[PedimentoValidation]: + def get_by_pedimento_id( + db: Session, pedimento_id: int, tenant_id: int + ) -> Optional[PedimentoValidation]: """Get validation by pedimento ID""" - return db.query(PedimentoValidation).filter( - PedimentoValidation.pedimento_id == pedimento_id, - PedimentoValidation.tenant_id == tenant_id - ).first() + return ( + db.query(PedimentoValidation) + .filter( + PedimentoValidation.pedimento_id == pedimento_id, + PedimentoValidation.tenant_id == tenant_id, + ) + .first() + ) @staticmethod def create(db: Session, data: PedimentoValidationCreate) -> PedimentoValidation: @@ -30,20 +40,19 @@ class PedimentoValidationService: @staticmethod def update( - db: Session, - pedimento_id: int, - tenant_id: int, - data: PedimentoValidationUpdate + db: Session, pedimento_id: int, tenant_id: int, data: PedimentoValidationUpdate ) -> Optional[PedimentoValidation]: """Update validation""" - validation = PedimentoValidationService.get_by_pedimento_id(db, pedimento_id, tenant_id) + validation = PedimentoValidationService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not validation: return None - + update_data = data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(validation, field, value) - + db.commit() db.refresh(validation) return validation @@ -51,10 +60,12 @@ class PedimentoValidationService: @staticmethod def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: """Delete validation""" - validation = PedimentoValidationService.get_by_pedimento_id(db, pedimento_id, tenant_id) + validation = PedimentoValidationService.get_by_pedimento_id( + db, pedimento_id, tenant_id + ) if not validation: return False - + db.delete(validation) db.commit() return True diff --git a/backend/api/v1/modules/a76/pedmientos/services/pedimentos.py b/backend/api/v1/modules/a76/pedmientos/services/pedimentos.py index e22717c2..5dcc1652 100644 --- a/backend/api/v1/modules/a76/pedmientos/services/pedimentos.py +++ b/backend/api/v1/modules/a76/pedmientos/services/pedimentos.py @@ -1,6 +1,7 @@ """ Service layer for Pedimentos CRUD operations """ + from typing import List, Optional, Dict, Any from sqlalchemy.orm import Session from sqlalchemy import desc @@ -17,25 +18,26 @@ class PedimentosService: def get_all( db: Session, tenant_id: int, + company_id: int, skip: int = 0, limit: int = 100, - filters: Optional[Dict[str, Any]] = None + filters: Optional[Dict[str, Any]] = None, ) -> tuple[List[Pedimentos], int]: """ Get all pedimentos for a tenant with pagination and filters - + Args: db: Database session tenant_id: Tenant ID skip: Number of records to skip limit: Maximum number of records to return filters: Optional filters dict - + Returns: Tuple of (list of pedimentos, total count) """ query = db.query(Pedimentos).filter(Pedimentos.tenant_id == tenant_id) - + if filters: if filters.get("status"): query = query.filter(Pedimentos.status == filters["status"]) @@ -43,45 +45,52 @@ class PedimentosService: query = query.filter(Pedimentos.client_id == filters["client_id"]) if filters.get("year"): query = query.filter(Pedimentos.year == filters["year"]) - + total = query.count() - items = query.order_by(desc(Pedimentos.created_at)).offset(skip).limit(limit).all() - + items = ( + query.order_by(desc(Pedimentos.created_at)).offset(skip).limit(limit).all() + ) + return items, total @staticmethod - def get_by_id(db: Session, pedimento_id: int, tenant_id: int) -> Optional[Pedimentos]: + def get_by_id( + db: Session, pedimento_id: int, tenant_id: int + ) -> Optional[Pedimentos]: """ Get a pedimento by ID - + Args: db: Database session pedimento_id: Pedimento ID tenant_id: Tenant ID - + Returns: Pedimento or None if not found """ - return db.query(Pedimentos).filter( - Pedimentos.id == pedimento_id, - Pedimentos.tenant_id == tenant_id - ).first() + return ( + db.query(Pedimentos) + .filter(Pedimentos.id == pedimento_id, Pedimentos.tenant_id == tenant_id) + .first() + ) @staticmethod - def create(db: Session, pedimento_data: PedimentosCreate, tenant_id: int) -> Pedimentos: + def create( + db: Session, pedimento_data: PedimentosCreate, tenant_id: int + ) -> Pedimentos: """ Create a new pedimento - + Args: db: Database session pedimento_data: Pedimento creation data - + Returns: Created pedimento """ pedimento = Pedimentos(**pedimento_data.model_dump()) pedimento.tenant_id = 1 - + db.add(pedimento) db.commit() db.refresh(pedimento) @@ -89,31 +98,28 @@ class PedimentosService: @staticmethod def update( - db: Session, - pedimento_id: int, - tenant_id: int, - pedimento_data: PedimentosUpdate + db: Session, pedimento_id: int, tenant_id: int, pedimento_data: PedimentosUpdate ) -> Optional[Pedimentos]: """ Update a pedimento - + Args: db: Database session pedimento_id: Pedimento ID tenant_id: Tenant ID pedimento_data: Updated data - + Returns: Updated pedimento or None if not found """ pedimento = PedimentosService.get_by_id(db, pedimento_id, tenant_id) if not pedimento: return None - + update_data = pedimento_data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(pedimento, field, value) - + db.commit() db.refresh(pedimento) return pedimento @@ -122,19 +128,19 @@ class PedimentosService: def delete(db: Session, pedimento_id: int, tenant_id: int) -> bool: """ Delete a pedimento - + Args: db: Database session pedimento_id: Pedimento ID tenant_id: Tenant ID - + Returns: True if deleted, False if not found """ pedimento = PedimentosService.get_by_id(db, pedimento_id, tenant_id) if not pedimento: return False - + db.delete(pedimento) db.commit() return True diff --git a/backend/api/v1/modules/a76/permission_rule_oct/dto.py b/backend/api/v1/modules/a76/permission_rule_oct/dto.py index fc31095c..9b79ca74 100644 --- a/backend/api/v1/modules/a76/permission_rule_oct/dto.py +++ b/backend/api/v1/modules/a76/permission_rule_oct/dto.py @@ -1,6 +1,7 @@ from pydantic import BaseModel from typing import Optional + class PermissionRuleOctBaseDTO(BaseModel): permission: str start_date: Optional[int] @@ -8,9 +9,11 @@ class PermissionRuleOctBaseDTO(BaseModel): sector: Optional[str] system: Optional[str] + class PermissionRuleOctCreateDTO(PermissionRuleOctBaseDTO): pass + class PermissionRuleOctResponseDTO(PermissionRuleOctBaseDTO): class Config: - from_attributes = True \ No newline at end of file + from_attributes = True diff --git a/backend/api/v1/modules/a76/permission_rule_oct/models.py b/backend/api/v1/modules/a76/permission_rule_oct/models.py index aae27a27..894276df 100644 --- a/backend/api/v1/modules/a76/permission_rule_oct/models.py +++ b/backend/api/v1/modules/a76/permission_rule_oct/models.py @@ -1,5 +1,12 @@ from typing import Optional -from sqlalchemy import Integer, String, ForeignKey, PrimaryKeyConstraint, ForeignKeyConstraint, UniqueConstraint +from sqlalchemy import ( + Integer, + String, + ForeignKey, + PrimaryKeyConstraint, + ForeignKeyConstraint, + UniqueConstraint, +) from sqlalchemy.orm import Mapped, mapped_column from core.database import Base @@ -7,19 +14,27 @@ from core.database import Base class PermissionRuleOct(Base): __tablename__ = "permission_rule_oct" __table_args__ = ( - PrimaryKeyConstraint('id', name='permission_rule_oct_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_permission_rule_oct_tenant'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_permission_rule_oct_company'), - UniqueConstraint('tenant_id', 'company_id', 'permission', name='permission_rule_oct_permission_tenant_ukey'), - {"schema": "a76"} + PrimaryKeyConstraint("id", name="permission_rule_oct_pkey"), + ForeignKeyConstraint( + ["tenant_id"], ["a76.tenants.id"], name="fk_permission_rule_oct_tenant" + ), + ForeignKeyConstraint( + ["company_id"], ["a76.company.id"], name="fk_permission_rule_oct_company" + ), + UniqueConstraint( + "tenant_id", + "company_id", + "permission", + name="permission_rule_oct_permission_tenant_ukey", + ), + {"schema": "a76"}, ) id: Mapped[int] = mapped_column(Integer, primary_key=True) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - + permission: Mapped[str] = mapped_column(String(20)) start_date: Mapped[Optional[int]] = mapped_column() end_date: Mapped[Optional[int]] = mapped_column() sector: Mapped[Optional[str]] = mapped_column(String(8)) system: Mapped[Optional[str]] = mapped_column(String(5)) - \ No newline at end of file diff --git a/backend/api/v1/modules/a76/permission_rule_oct/routes.py b/backend/api/v1/modules/a76/permission_rule_oct/routes.py index 34a20e5e..4142a2e2 100644 --- a/backend/api/v1/modules/a76/permission_rule_oct/routes.py +++ b/backend/api/v1/modules/a76/permission_rule_oct/routes.py @@ -12,8 +12,7 @@ router = APIRouter(prefix="/permission-rule-oct", tags=["PermissionRuleOct"]) @router.get("/", response_model=List[PermissionRuleOctResponseDTO]) async def list_permissions( - db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) ): """ List all PermissionRuleOct entries. @@ -32,7 +31,7 @@ async def list_permissions( async def read_permission( permission: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Get a specific PermissionRuleOct by its permission. @@ -50,11 +49,15 @@ async def read_permission( return permission -@router.post("/", response_model=PermissionRuleOctResponseDTO, status_code=status.HTTP_201_CREATED) +@router.post( + "/", + response_model=PermissionRuleOctResponseDTO, + status_code=status.HTTP_201_CREATED, +) async def create_permission( permission_data: PermissionRuleOctCreateDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Create a new PermissionRuleOct entry. @@ -73,7 +76,7 @@ async def create_permission( async def delete_permission( permission: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Delete a PermissionRuleOct by its permission. @@ -87,4 +90,4 @@ async def delete_permission( permission = PermissionRuleOctService.delete_permission(db, permission) if not permission: - raise HTTPException(status_code=404, detail="PermissionRuleOct not found") \ No newline at end of file + raise HTTPException(status_code=404, detail="PermissionRuleOct not found") diff --git a/backend/api/v1/modules/a76/permission_rule_oct/services.py b/backend/api/v1/modules/a76/permission_rule_oct/services.py index 9de13cb7..1fe0d0e9 100644 --- a/backend/api/v1/modules/a76/permission_rule_oct/services.py +++ b/backend/api/v1/modules/a76/permission_rule_oct/services.py @@ -1,10 +1,15 @@ from sqlalchemy.orm import Session from . import models, dto + class PermissionRuleOctService: @staticmethod def get_permission_by_id(db: Session, permission: str): - return db.query(models.PermissionRuleOct).filter(models.PermissionRuleOct.permission == permission).first() + return ( + db.query(models.PermissionRuleOct) + .filter(models.PermissionRuleOct.permission == permission) + .first() + ) @staticmethod def create_permission(db: Session, permission_data: dto.PermissionRuleOctCreateDTO): @@ -20,4 +25,4 @@ class PermissionRuleOctService: if permission: db.delete(permission) db.commit() - return permission \ No newline at end of file + return permission diff --git a/backend/api/v1/modules/a76/router.py b/backend/api/v1/modules/a76/router.py index 25305015..b35d2c8f 100644 --- a/backend/api/v1/modules/a76/router.py +++ b/backend/api/v1/modules/a76/router.py @@ -2,6 +2,7 @@ Router principal de API v1 Agrega todos los módulos de la aplicación """ + from fastapi import APIRouter # Importar routers de módulos @@ -38,15 +39,23 @@ router.include_router(tenants_router, prefix="/a76", tags=["a76 / tenants"]) router.include_router(user_tenant_router, prefix="/a76", tags=["a76 / user-tenants"]) router.include_router(licenses_router, prefix="/a76", tags=["a76 / licenses"]) router.include_router(pedimentos_router, prefix="/a76") -router.include_router(client_and_provider_router, prefix="/a76", tags=["a76 / clients and providers"]) +router.include_router( + client_and_provider_router, prefix="/a76", tags=["a76 / clients and providers"] +) router.include_router(company_router, prefix="/a76", tags=["a76 / company"]) router.include_router(classes_router, prefix="/a76", tags=["a76 / classes"]) router.include_router(parts_router, prefix="/a76", tags=["a76 / parts"]) -router.include_router(permission_rule_oct_router, prefix="/a76", tags=["a76 / PermissionRuleOct"]) +router.include_router( + permission_rule_oct_router, prefix="/a76", tags=["a76 / PermissionRuleOct"] +) router.include_router(package_router, prefix="/a76", tags=["a76 / Package"]) router.include_router(seal_router, prefix="/a76", tags=["a76 / Seal"]) -router.include_router(fraction_rule_octave_router, prefix="/a76", tags=["a76 / FractionRuleOctave"]) -router.include_router(country_rule_oct_router, prefix="/a76", tags=["a76 / CountryRuleOct"]) +router.include_router( + fraction_rule_octave_router, prefix="/a76", tags=["a76 / FractionRuleOctave"] +) +router.include_router( + country_rule_oct_router, prefix="/a76", tags=["a76 / CountryRuleOct"] +) router.include_router(exchange_rate_router, prefix="/a76", tags=["a76 / ExchangeRate"]) router.include_router(transport_types_router, prefix="/a76", tags=["a76 / TransportTypes"]) router.include_router(trailers_router, prefix="/a76", tags=["a76 / Trailers"]) diff --git a/backend/api/v1/modules/a76/seal/dto.py b/backend/api/v1/modules/a76/seal/dto.py index 48fdd5d1..cdbdc7c7 100644 --- a/backend/api/v1/modules/a76/seal/dto.py +++ b/backend/api/v1/modules/a76/seal/dto.py @@ -4,12 +4,15 @@ DTOs for Seal. from pydantic import BaseModel + class SealBaseDTO(BaseModel): seal: str + class SealCreateDTO(SealBaseDTO): pass + class SealResponseDTO(SealBaseDTO): class Config: - from_attributes = True \ No newline at end of file + from_attributes = True diff --git a/backend/api/v1/modules/a76/seal/models.py b/backend/api/v1/modules/a76/seal/models.py index e47b5ba2..fa9ae245 100644 --- a/backend/api/v1/modules/a76/seal/models.py +++ b/backend/api/v1/modules/a76/seal/models.py @@ -1,4 +1,11 @@ -from sqlalchemy import Integer, String, ForeignKey, PrimaryKeyConstraint, ForeignKeyConstraint, UniqueConstraint +from sqlalchemy import ( + Integer, + String, + ForeignKey, + PrimaryKeyConstraint, + ForeignKeyConstraint, + UniqueConstraint, +) from sqlalchemy.orm import Mapped, mapped_column from core.database import Base @@ -6,16 +13,17 @@ from core.database import Base class Seal(Base): __tablename__ = "seal" __table_args__ = ( - PrimaryKeyConstraint('id', name='seal_pkey'), - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], name='fk_seal_tenant'), - ForeignKeyConstraint(['company_id'], ['a76.company.id'], name='fk_seal_company'), - UniqueConstraint('tenant_id', 'company_id', 'seal', name='seal_ukey'), - {"schema": "a76"} + PrimaryKeyConstraint("id", name="seal_pkey"), + ForeignKeyConstraint(["tenant_id"], ["a76.tenants.id"], name="fk_seal_tenant"), + ForeignKeyConstraint( + ["company_id"], ["a76.company.id"], name="fk_seal_company" + ), + UniqueConstraint("tenant_id", "company_id", "seal", name="seal_ukey"), + {"schema": "a76"}, ) - + id: Mapped[int] = mapped_column(Integer, primary_key=True) tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - + seal: Mapped[str] = mapped_column(String(15)) - \ No newline at end of file diff --git a/backend/api/v1/modules/a76/seal/routes.py b/backend/api/v1/modules/a76/seal/routes.py index ba1e6892..2d7aab19 100644 --- a/backend/api/v1/modules/a76/seal/routes.py +++ b/backend/api/v1/modules/a76/seal/routes.py @@ -16,8 +16,7 @@ router = APIRouter(prefix="/seals", tags=["Seal"]) @router.get("/", response_model=List[SealResponseDTO]) async def list_seals( - db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user) ): """ List all Seal entries. @@ -36,7 +35,7 @@ async def list_seals( async def read_seal( seal: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Get a specific Seal by its seal. @@ -58,7 +57,7 @@ async def read_seal( async def create_seal( seal_data: SealCreateDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Create a new Seal entry. @@ -77,7 +76,7 @@ async def create_seal( async def delete_seal( seal: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Delete a Seal by its seal. @@ -91,4 +90,4 @@ async def delete_seal( seal = SealService.delete_seal(db, seal) if not seal: - raise HTTPException(status_code=404, detail="Seal not found") \ No newline at end of file + raise HTTPException(status_code=404, detail="Seal not found") diff --git a/backend/api/v1/modules/a76/seal/services.py b/backend/api/v1/modules/a76/seal/services.py index e39fafef..d62ad6fc 100644 --- a/backend/api/v1/modules/a76/seal/services.py +++ b/backend/api/v1/modules/a76/seal/services.py @@ -5,6 +5,7 @@ from . import models, dto Service layer for Seal. """ + class SealService: @staticmethod def get_seal_by_id(db: Session, seal: str): @@ -24,4 +25,4 @@ class SealService: if seal: db.delete(seal) db.commit() - return seal \ No newline at end of file + return seal diff --git a/backend/api/v1/modules/a76/tenants/__init__.py b/backend/api/v1/modules/a76/tenants/__init__.py index 7c89dc70..73aa739e 100644 --- a/backend/api/v1/modules/a76/tenants/__init__.py +++ b/backend/api/v1/modules/a76/tenants/__init__.py @@ -1,6 +1,7 @@ """ Módulo de Tenants """ + from .routes import router __all__ = ["router"] diff --git a/backend/api/v1/modules/a76/tenants/dto.py b/backend/api/v1/modules/a76/tenants/dto.py index 9fa9b174..c19dbfe9 100644 --- a/backend/api/v1/modules/a76/tenants/dto.py +++ b/backend/api/v1/modules/a76/tenants/dto.py @@ -2,6 +2,7 @@ DTOs (Data Transfer Objects) para módulo de tenants Reemplaza schemas.py siguiendo enfoque DDD y estilo NestJS """ + from pydantic import BaseModel, Field, EmailStr from typing import Optional from datetime import datetime @@ -10,21 +11,35 @@ from enum import Enum class TenantTypeDTO(str, Enum): """Tipo de tenant""" + SHARED = "shared" DEDICATED = "dedicated" class TenantCreateDTO(BaseModel): """DTO para crear un nuevo tenant""" - name: str = Field(..., min_length=3, max_length=255, description="Nombre del tenant") - slug: str = Field(..., min_length=3, max_length=100, description="Identificador único del tenant") - keycloak_realm: str = Field(..., min_length=3, max_length=255, description="Nombre del realm en Keycloak") - type: TenantTypeDTO = Field(default=TenantTypeDTO.SHARED, description="Tipo de tenant") - - contact_name: Optional[str] = Field(None, max_length=255, description="Nombre de contacto") + + name: str = Field( + ..., min_length=3, max_length=255, description="Nombre del tenant" + ) + slug: str = Field( + ..., min_length=3, max_length=100, description="Identificador único del tenant" + ) + keycloak_realm: str = Field( + ..., min_length=3, max_length=255, description="Nombre del realm en Keycloak" + ) + type: TenantTypeDTO = Field( + default=TenantTypeDTO.SHARED, description="Tipo de tenant" + ) + + contact_name: Optional[str] = Field( + None, max_length=255, description="Nombre de contacto" + ) contact_email: Optional[EmailStr] = Field(None, description="Email de contacto") - contact_phone: Optional[str] = Field(None, max_length=50, description="Teléfono de contacto") - + contact_phone: Optional[str] = Field( + None, max_length=50, description="Teléfono de contacto" + ) + class Config: json_schema_extra = { "example": { @@ -34,30 +49,32 @@ class TenantCreateDTO(BaseModel): "type": "shared", "contact_name": "Juan Pérez", "contact_email": "juan.perez@empresa-abc.com", - "contact_phone": "+52 55 1234 5678" + "contact_phone": "+52 55 1234 5678", } } class TenantUpdateDTO(BaseModel): """DTO para actualizar un tenant""" + name: Optional[str] = Field(None, min_length=3, max_length=255) contact_name: Optional[str] = Field(None, max_length=255) contact_email: Optional[EmailStr] = None contact_phone: Optional[str] = Field(None, max_length=50) is_active: Optional[bool] = None - + class Config: json_schema_extra = { "example": { "name": "Empresa ABC S.A. de C.V. - Actualizado", - "contact_email": "nuevo@empresa-abc.com" + "contact_email": "nuevo@empresa-abc.com", } } class TenantResponseDTO(BaseModel): """DTO para respuesta de tenant""" + id: int name: str slug: str @@ -69,7 +86,7 @@ class TenantResponseDTO(BaseModel): is_active: bool created_at: datetime updated_at: datetime - + class Config: from_attributes = True json_schema_extra = { @@ -84,13 +101,14 @@ class TenantResponseDTO(BaseModel): "contact_phone": "+52 55 1234 5678", "is_active": True, "created_at": "2025-01-15T10:30:00Z", - "updated_at": "2025-01-15T10:30:00Z" + "updated_at": "2025-01-15T10:30:00Z", } } class TenantListResponseDTO(BaseModel): """DTO para lista de tenants""" + tenants: list[TenantResponseDTO] total: int page: int diff --git a/backend/api/v1/modules/a76/tenants/models.py b/backend/api/v1/modules/a76/tenants/models.py index 49c7dca1..5f38c36b 100644 --- a/backend/api/v1/modules/a76/tenants/models.py +++ b/backend/api/v1/modules/a76/tenants/models.py @@ -1,6 +1,7 @@ """ Modelos ORM para gestión de tenants """ + from sqlalchemy import Column, Integer, String, DateTime, Boolean, Text, Enum as SQLEnum from sqlalchemy.sql import func from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -15,6 +16,7 @@ if TYPE_CHECKING: class TenantType(enum.Enum): """Tipo de tenant según tamaño y necesidades""" + SHARED = "shared" # BD compartida DEDICATED = "dedicated" # BD dedicada @@ -24,37 +26,44 @@ class Tenant(Base): Modelo de Tenant - Cliente/Organización en el sistema Cada tenant puede tener BD compartida o dedicada """ + __tablename__ = "tenants" __table_args__ = {"schema": "a76"} - + id = Column(Integer, primary_key=True, index=True) name = Column(String(255), nullable=False, index=True) slug = Column(String(100), unique=True, nullable=False, index=True) - + # Tipo de tenant (compartido o dedicado) type = Column(SQLEnum(TenantType), default=TenantType.SHARED, nullable=False) - + # Keycloak realm asociado keycloak_realm = Column(String(255), nullable=False) - + # Configuración de BD dedicada (JSON string o NULL si usa BD compartida) db_config = Column(Text, nullable=True) # JSON: {host, port, name, user, password} - + # Información de contacto contact_name = Column(String(255)) contact_email = Column(String(255)) contact_phone = Column(String(50)) - + # Estado is_active = Column(Boolean, default=True, nullable=False) - + # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now()) - updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, default=func.now(), onupdate=func.now()) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, default=func.now(), onupdate=func.now() + ) deleted_at: Mapped[datetime | None] = mapped_column(DateTime, nullable=True) - + # Relación con UserTenant - user_relations: Mapped[List["UserTenant"]] = relationship("UserTenant", back_populates="tenant") - + user_relations: Mapped[List["UserTenant"]] = relationship( + "UserTenant", back_populates="tenant" + ) + def __repr__(self): return f"" diff --git a/backend/api/v1/modules/a76/tenants/routes.py b/backend/api/v1/modules/a76/tenants/routes.py index 2768c025..c6679dec 100644 --- a/backend/api/v1/modules/a76/tenants/routes.py +++ b/backend/api/v1/modules/a76/tenants/routes.py @@ -1,13 +1,19 @@ """ Endpoints API para gestión de tenants """ + from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from typing import List from core.database import get_core_db from core.security import get_current_user, has_role -from .dto import TenantCreateDTO, TenantUpdateDTO, TenantResponseDTO, TenantListResponseDTO +from .dto import ( + TenantCreateDTO, + TenantUpdateDTO, + TenantResponseDTO, + TenantListResponseDTO, +) from .service import TenantService router = APIRouter(prefix="/tenants") @@ -17,11 +23,11 @@ router = APIRouter(prefix="/tenants") async def create_tenant( tenant_data: TenantCreateDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): """ Crea un nuevo tenant en el sistema - + Requiere rol: admin """ service = TenantService(db) @@ -34,29 +40,27 @@ async def list_tenants( page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), active_only: bool = Query(False, description="Solo tenants activos"), db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): """ Lista todos los tenants - + Requiere rol: admin """ service = TenantService(db) skip = (page - 1) * page_size tenants = service.list_tenants(skip=skip, limit=page_size, active_only=active_only) - + # Contar total from .models import Tenant + query = db.query(Tenant) if active_only: query = query.filter(Tenant.is_active == True) total = query.count() - + return TenantListResponseDTO( - tenants=tenants, - total=total, - page=page, - page_size=page_size + tenants=tenants, total=total, page=page, page_size=page_size ) @@ -64,7 +68,7 @@ async def list_tenants( async def get_tenant( tenant_id: int, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Obtiene información de un tenant por ID @@ -81,11 +85,11 @@ async def update_tenant( tenant_id: int, tenant_data: TenantUpdateDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): """ Actualiza un tenant - + Requiere rol: admin """ service = TenantService(db) @@ -99,11 +103,11 @@ async def update_tenant( async def delete_tenant( tenant_id: int, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): """ Elimina (desactiva) un tenant - + Requiere rol: admin """ service = TenantService(db) @@ -116,7 +120,7 @@ async def delete_tenant( async def get_tenant_by_slug( slug: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Obtiene un tenant por su slug diff --git a/backend/api/v1/modules/a76/tenants/service.py b/backend/api/v1/modules/a76/tenants/service.py index 7b19f056..779502be 100644 --- a/backend/api/v1/modules/a76/tenants/service.py +++ b/backend/api/v1/modules/a76/tenants/service.py @@ -1,6 +1,7 @@ """ Capa de servicio para lógica de negocio de tenants """ + from sqlalchemy.orm import Session from sqlalchemy.exc import IntegrityError from fastapi import HTTPException @@ -16,29 +17,34 @@ logger = logging.getLogger(__name__) class TenantService: """Servicio para gestión de tenants""" - + def __init__(self, db: Session): self.db = db - + def create_tenant(self, tenant_data: TenantCreateDTO) -> TenantResponseDTO: """ Crea un nuevo tenant en el sistema - + Args: tenant_data: Datos del tenant a crear - + Returns: TenantResponseDTO con información del tenant creado - + Raises: HTTPException: Si el slug o realm ya existen """ try: # Verificar que no exista el slug - existing = self.db.query(Tenant).filter(Tenant.slug == tenant_data.slug).first() + existing = ( + self.db.query(Tenant).filter(Tenant.slug == tenant_data.slug).first() + ) if existing: - raise HTTPException(status_code=400, detail=f"Tenant with slug '{tenant_data.slug}' already exists") - + raise HTTPException( + status_code=400, + detail=f"Tenant with slug '{tenant_data.slug}' already exists", + ) + # Crear tenant db_tenant = Tenant( name=tenant_data.name, @@ -48,35 +54,37 @@ class TenantService: contact_name=tenant_data.contact_name, contact_email=tenant_data.contact_email, contact_phone=tenant_data.contact_phone, - is_active=True + is_active=True, ) - + self.db.add(db_tenant) self.db.commit() self.db.refresh(db_tenant) - + logger.info(f"Tenant created: {db_tenant.id} - {db_tenant.name}") - + return TenantResponseDTO.model_validate(db_tenant) - + except IntegrityError as e: self.db.rollback() logger.error(f"IntegrityError creating tenant: {str(e)}") - raise HTTPException(status_code=400, detail="Tenant with this slug or realm already exists") + raise HTTPException( + status_code=400, detail="Tenant with this slug or realm already exists" + ) except HTTPException: raise except Exception as e: self.db.rollback() logger.error(f"Error creating tenant: {str(e)}") raise HTTPException(status_code=500, detail="Error creating tenant") - + def get_tenant(self, tenant_id: int) -> Optional[TenantResponseDTO]: """ Obtiene un tenant por ID - + Args: tenant_id: ID del tenant - + Returns: TenantResponseDTO o None si no existe """ @@ -84,54 +92,58 @@ class TenantService: if not tenant: return None return TenantResponseDTO.model_validate(tenant) - + def get_tenant_by_slug(self, slug: str) -> Optional[TenantResponseDTO]: """Obtiene un tenant por slug""" tenant = self.db.query(Tenant).filter(Tenant.slug == slug).first() if not tenant: return None return TenantResponseDTO.model_validate(tenant) - - def list_tenants(self, skip: int = 0, limit: int = 100, active_only: bool = False) -> List[TenantResponseDTO]: + + def list_tenants( + self, skip: int = 0, limit: int = 100, active_only: bool = False + ) -> List[TenantResponseDTO]: """ Lista todos los tenants - + Args: skip: Número de registros a omitir limit: Número máximo de registros a retornar active_only: Si True, solo retorna tenants activos - + Returns: Lista de TenantResponseDTO """ query = self.db.query(Tenant) - + if active_only: query = query.filter(Tenant.is_active == True) - + tenants = query.offset(skip).limit(limit).all() return [TenantResponseDTO.model_validate(t) for t in tenants] - - def update_tenant(self, tenant_id: int, tenant_data: TenantUpdateDTO) -> Optional[TenantResponseDTO]: + + def update_tenant( + self, tenant_id: int, tenant_data: TenantUpdateDTO + ) -> Optional[TenantResponseDTO]: """ Actualiza un tenant - + Args: tenant_id: ID del tenant a actualizar tenant_data: Datos a actualizar - + Returns: TenantResponseDTO actualizado o None si no existe """ tenant = self.db.query(Tenant).filter(Tenant.id == tenant_id).first() if not tenant: return None - + # Actualizar solo campos proporcionados update_data = tenant_data.model_dump(exclude_unset=True) for field, value in update_data.items(): setattr(tenant, field, value) - + try: self.db.commit() self.db.refresh(tenant) @@ -141,24 +153,24 @@ class TenantService: self.db.rollback() logger.error(f"Error updating tenant {tenant_id}: {str(e)}") raise HTTPException(status_code=500, detail="Error updating tenant") - + def delete_tenant(self, tenant_id: int) -> bool: """ Elimina (desactiva) un tenant - + Args: tenant_id: ID del tenant a eliminar - + Returns: True si se eliminó, False si no existe """ tenant = self.db.query(Tenant).filter(Tenant.id == tenant_id).first() if not tenant: return False - + # Soft delete: marcar como inactivo tenant.is_active = False - + try: self.db.commit() logger.info(f"Tenant deleted (soft): {tenant_id}") @@ -167,25 +179,27 @@ class TenantService: self.db.rollback() logger.error(f"Error deleting tenant {tenant_id}: {str(e)}") raise HTTPException(status_code=500, detail="Error deleting tenant") - - def upgrade_to_dedicated(self, tenant_id: int, db_config: dict) -> Optional[TenantResponseDTO]: + + def upgrade_to_dedicated( + self, tenant_id: int, db_config: dict + ) -> Optional[TenantResponseDTO]: """ Actualiza un tenant de BD compartida a BD dedicada - + Args: tenant_id: ID del tenant db_config: Configuración de BD dedicada - + Returns: TenantResponseDTO actualizado """ tenant = self.db.query(Tenant).filter(Tenant.id == tenant_id).first() if not tenant: return None - + tenant.type = TenantType.DEDICATED tenant.db_config = json.dumps(db_config) - + try: self.db.commit() self.db.refresh(tenant) diff --git a/backend/api/v1/modules/a76/user_tenant/dto.py b/backend/api/v1/modules/a76/user_tenant/dto.py index 4221d3cc..0575d23a 100644 --- a/backend/api/v1/modules/a76/user_tenant/dto.py +++ b/backend/api/v1/modules/a76/user_tenant/dto.py @@ -1,6 +1,7 @@ """ DTOs para gestión de relaciones usuario-tenant """ + from pydantic import BaseModel, Field from typing import Optional from datetime import datetime @@ -8,6 +9,7 @@ from datetime import datetime class AddUserToTenantRequestDTO(BaseModel): """Request para agregar un usuario a un tenant""" + keycloak_user_id: str = Field(..., description="ID del usuario en Keycloak") tenant_id: int = Field(..., description="ID del tenant") role: Optional[str] = Field(None, description="Rol del usuario en el tenant") @@ -15,6 +17,7 @@ class AddUserToTenantRequestDTO(BaseModel): class RemoveUserFromTenantRequestDTO(BaseModel): """Request para eliminar un usuario de un tenant""" + keycloak_user_id: str = Field(..., description="ID del usuario en Keycloak") tenant_id: int = Field(..., description="ID del tenant") soft_delete: bool = Field(True, description="Si True, desactiva. Si False, elimina") @@ -22,6 +25,7 @@ class RemoveUserFromTenantRequestDTO(BaseModel): class UpdateUserRoleRequestDTO(BaseModel): """Request para actualizar el rol de un usuario en un tenant""" + keycloak_user_id: str = Field(..., description="ID del usuario en Keycloak") tenant_id: int = Field(..., description="ID del tenant") role: str = Field(..., description="Nuevo rol del usuario") @@ -29,6 +33,7 @@ class UpdateUserRoleRequestDTO(BaseModel): class UserTenantResponseDTO(BaseModel): """Response con información de relación usuario-tenant""" + id: int keycloak_user_id: str tenant_id: int @@ -36,24 +41,26 @@ class UserTenantResponseDTO(BaseModel): role: Optional[str] created_at: datetime updated_at: datetime - + class Config: from_attributes = True class TenantBasicInfoDTO(BaseModel): """Información básica de un tenant""" + id: int name: str slug: str is_active: bool keycloak_realm: str - + class Config: from_attributes = True class UserTenantsResponseDTO(BaseModel): """Response con los tenants de un usuario""" + keycloak_user_id: str tenants: list[TenantBasicInfoDTO] diff --git a/backend/api/v1/modules/a76/user_tenant/models.py b/backend/api/v1/modules/a76/user_tenant/models.py index d67ce1ee..fbbee61d 100644 --- a/backend/api/v1/modules/a76/user_tenant/models.py +++ b/backend/api/v1/modules/a76/user_tenant/models.py @@ -1,7 +1,15 @@ """ Modelo de relación entre usuarios (Keycloak) y tenants """ -from sqlalchemy import Integer, String, DateTime, Boolean, UniqueConstraint, ForeignKeyConstraint, ForeignKey + +from sqlalchemy import ( + Integer, + String, + DateTime, + Boolean, + UniqueConstraint, + ForeignKeyConstraint, +) from sqlalchemy.sql import func from sqlalchemy.orm import Mapped, mapped_column, relationship from datetime import datetime @@ -11,42 +19,51 @@ from core.database import Base if TYPE_CHECKING: from api.v1.modules.a76.tenants.models import Tenant + class UserTenant(Base): """ Relación muchos-a-muchos entre usuarios de Keycloak y tenants - + Un usuario puede pertenecer a múltiples tenants Un tenant puede tener múltiples usuarios """ + __tablename__ = "user_tenants" __table_args__ = ( - ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id']), - UniqueConstraint('keycloak_user_id', 'tenant_id', name='uq_user_tenant'), - {"schema": "a76"} + ForeignKeyConstraint(["company_id"], ["a76.company.id"]), + ForeignKeyConstraint(["tenant_id"], ["a76.tenants.id"]), + UniqueConstraint( + "keycloak_user_id", "tenant_id", "company_id", name="uq_user_tenant" + ), + {"schema": "a76"}, ) - + # Primary Key id: Mapped[int] = mapped_column(primary_key=True, index=True) - + # ID del usuario en Keycloak (UUID string) - keycloak_user_id: Mapped[str] = mapped_column(String(255), nullable=False, index=True) - + keycloak_user_id: Mapped[str] = mapped_column( + String(255), nullable=False, index=True + ) + # ID del tenant tenant_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) - - # ID de la empresa asociada - company_id: Mapped[int] = mapped_column(Integer, ForeignKey("a76.companies.id"), nullable=False, index=True) - + company_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True) + # Estado de la relación is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False) - + # Información adicional - Rol del usuario en este tenant (opcional) role: Mapped[Optional[str]] = mapped_column(String(50), nullable=True) - + # Timestamps - created_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.now()) - updated_at: Mapped[datetime] = mapped_column(DateTime, nullable=False, server_default=func.now(), onupdate=func.now()) + created_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, server_default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + DateTime, nullable=False, server_default=func.now(), onupdate=func.now() + ) deleted_at: Mapped[Optional[datetime]] = mapped_column(DateTime, nullable=True) - + # Relación con Tenant tenant: Mapped["Tenant"] = relationship("Tenant", back_populates="user_relations") diff --git a/backend/api/v1/modules/a76/user_tenant/routes.py b/backend/api/v1/modules/a76/user_tenant/routes.py index cab811ba..13bc168d 100644 --- a/backend/api/v1/modules/a76/user_tenant/routes.py +++ b/backend/api/v1/modules/a76/user_tenant/routes.py @@ -1,6 +1,7 @@ """ Rutas para gestión de relaciones usuario-tenant """ + from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session from typing import List @@ -14,31 +15,26 @@ from .dto import ( UpdateUserRoleRequestDTO, UserTenantResponseDTO, UserTenantsResponseDTO, - TenantBasicInfoDTO + TenantBasicInfoDTO, ) -router = APIRouter( - prefix="/user-tenants", - tags=["User-Tenant Relations"] -) +router = APIRouter(prefix="/user-tenants", tags=["User-Tenant Relations"]) @router.post("/add", response_model=UserTenantResponseDTO) def add_user_to_tenant( data: AddUserToTenantRequestDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Agrega un usuario a un tenant - + Requiere permisos de administrador """ service = UserTenantService(db) result = service.add_user_to_tenant( - keycloak_user_id=data.keycloak_user_id, - tenant_id=data.tenant_id, - role=data.role + keycloak_user_id=data.keycloak_user_id, tenant_id=data.tenant_id, role=data.role ) return result @@ -47,18 +43,18 @@ def add_user_to_tenant( def remove_user_from_tenant( data: RemoveUserFromTenantRequestDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Elimina un usuario de un tenant - + Requiere permisos de administrador """ service = UserTenantService(db) service.remove_user_from_tenant( keycloak_user_id=data.keycloak_user_id, tenant_id=data.tenant_id, - soft_delete=data.soft_delete + soft_delete=data.soft_delete, ) return {"message": "User removed from tenant successfully"} @@ -67,18 +63,16 @@ def remove_user_from_tenant( def update_user_role( data: UpdateUserRoleRequestDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Actualiza el rol de un usuario en un tenant - + Requiere permisos de administrador """ service = UserTenantService(db) result = service.update_user_role_in_tenant( - keycloak_user_id=data.keycloak_user_id, - tenant_id=data.tenant_id, - role=data.role + keycloak_user_id=data.keycloak_user_id, tenant_id=data.tenant_id, role=data.role ) return result @@ -87,27 +81,26 @@ def update_user_role( def get_user_tenants( keycloak_user_id: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Obtiene todos los tenants a los que tiene acceso un usuario - + Los usuarios solo pueden ver sus propios tenants, a menos que sean admin """ # Verificar que el usuario solo pueda ver sus propios tenants (excepto admin) if current_user.get("sub") != keycloak_user_id: # TODO: Verificar si es admin raise HTTPException( - status_code=403, - detail="You can only view your own tenants" + status_code=403, detail="You can only view your own tenants" ) - + service = UserTenantService(db) tenants = service.get_user_tenants(keycloak_user_id) - + return UserTenantsResponseDTO( keycloak_user_id=keycloak_user_id, - tenants=[TenantBasicInfoDTO.model_validate(t) for t in tenants] + tenants=[TenantBasicInfoDTO.model_validate(t) for t in tenants], ) @@ -115,11 +108,11 @@ def get_user_tenants( def get_tenant_users( tenant_id: int, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Obtiene todos los usuarios que tienen acceso a un tenant - + Requiere permisos de administrador del tenant """ service = UserTenantService(db) @@ -132,16 +125,16 @@ def check_user_access( keycloak_user_id: str, tenant_id: int, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): """ Verifica si un usuario tiene acceso a un tenant """ service = UserTenantService(db) has_access = service.user_has_access_to_tenant(keycloak_user_id, tenant_id) - + return { "keycloak_user_id": keycloak_user_id, "tenant_id": tenant_id, - "has_access": has_access + "has_access": has_access, } diff --git a/backend/api/v1/modules/a76/user_tenant/service.py b/backend/api/v1/modules/a76/user_tenant/service.py index f93d4c03..e168a418 100644 --- a/backend/api/v1/modules/a76/user_tenant/service.py +++ b/backend/api/v1/modules/a76/user_tenant/service.py @@ -1,6 +1,7 @@ """ Servicio para gestionar relaciones entre usuarios y tenants """ + from sqlalchemy.orm import Session from sqlalchemy import and_ from typing import List, Optional @@ -15,24 +16,21 @@ logger = logging.getLogger(__name__) class UserTenantService: """Servicio para gestionar acceso de usuarios a tenants""" - + def __init__(self, db: Session): self.db = db - + def add_user_to_tenant( - self, - keycloak_user_id: str, - tenant_id: int, - role: Optional[str] = None + self, keycloak_user_id: str, tenant_id: int, role: Optional[str] = None ) -> UserTenant: """ Agrega un usuario a un tenant - + Args: keycloak_user_id: ID del usuario en Keycloak tenant_id: ID del tenant role: Rol opcional del usuario en este tenant - + Returns: UserTenant creado """ @@ -40,15 +38,19 @@ class UserTenantService: tenant = self.db.query(Tenant).filter(Tenant.id == tenant_id).first() if not tenant: raise HTTPException(status_code=404, detail="Tenant not found") - + # Verificar si la relación ya existe - existing = self.db.query(UserTenant).filter( - and_( - UserTenant.keycloak_user_id == keycloak_user_id, - UserTenant.tenant_id == tenant_id + existing = ( + self.db.query(UserTenant) + .filter( + and_( + UserTenant.keycloak_user_id == keycloak_user_id, + UserTenant.tenant_id == tenant_id, + ) ) - ).first() - + .first() + ) + if existing: # Si existe pero está inactiva, reactivarla if not existing.is_active: @@ -56,59 +58,60 @@ class UserTenantService: existing.role = role self.db.commit() self.db.refresh(existing) - logger.info(f"Reactivated user {keycloak_user_id} in tenant {tenant_id}") + logger.info( + f"Reactivated user {keycloak_user_id} in tenant {tenant_id}" + ) return existing else: raise HTTPException( - status_code=409, - detail="User already has access to this tenant" + status_code=409, detail="User already has access to this tenant" ) - + # Crear nueva relación user_tenant = UserTenant( keycloak_user_id=keycloak_user_id, tenant_id=tenant_id, role=role, - is_active=True + is_active=True, ) - + self.db.add(user_tenant) self.db.commit() self.db.refresh(user_tenant) - + logger.info(f"Added user {keycloak_user_id} to tenant {tenant_id}") return user_tenant - + def remove_user_from_tenant( - self, - keycloak_user_id: str, - tenant_id: int, - soft_delete: bool = True + self, keycloak_user_id: str, tenant_id: int, soft_delete: bool = True ) -> bool: """ Elimina un usuario de un tenant - + Args: keycloak_user_id: ID del usuario en Keycloak tenant_id: ID del tenant soft_delete: Si True, solo marca como inactivo. Si False, elimina físicamente - + Returns: True si se eliminó correctamente """ - user_tenant = self.db.query(UserTenant).filter( - and_( - UserTenant.keycloak_user_id == keycloak_user_id, - UserTenant.tenant_id == tenant_id + user_tenant = ( + self.db.query(UserTenant) + .filter( + and_( + UserTenant.keycloak_user_id == keycloak_user_id, + UserTenant.tenant_id == tenant_id, + ) ) - ).first() - + .first() + ) + if not user_tenant: raise HTTPException( - status_code=404, - detail="User-tenant relationship not found" + status_code=404, detail="User-tenant relationship not found" ) - + if soft_delete: user_tenant.is_active = False self.db.commit() @@ -117,112 +120,118 @@ class UserTenantService: self.db.delete(user_tenant) self.db.commit() logger.info(f"Deleted user {keycloak_user_id} from tenant {tenant_id}") - + return True - + def get_user_tenants(self, keycloak_user_id: str) -> List[Tenant]: """ Obtiene todos los tenants a los que tiene acceso un usuario - + Args: keycloak_user_id: ID del usuario en Keycloak - + Returns: Lista de tenants """ - user_tenants = self.db.query(UserTenant).filter( - and_( - UserTenant.keycloak_user_id == keycloak_user_id, - UserTenant.is_active == True + user_tenants = ( + self.db.query(UserTenant) + .filter( + and_( + UserTenant.keycloak_user_id == keycloak_user_id, + UserTenant.is_active == True, + ) ) - ).all() - + .all() + ) + tenant_ids = [ut.tenant_id for ut in user_tenants] - - tenants = self.db.query(Tenant).filter( - and_( - Tenant.id.in_(tenant_ids), - Tenant.is_active == True - ) - ).all() - + + tenants = ( + self.db.query(Tenant) + .filter(and_(Tenant.id.in_(tenant_ids), Tenant.is_active == True)) + .all() + ) + return tenants - + def get_tenant_users(self, tenant_id: int) -> List[UserTenant]: """ Obtiene todos los usuarios que tienen acceso a un tenant - + Args: tenant_id: ID del tenant - + Returns: Lista de relaciones UserTenant """ - return self.db.query(UserTenant).filter( - and_( - UserTenant.tenant_id == tenant_id, - UserTenant.is_active == True + return ( + self.db.query(UserTenant) + .filter( + and_(UserTenant.tenant_id == tenant_id, UserTenant.is_active == True) ) - ).all() - - def user_has_access_to_tenant( - self, - keycloak_user_id: str, - tenant_id: int - ) -> bool: + .all() + ) + + def user_has_access_to_tenant(self, keycloak_user_id: str, tenant_id: int) -> bool: """ Verifica si un usuario tiene acceso a un tenant - + Args: keycloak_user_id: ID del usuario en Keycloak tenant_id: ID del tenant - + Returns: True si tiene acceso, False en caso contrario """ - user_tenant = self.db.query(UserTenant).filter( - and_( - UserTenant.keycloak_user_id == keycloak_user_id, - UserTenant.tenant_id == tenant_id, - UserTenant.is_active == True + user_tenant = ( + self.db.query(UserTenant) + .filter( + and_( + UserTenant.keycloak_user_id == keycloak_user_id, + UserTenant.tenant_id == tenant_id, + UserTenant.is_active == True, + ) ) - ).first() - + .first() + ) + return user_tenant is not None - + def update_user_role_in_tenant( - self, - keycloak_user_id: str, - tenant_id: int, - role: str + self, keycloak_user_id: str, tenant_id: int, role: str ) -> UserTenant: """ Actualiza el rol de un usuario en un tenant - + Args: keycloak_user_id: ID del usuario en Keycloak tenant_id: ID del tenant role: Nuevo rol - + Returns: UserTenant actualizado """ - user_tenant = self.db.query(UserTenant).filter( - and_( - UserTenant.keycloak_user_id == keycloak_user_id, - UserTenant.tenant_id == tenant_id + user_tenant = ( + self.db.query(UserTenant) + .filter( + and_( + UserTenant.keycloak_user_id == keycloak_user_id, + UserTenant.tenant_id == tenant_id, + ) ) - ).first() - + .first() + ) + if not user_tenant: raise HTTPException( - status_code=404, - detail="User-tenant relationship not found" + status_code=404, detail="User-tenant relationship not found" ) - + user_tenant.role = role self.db.commit() self.db.refresh(user_tenant) - - logger.info(f"Updated role for user {keycloak_user_id} in tenant {tenant_id} to {role}") + + logger.info( + f"Updated role for user {keycloak_user_id} in tenant {tenant_id} to {role}" + ) return user_tenant diff --git a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/dto.py b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/dto.py index feba0810..b41c472d 100644 --- a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/dto.py +++ b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/dto.py @@ -2,6 +2,7 @@ from pydantic import BaseModel, Field from pydantic import ConfigDict from typing import Optional + class CodePedimentoRegimenDTO(BaseModel): id: Optional[int] = None pedimento_code: str = Field(..., min_length=1, max_length=3) diff --git a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/models.py b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/models.py index 075e8e2e..5f39b04c 100644 --- a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/models.py +++ b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/models.py @@ -1,5 +1,11 @@ from typing import TYPE_CHECKING, Optional -from sqlalchemy import String, Integer, ForeignKey, ForeignKeyConstraint, PrimaryKeyConstraint +from sqlalchemy import ( + String, + Integer, + ForeignKey, + ForeignKeyConstraint, + PrimaryKeyConstraint, +) from sqlalchemy.orm import mapped_column, Mapped, relationship from core.database import Base @@ -7,29 +13,36 @@ if TYPE_CHECKING: from ..pedimento_codes.models import PedimentoCode from ..pedimento_regimens.models import RegimenPedimento + class CodePedimentoRegimen(Base): - __tablename__ = "code_pedimento_regimens" #GClavePedRegimen + __tablename__ = "code_pedimento_regimens" # GClavePedRegimen __table_args__ = ( - ForeignKeyConstraint(['pedimento_code'], ['public.pedimento_codes.code'], name='fk_codeped'), - ForeignKeyConstraint(['regimen_code'], ['public.pedimento_regimens.code'], name='fk_regimenped'), - PrimaryKeyConstraint('id', name='clave_pedimento_regimens_pkey'), - {"schema": "public"} + ForeignKeyConstraint( + ["pedimento_code"], ["public.pedimento_codes.code"], name="fk_codeped" + ), + ForeignKeyConstraint( + ["regimen_code"], ["public.pedimento_regimens.code"], name="fk_regimenped" + ), + PrimaryKeyConstraint("id", name="clave_pedimento_regimens_pkey"), + {"schema": "public"}, ) id: Mapped[int] = mapped_column(Integer) pedimento_code: Mapped[str] = mapped_column(String(3), nullable=False) regimen_code: Mapped[Optional[str]] = mapped_column(String(3), nullable=False) - type_code: Mapped[Optional[str]] = mapped_column(String(1)) # si aplica un tipo de relación + type_code: Mapped[Optional[str]] = mapped_column( + String(1) + ) # si aplica un tipo de relación # Relaciones ORM - #GClavePed - pedimento: Mapped['PedimentoCode'] = relationship( - 'PedimentoCode', back_populates='regimens' + # GClavePed + pedimento: Mapped["PedimentoCode"] = relationship( + "PedimentoCode", back_populates="regimens" ) - #GRegimenPed - regimen: Mapped[Optional['RegimenPedimento']] = relationship( - 'RegimenPedimento', back_populates='claves_pedimento' + # GRegimenPed + regimen: Mapped[Optional["RegimenPedimento"]] = relationship( + "RegimenPedimento", back_populates="claves_pedimento" ) def __repr__(self): - return f"" \ No newline at end of file + return f"" diff --git a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/routes.py b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/routes.py index b0db57d2..d946e919 100644 --- a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/routes.py +++ b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/routes.py @@ -1,4 +1,3 @@ - from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db @@ -16,7 +15,7 @@ def list_code_pedimento_regimens( page: int = Query(1, ge=1, description="Número de página"), page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): skip = (page - 1) * page_size query = db.query(CodePedimentoRegimen) @@ -26,25 +25,27 @@ def list_code_pedimento_regimens( "items": [CodePedimentoRegimenDTO.model_validate(obj) for obj in items], "total": total, "page": page, - "page_size": page_size + "page_size": page_size, } + @router.get("/{id}", response_model=CodePedimentoRegimenDTO) def get_code_pedimento_regimen( id: int, db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): obj = db.query(CodePedimentoRegimen).filter(CodePedimentoRegimen.id == id).first() if not obj: raise HTTPException(status_code=404, detail="Not found") return CodePedimentoRegimenDTO.model_validate(obj) + @router.post("/", response_model=CodePedimentoRegimenDTO, status_code=201) def create_code_pedimento_regimen( data: CodePedimentoRegimenDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = CodePedimentoRegimen(**data.model_dump()) db.add(obj) @@ -52,12 +53,13 @@ def create_code_pedimento_regimen( db.refresh(obj) return CodePedimentoRegimenDTO.model_validate(obj) + @router.put("/{id}", response_model=CodePedimentoRegimenDTO) def update_code_pedimento_regimen( id: int, data: CodePedimentoRegimenDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(CodePedimentoRegimen).filter(CodePedimentoRegimen.id == id).first() if not obj: @@ -68,11 +70,12 @@ def update_code_pedimento_regimen( db.refresh(obj) return CodePedimentoRegimenDTO.model_validate(obj) + @router.delete("/{id}", status_code=204) def delete_code_pedimento_regimen( id: int, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(CodePedimentoRegimen).filter(CodePedimentoRegimen.id == id).first() if not obj: diff --git a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/seed.py b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/seed.py index f71a7600..1df2120c 100644 --- a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/seed.py +++ b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/seed.py @@ -33,8 +33,8 @@ seed = [ ("H1", "EXD", "E"), ("H8", "EXD", "E"), ("I1", "EXD", "E"), - #("J1", "EXD", "E"), - #("J2", "EXD", "E"), + # ("J1", "EXD", "E"), + # ("J2", "EXD", "E"), ("K1", "EXD", "E"), ("K2", "EXD", "E"), ("K3", "EXD", "E"), @@ -53,7 +53,7 @@ seed = [ ("A1", "IMD", "I"), ("A3", "IMD", "I"), ("C1", "IMD", "I"), - #("C2", "IMD", "I"), + # ("C2", "IMD", "I"), ("C3", "IMD", "I"), ("D1", "IMD", "I"), ("F3", "IMD", "I"), @@ -78,19 +78,19 @@ seed = [ ("V9", "IMD", "I"), ("VF", "IMD", "I"), ("VU", "IMD", "I"), - #("A2", "ITE", "I"), - #("A8", "ITE", "I"), - #("AA", "ITE", "I"), + # ("A2", "ITE", "I"), + # ("A8", "ITE", "I"), + # ("AA", "ITE", "I"), ("AF", "ITE", "I"), ("E1", "ITE", "I"), ("E3", "ITE", "I"), - #("H3", "ITE", "I"), + # ("H3", "ITE", "I"), ("IN", "ITE", "I"), ("R1", "ITE", "I"), ("V1", "ITE", "I"), ("A6", "ITR", "I"), - #("A7", "ITR", "I"), - #("A9", "ITR", "I"), + # ("A7", "ITR", "I"), + # ("A9", "ITR", "I"), ("AD", "ITR", "I"), ("AF", "ITR", "I"), ("AJ", "ITR", "I"), @@ -104,7 +104,7 @@ seed = [ ("BP", "ITR", "I"), ("E2", "ITR", "I"), ("E4", "ITR", "I"), - #("H3", "ITR", "I"), + # ("H3", "ITR", "I"), ("R1", "ITR", "I"), ("V1", "ITR", "I"), ("V4", "ITR", "I"), @@ -120,5 +120,5 @@ seed = [ ("T3", "TRA", "I"), ("T6", "TRA", "E"), ("T7", "TRA", "I"), - ("T9", "TRA", "I") -] \ No newline at end of file + ("T9", "TRA", "I"), +] diff --git a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/test_code_pedimento_regimens.py b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/test_code_pedimento_regimens.py index bd60c611..c0eb35f7 100644 --- a/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/test_code_pedimento_regimens.py +++ b/backend/api/v1/modules/public/reference_data/code_pedimento_regimens/test_code_pedimento_regimens.py @@ -7,6 +7,7 @@ app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_code_pedimento_regimens(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,20 +17,28 @@ def test_list_code_pedimento_regimens(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_code_pedimento_regimen_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/code-pedimento-regimens/999999", headers=headers) assert response.status_code == 404 + def test_create_code_pedimento_regimen_forbidden(): - response = client.post("/code-pedimento-regimens/", json={"id": 999999, "description": "Test"}) + response = client.post( + "/code-pedimento-regimens/", json={"id": 999999, "description": "Test"} + ) assert response.status_code in (403, 405, 404) + def test_update_code_pedimento_regimen_forbidden(): - response = client.put("/code-pedimento-regimens/999999", json={"id": 999999, "description": "Test"}) + response = client.put( + "/code-pedimento-regimens/999999", json={"id": 999999, "description": "Test"} + ) assert response.status_code in (403, 405, 404) + def test_delete_code_pedimento_regimen_forbidden(): response = client.delete("/code-pedimento-regimens/999999") assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/conftest.py b/backend/api/v1/modules/public/reference_data/conftest.py index 398c8e3f..a31d4689 100644 --- a/backend/api/v1/modules/public/reference_data/conftest.py +++ b/backend/api/v1/modules/public/reference_data/conftest.py @@ -3,11 +3,13 @@ from fastapi.testclient import TestClient from api.v1.modules.public.reference_data.transport_types.routes import router from fastapi import FastAPI + @pytest.fixture(scope="session") def access_token(): # Reemplaza este token por uno válido generado por Keycloak return "aqui-va-tu-token-valido" + @pytest.fixture(scope="session") def client(): app = FastAPI() diff --git a/backend/api/v1/modules/public/reference_data/containers/dto.py b/backend/api/v1/modules/public/reference_data/containers/dto.py index baf3f921..872729ce 100644 --- a/backend/api/v1/modules/public/reference_data/containers/dto.py +++ b/backend/api/v1/modules/public/reference_data/containers/dto.py @@ -1,9 +1,9 @@ from pydantic import BaseModel, Field from pydantic import ConfigDict + class ContainerDTO(BaseModel): key: str = Field(..., min_length=1, max_length=3) description: str model_config = ConfigDict(from_attributes=True) - diff --git a/backend/api/v1/modules/public/reference_data/containers/models.py b/backend/api/v1/modules/public/reference_data/containers/models.py index c53cebac..94ad14b6 100644 --- a/backend/api/v1/modules/public/reference_data/containers/models.py +++ b/backend/api/v1/modules/public/reference_data/containers/models.py @@ -2,15 +2,20 @@ from sqlalchemy import String, PrimaryKeyConstraint from sqlalchemy.orm import mapped_column, Mapped from core.database import Base + class Container(Base): - __tablename__ = "containers" #GContenedores + __tablename__ = "containers" # GContenedores __table_args__ = ( PrimaryKeyConstraint("key", name="containers_pkey"), - {"schema": "public"} # opcional + {"schema": "public"}, # opcional ) - key: Mapped[str] = mapped_column(String(3), nullable=False) # mantiene ceros iniciales - description: Mapped[str] = mapped_column(String(500), nullable=False) # descripción legal en español + key: Mapped[str] = mapped_column( + String(3), nullable=False + ) # mantiene ceros iniciales + description: Mapped[str] = mapped_column( + String(500), nullable=False + ) # descripción legal en español def __repr__(self): - return f"" \ No newline at end of file + return f"" diff --git a/backend/api/v1/modules/public/reference_data/containers/routes.py b/backend/api/v1/modules/public/reference_data/containers/routes.py index 73265a2a..944098b4 100644 --- a/backend/api/v1/modules/public/reference_data/containers/routes.py +++ b/backend/api/v1/modules/public/reference_data/containers/routes.py @@ -1,4 +1,3 @@ - from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db @@ -11,13 +10,12 @@ from typing import Any, Dict router = APIRouter(prefix="/containers") - @router.get("/", response_model=Dict[str, Any]) async def list_containers( page: int = Query(1, ge=1, description="Número de página"), page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): skip = (page - 1) * page_size query = db.query(Container) @@ -27,23 +25,27 @@ async def list_containers( "items": [ContainerDTO.model_validate(obj) for obj in items], "total": total, "page": page, - "page_size": page_size + "page_size": page_size, } - + @router.get("/{key}", response_model=ContainerDTO) -async def get_container(key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): +async def get_container( + key: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): obj = db.query(Container).filter(Container.key == key).first() if not obj: raise HTTPException(status_code=404, detail="Not found") return obj - + @router.post("/", response_model=ContainerDTO, status_code=201) async def create_container( data: ContainerDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = Container(**data.dict()) db.add(obj) @@ -51,13 +53,13 @@ async def create_container( db.refresh(obj) return obj - + @router.put("/{key}", response_model=ContainerDTO) async def update_container( key: str, data: ContainerDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(Container).filter(Container.key == key).first() if not obj: @@ -68,12 +70,12 @@ async def update_container( db.refresh(obj) return obj - + @router.delete("/{key}", status_code=204) async def delete_container( key: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(Container).filter(Container.key == key).first() if not obj: diff --git a/backend/api/v1/modules/public/reference_data/containers/seed.py b/backend/api/v1/modules/public/reference_data/containers/seed.py index 22e85145..84ceb7bf 100644 --- a/backend/api/v1/modules/public/reference_data/containers/seed.py +++ b/backend/api/v1/modules/public/reference_data/containers/seed.py @@ -1,13 +1,13 @@ seed = [ - ("1", "CONTENEDOR ESTANDAR 20' (STANDARD CONTAINER 20')."), - ("2", "CONTENEDOR ESTANDAR 40' (STANDARD CONTAINER 40')."), - ("3", "CONTENEDOR ESTANDAR DE CUBO ALTO 40' (HIGH CUBE STANDARD CONTAINER 40')."), - ("4", "CONTENEDOR TAPA DURA 20’ (HARDTOP CONTAINER 20')."), - ("5", "CONTENEDOR TAPA DURA 40’ (HARDTOP CONTAINER 40')."), - ("6", "CONTENEDOR TAPA ABIERTA 20’ (OPEN TOP CONTAINER 20')."), - ("7", "CONTENEDOR TAPA ABIERTA 40' (OPEN TOP CONTAINER 40')."), - ("8", "FLAT 20' (FLAT 20')."), - ("9", "FLAT 40' (FLAT 40')."), + ("1", "CONTENEDOR ESTANDAR 20' (STANDARD CONTAINER 20')."), + ("2", "CONTENEDOR ESTANDAR 40' (STANDARD CONTAINER 40')."), + ("3", "CONTENEDOR ESTANDAR DE CUBO ALTO 40' (HIGH CUBE STANDARD CONTAINER 40')."), + ("4", "CONTENEDOR TAPA DURA 20’ (HARDTOP CONTAINER 20')."), + ("5", "CONTENEDOR TAPA DURA 40’ (HARDTOP CONTAINER 40')."), + ("6", "CONTENEDOR TAPA ABIERTA 20’ (OPEN TOP CONTAINER 20')."), + ("7", "CONTENEDOR TAPA ABIERTA 40' (OPEN TOP CONTAINER 40')."), + ("8", "FLAT 20' (FLAT 20')."), + ("9", "FLAT 40' (FLAT 40')."), ("10", "PLATAFORMA 20' (PLATFORM 20')."), ("11", "PLATAFORMA 40' (PLATFORM 40')."), ("12", "CONTENEDOR VENTILADO 20’ (VENTILATED CONTAINER 20')."), @@ -15,9 +15,12 @@ seed = [ ("14", "CONTENEDOR TERMICO 40' (INSULATED CONTAINER 40')."), ("15", "CONTENEDOR REFRIGERANTE 20’ (REFRIGERATED CONTAINER 20')."), ("16", "CONTENEDOR REFRIGERANTE 40’ (REFRIGERATED CONTAINER 40')."), - ("17", "CONTENEDOR REFRIGERANTE CUBO ALTO 40’ (HIGH CUBE REFRIGERATED CONTAINER 40')."), + ( + "17", + "CONTENEDOR REFRIGERANTE CUBO ALTO 40’ (HIGH CUBE REFRIGERATED CONTAINER 40').", + ), ("18", "CONTENEDOR CARGA A GRANEL 20’ (BULK CONTAINER 20')."), - ("19", "CONTENEDOR TIPO TANQUE 20’ (TANK CONTAINER 20')."), + ("19", "CONTENEDOR TIPO TANQUE 20’ (TANK CONTAINER 20')."), ("20", "CONTENEDOR ESTANDAR 45' (STANDARD CONTAINER 45')."), ("21", "CONTENEDOR ESTANDAR 48' (STANDARD CONTAINER 48')."), ("22", "CONTENEDOR ESTANDAR 53' (STANDARD CONTAINER 53')."), @@ -27,7 +30,7 @@ seed = [ ("26", "SEMIRREMOLQUE CON RACKS PARA ENVASES DE BEBIDAS."), ("27", "SEMIRREMOLQUE CUELLO DE GANZO."), ("28", "SEMIRREMOLQUE TOLVA CUBIERTO."), - ("29", "SEMIRREMOLQUE TOLVA (ABIERTO)."), + ("29", "SEMIRREMOLQUE TOLVA (ABIERTO)."), ("30", "AUTO-TOLVA CUBIERTO/DESCARGA NEUMATICA."), ("31", "SEMIRREMOLQUE CHASIS."), ("32", "SEMIRREMOLQUE AUTOCARGABLE (CON SISTEMA DE ELEVACION)."), @@ -37,7 +40,7 @@ seed = [ ("36", "PLATAFORMA DE 28’."), ("37", "PLATAFORMA DE 45’."), ("38", "PLATAFORMA DE 48’."), - ("39", "SEMIRREMOLQUE PARA TRANSPORTE DE CABALLOS."), + ("39", "SEMIRREMOLQUE PARA TRANSPORTE DE CABALLOS."), ("40", "SEMIRREMOLQUE PARA TRANSPORTE DE GANADO."), ("41", "SEMIRREMOLQUE TANQUE (LIQUIDOS)/SIN CALEFACCION/SIN AISLAR."), ("42", "SEMIRREOLQUE TANQUE (LIQUIDOS)/CON CALEFACCION/SIN AISLAR."), @@ -47,7 +50,7 @@ seed = [ ("46", "SEMIRREMOLQUE TANQUE (GAS)/CON CALEFACCION/SIN AISLAR."), ("47", "SEMIRREMOLQUE TANQUE (GAS)/SIN CALEFACCION/AISLADO."), ("48", "SEMIRREMOLQUE TANQUE (GAS)/CON CALEFACCION/AISLADO."), - ("49", "SEMIRREMOLQUE TANQUE (QUIMICOS)/SIN CALEFACCION/SIN AISLAR."), + ("49", "SEMIRREMOLQUE TANQUE (QUIMICOS)/SIN CALEFACCION/SIN AISLAR."), ("50", "SEMIRREMOLQUE TANQUE (QUIMICOS)/CON CALEFACCION/SIN AISLAR."), ("51", "SEMIRREMOLQUE TANQUE (QUIMICOS)/SIN CALEFACCION/AISLADO."), ("52", "SEMIRREMOLQUE TANQUE (QUIMICOS)/CON CALEFACCION/AISLADO."), @@ -68,4 +71,4 @@ seed = [ ("67", "CAMIÓN UNITARIO DE TRES EJES"), ("68", "VEHÍCULOS CON CAPACIDAD DE CARGA DE HASTA 3.5. TONELADAS"), ("69", "TRACTOCAMIÓN"), -] \ No newline at end of file +] diff --git a/backend/api/v1/modules/public/reference_data/containers/test_containers.py b/backend/api/v1/modules/public/reference_data/containers/test_containers.py index 392dd06e..c6381063 100644 --- a/backend/api/v1/modules/public/reference_data/containers/test_containers.py +++ b/backend/api/v1/modules/public/reference_data/containers/test_containers.py @@ -7,6 +7,7 @@ app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_containers(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,20 +17,24 @@ def test_list_containers(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_container_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/containers/invalid_key", headers=headers) assert response.status_code == 404 + def test_create_container_forbidden(): response = client.post("/containers/", json={"key": "TST", "description": "Test"}) assert response.status_code in (403, 405, 404) + def test_update_container_forbidden(): response = client.put("/containers/TST", json={"key": "TST", "description": "Test"}) assert response.status_code in (403, 405, 404) + def test_delete_container_forbidden(): response = client.delete("/containers/TST") assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/countries/dto.py b/backend/api/v1/modules/public/reference_data/countries/dto.py index 6762b39a..8814972f 100644 --- a/backend/api/v1/modules/public/reference_data/countries/dto.py +++ b/backend/api/v1/modules/public/reference_data/countries/dto.py @@ -1,6 +1,7 @@ from pydantic import BaseModel, Field from pydantic import ConfigDict + class CountryDTO(BaseModel): m3_key: str = Field(..., min_length=1, max_length=3) mex_key: str = Field(..., min_length=1, max_length=2) @@ -9,4 +10,3 @@ class CountryDTO(BaseModel): description_en: str model_config = ConfigDict(from_attributes=True) - diff --git a/backend/api/v1/modules/public/reference_data/countries/models.py b/backend/api/v1/modules/public/reference_data/countries/models.py index 6e434af2..315d2cf1 100644 --- a/backend/api/v1/modules/public/reference_data/countries/models.py +++ b/backend/api/v1/modules/public/reference_data/countries/models.py @@ -2,19 +2,26 @@ from sqlalchemy import String, PrimaryKeyConstraint, Index from sqlalchemy.orm import mapped_column, Mapped from core.database import Base + class Country(Base): - __tablename__ = "countries" #GPaises + __tablename__ = "countries" # GPaises __table_args__ = ( PrimaryKeyConstraint("m3_key", name="countries_pkey"), Index("ak_country_ame", "ame_key", unique=True), - {"schema": "public"} # opcional + {"schema": "public"}, # opcional ) - m3_key: Mapped[str] = mapped_column(String(3), nullable=False) # clave M3 - mex_key: Mapped[str] = mapped_column(String(2), nullable=False) # clave país México - ame_key: Mapped[str] = mapped_column(String(2), nullable=False) # clave país América / regional - description_es: Mapped[str] = mapped_column(String(50), nullable=False) # nombre oficial en español - description_en: Mapped[str] = mapped_column(String(50), nullable=False) # nombre en inglés para UI + m3_key: Mapped[str] = mapped_column(String(3), nullable=False) # clave M3 + mex_key: Mapped[str] = mapped_column(String(2), nullable=False) # clave país México + ame_key: Mapped[str] = mapped_column( + String(2), nullable=False + ) # clave país América / regional + description_es: Mapped[str] = mapped_column( + String(50), nullable=False + ) # nombre oficial en español + description_en: Mapped[str] = mapped_column( + String(50), nullable=False + ) # nombre en inglés para UI def __repr__(self): return f"" diff --git a/backend/api/v1/modules/public/reference_data/countries/routes.py b/backend/api/v1/modules/public/reference_data/countries/routes.py index c2271122..0fdb0ee1 100644 --- a/backend/api/v1/modules/public/reference_data/countries/routes.py +++ b/backend/api/v1/modules/public/reference_data/countries/routes.py @@ -1,4 +1,3 @@ - from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db @@ -11,13 +10,12 @@ from typing import Any, Dict router = APIRouter(prefix="/countries") - @router.get("/", response_model=Dict[str, Any]) async def list_countries( page: int = Query(1, ge=1, description="Número de página"), page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): skip = (page - 1) * page_size query = db.query(Country) @@ -27,23 +25,27 @@ async def list_countries( "items": [CountryDTO.model_validate(obj) for obj in items], "total": total, "page": page, - "page_size": page_size + "page_size": page_size, } - + @router.get("/{m3_key}", response_model=CountryDTO) -async def get_country(m3_key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): +async def get_country( + m3_key: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): obj = db.query(Country).filter(Country.m3_key == m3_key).first() if not obj: raise HTTPException(status_code=404, detail="Not found") return obj - + @router.post("/", response_model=CountryDTO, status_code=201) async def create_country( data: CountryDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = Country(**data.dict()) db.add(obj) @@ -51,13 +53,13 @@ async def create_country( db.refresh(obj) return obj - + @router.put("/{m3_key}", response_model=CountryDTO) async def update_country( m3_key: str, data: CountryDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(Country).filter(Country.m3_key == m3_key).first() if not obj: @@ -68,12 +70,12 @@ async def update_country( db.refresh(obj) return obj - + @router.delete("/{m3_key}", status_code=204) async def delete_country( m3_key: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(Country).filter(Country.m3_key == m3_key).first() if not obj: diff --git a/backend/api/v1/modules/public/reference_data/countries/seed.py b/backend/api/v1/modules/public/reference_data/countries/seed.py index 33fce2d3..bba72c82 100644 --- a/backend/api/v1/modules/public/reference_data/countries/seed.py +++ b/backend/api/v1/modules/public/reference_data/countries/seed.py @@ -1,15 +1,45 @@ seed = [ - ("ABW", "A0", "AW", "Aruba (Territorio Holandes de Ultramar)", "Aruba (Netherlands Territory)"), - ("AFG", "A1", "AF", "Afganistan (Emirato Islamico De)", "Afghanistan (Islamic Emirate of)"), + ( + "ABW", + "A0", + "AW", + "Aruba (Territorio Holandes de Ultramar)", + "Aruba (Netherlands Territory)", + ), + ( + "AFG", + "A1", + "AF", + "Afganistan (Emirato Islamico De)", + "Afghanistan (Islamic Emirate of)", + ), ("AGO", "A8", "AO", "Angola ( Republica De )", "Angola (People's Republic of )"), ("AIA", "AI", "AI", "Anguila", "Anguilla"), - ("ALB", "A2", "AL", "Albania ( Republica De)", "Albania (People's Socialist Republic)"), + ( + "ALB", + "A2", + "AL", + "Albania ( Republica De)", + "Albania (People's Socialist Republic)", + ), ("AND", "A7", "AD", "Andorra (Principado De)", "Andorra (Principated of)"), - ("ANT", "B1", "AN", "Antillas Neerlandesas (Terr. Holandes de Ultramar)", "Antilles Netherlands"), + ( + "ANT", + "B1", + "AN", + "Antillas Neerlandesas (Terr. Holandes de Ultramar)", + "Antilles Netherlands", + ), ("ARE", "G6", "AE", "Emiratos Arabes Unidos", "United Arab Emirates"), ("ARG", "B4", "AR", "Argentina ( Republica )", "Argentina (Republic of)"), ("ARM", "AM", "AM", "Armenia (Republica De)", "Armenia (Republic of)"), - ("ATG", "A9", "AG", "Antigua Y Barbuda (Com. Britanica de Naciones)", "Antigua & Barbuda (Brithish Community)"), + ( + "ATG", + "A9", + "AG", + "Antigua Y Barbuda (Com. Britanica de Naciones)", + "Antigua & Barbuda (Brithish Community)", + ), ("AUS", "B5", "AU", "Australia ( Comunidad De )", "Australia (Community of)"), ("AUT", "B6", "AT", "Austria ( Republica De )", "Austria (Republic of)"), ("AZE", "AZ", "AZ", "Azerbaijan (Republica Azerbaijani)", "Azerbaijan"), @@ -17,7 +47,13 @@ seed = [ ("BEL", "C2", "BE", "Belgica ( Reino De )", "Belgium (Kingdom of)"), ("BEN", "F9", "BJ", "Benin ( Republica De)", "Benin (People's Republic of)"), ("BFA", "A6", "BF", "Burkina Faso", "Burkina Faso"), - ("BGD", "B9", "BD", "Bangladesh ( Republica Popular De )", "Bangladesh (People's Republic of)"), + ( + "BGD", + "B9", + "BD", + "Bangladesh ( Republica Popular De )", + "Bangladesh (People's Republic of)", + ), ("BGR", "D1", "BG", "Bulgaria ( Republica De )", "Bulgaria (Republic of)"), ("BHR", "B8", "BH", "Bahrein ( Estado De )", "Bahrain (State of)"), ("BHS", "B7", "BS", "Bahamas( Comunidad De Las )", "Bahamas (Community of the )"), @@ -26,20 +62,50 @@ seed = [ ("BLZ", "C3", "BZ", "Belice", "Belize"), ("BMU", "C4", "BM", "Bermudas", "Bermuda"), ("BOL", "C6", "BO", "Bolivia ( Republica De )", "Bolivia (Republic of)"), - ("BRA", "C8", "BR", "Brasil (Republica Federativa De)", "Brazil (Federative Republic of)"), - ("BRB", "C1", "BB", "Barbados (Comunidad Britanica de Naciones)", "Barbados (Brithish Community of Nations)"), + ( + "BRA", + "C8", + "BR", + "Brasil (Republica Federativa De)", + "Brazil (Federative Republic of)", + ), + ( + "BRB", + "C1", + "BB", + "Barbados (Comunidad Britanica de Naciones)", + "Barbados (Brithish Community of Nations)", + ), ("BRN", "C9", "BN", "Brunei (Estado De)(Residencia de Paz)", "Brunei (State of)"), ("BTN", "D3", "BT", "Butan (Reino De )", "Bhutan (Royal Goverment of)"), ("BUR", "BU", "BU", "Burma ( Birmania )", "Burma (Birmany)"), ("BWA", "C7", "BW", "Bostwana ( Republica De )", "Botswana (Republic of)"), ("CAF", "CF", "RB", "Republica Centro Africana", "Central African Republic"), ("CAN", "D9", "CA", "Canada", "Canada"), - ("CCK", "E3", "CC", "Cocos ( Keeling, Islas Australianas)", "Cocos Keeling Islands (Australian Island"), + ( + "CCK", + "E3", + "CC", + "Cocos ( Keeling, Islas Australianas)", + "Cocos Keeling Islands (Australian Island", + ), ("CHE", "U8", "CH", "Suiza (Confederacion)", "Switzerland (Confederation)"), ("CHL", "F6", "CL", "Chile ( Republica De )", "Chile (Republic of)"), - ("CHN", "Z3", "CN", "China ( Republica Popular) Derogado", "China (People's Republic of)"), + ( + "CHN", + "Z3", + "CN", + "China ( Republica Popular) Derogado", + "China (People's Republic of)", + ), ("CIA", "E2", "VA", "Ciudad Del Vaticano ( Estado De La )", "Vatican City State"), - ("CIV", "F1", "CT", "Costa de Marfil (Republica De La)", "Ivory Coast (Republic of)"), + ( + "CIV", + "F1", + "CT", + "Costa de Marfil (Republica De La)", + "Ivory Coast (Republic of)", + ), ("CMR", "D8", "CM", "Camerun ( Republica Del )", "Cameroon (Republic of the)"), ("COG", "E6", "CG", "Congo ( Republica Del )", " Congo (Republic of the)"), ("COK", "E7", "CK", "Cook ( Islas )", "Cook Islands"), @@ -48,50 +114,134 @@ seed = [ ("CPV", "D4", "CV", "Cabo Verde ( Republica De )", "Cape Verde (Republic of)"), ("CRI", "F2", "CR", "Costa Rica ( Republica De )", "Costa Rica (Republic of)"), ("CUB", "F3", "CU", "Cuba ( Republica De )", "Cuba (Republic of)"), - ("CUR", "D0", "UR", "Curazao (Terr. Holandes De Ultramar)", "Curazao (Netherlands Territory)"), + ( + "CUR", + "D0", + "UR", + "Curazao (Terr. Holandes De Ultramar)", + "Curazao (Netherlands Territory)", + ), ("CXI", "N8", "CX", "Navidad ( Christmas ) ( Islas )", "Christmas Islands"), ("CYM", "D6", "KY", "Caiman ( Islas )", "Cayman Islands"), ("CYP", "F8", "CY", "Chipre ( Republica De )", "Cyprus (Island of)"), ("CZE", "CZ", "CZ", "Republica Checa", "Czech Federative Republic"), - ("DEU", "A4", "DE", "Alemania ( Republica Federal De )", "Germany (Federal Republic of)"), + ( + "DEU", + "A4", + "DE", + "Alemania ( Republica Federal De )", + "Germany (Federal Republic of)", + ), ("DJI", "V4", "DJ", "Djibouti ( Republica De )", "Djibouti (Republic of)"), ("DMA", "G2", "DM", "Dominica ( Comunidad De )", "Dominica (Community of)"), ("DNK", "G1", "DK", "Dinamarca ( Reino De )", "Denmark (Kingdom of)"), ("DOM", "S2", "DO", "Republica Dominicana", "Dominican Republic"), - ("DSM", "FM", "FM", "Estado Federado De Micronesia", "Micronesia Federated State of"), - ("DZA", "B3", "DZ", "Argelia ( Republica Democratica y Popular de) Dero", "Argelia (People's Democratic Republic)"), + ( + "DSM", + "FM", + "FM", + "Estado Federado De Micronesia", + "Micronesia Federated State of", + ), + ( + "DZA", + "B3", + "DZ", + "Argelia ( Republica Democratica y Popular de) Dero", + "Argelia (People's Democratic Republic)", + ), ("ECU", "G3", "EC", "Ecuador ( Republica Del)", "Ecuador (Republic of the)"), ("EGY", "G4", "EG", "Egipto ( Republica Arabe De )", "Egypt (Arab Republic of)"), ("EMU", "EU", "EU", "Comunidad Europea", "European Economic Community"), ("ERI", "ER", "ER", "Eritrea (Estado De)", "Eritrea (State of)"), - ("ESH", "EH", "EH", "Sahara Occidental (Rep. Arabe Saharavi Dem.)", "Western Sahara (Arab Democratic Rep.)"), + ( + "ESH", + "EH", + "EH", + "Sahara Occidental (Rep. Arabe Saharavi Dem.)", + "Western Sahara (Arab Democratic Rep.)", + ), ("ESP", "G7", "ES", "España ( Reino De )", "Spain (Kingdom of)"), ("EST", "G0", "EE", "Estonia (Republica De)", "Estonia (Republic of)"), - ("ETH", "G9", "ET", "Etiopia ( Republica Democratica Federal)", "Ethiopia (Federal Democratic Republic)"), + ( + "ETH", + "G9", + "ET", + "Etiopia ( Republica Democratica Federal)", + "Ethiopia (Federal Democratic Republic)", + ), ("FIN", "H4", "FI", "Finlandia ( Republica De )", "Finland (Republic of)"), ("FJI", "H1", "FJ", "Fidji (Republica De )", "Fiji Islands"), ("FLK", "FK", "IV", "Islas Malvinas (R.U.)", "Malvine Islands"), ("FRA", "H5", "FR", "Francia (Republica Francesa)", "France (Republic)"), - ("FXA", "TF", "TF", "Territorios Franceses Austriales y Antarticos", "French Territory of Antartic Austral"), + ( + "FXA", + "TF", + "TF", + "Territorios Franceses Austriales y Antarticos", + "French Territory of Antartic Austral", + ), ("GAB", "H6", "GA", "Gabonesa ( Republica )", "Gabonese Republic"), - ("GBR", "R9", "GB", "Reino Unido de la Gran Bretaña e Irlanda del Norte", "United Kingdom (Great Britain, Ireland N"), + ( + "GBR", + "R9", + "GB", + "Reino Unido de la Gran Bretaña e Irlanda del Norte", + "United Kingdom (Great Britain, Ireland N", + ), ("GEO", "GE", "GE", "Georgia (Republica De)", "Georgia (Republic of)"), ("GHA", "H8", "GH", "Ghana ( Republica De )", "Ghana (Republic of)"), ("GIB", "GI", "GI", "Gibraltar (R.U.)", "Gibraltar (U. K.)"), ("GIN", "I8", "GN", "Guinea ( Republica De )", "Guinea (Republic of)"), - ("GLP", "I4", "GP", "Guadalupe (Departamento De)", "Guadeloupe (French Caribean Dependences)"), + ( + "GLP", + "I4", + "GP", + "Guadalupe (Departamento De)", + "Guadeloupe (French Caribean Dependences)", + ), ("GMB", "H7", "GM", "Gambia ( Republica De La)", "Gambia (Republic of)"), - ("GNB", "J1", "GW", "Guinea-Bissau ( Republica De )", "Guinea-Bissau (Republic of)"), - ("GNQ", "I9", "GQ", "Guinea Ecuatorial ( Republica De )", "Equatorial Guinea (Republic of)"), - ("GRC", "I2", "GR", "Grecia (Republica Helenica)", "Greece (Helenical Republic of)"), + ( + "GNB", + "J1", + "GW", + "Guinea-Bissau ( Republica De )", + "Guinea-Bissau (Republic of)", + ), + ( + "GNQ", + "I9", + "GQ", + "Guinea Ecuatorial ( Republica De )", + "Equatorial Guinea (Republic of)", + ), + ( + "GRC", + "I2", + "GR", + "Grecia (Republica Helenica)", + "Greece (Helenical Republic of)", + ), ("GRD", "I1", "GD", "Granada", "Grenada"), ("GRL", "GL", "GL", "Groenlandia (Dinamarca)", "Greenland (Denmark)"), ("GTM", "I6", "GT", "Guatemala ( Republica De )", "Guatemala (Republic of)"), ("GUF", "I7", "GF", "Guyana Francesa", "French Guyana"), ("GUM", "I5", "GU", "Guam ( E.U.A )", "Guam (U.S.A.)"), - ("GUY", "J2", "GY", "Guyana ( Republica Cooperativa De )", "Guyana (Cooperative Republic of)"), + ( + "GUY", + "J2", + "GY", + "Guyana ( Republica Cooperativa De )", + "Guyana (Cooperative Republic of)", + ), ("GZA", "GZ", "GZ", "Franja De Gaza", "Gaza Strip"), - ("HKG", "J6", "HK", "Hong Kong (Region Admiva. Especial de la Rep. )", "Hong Kong (Territory of)"), + ( + "HKG", + "J6", + "HK", + "Hong Kong (Region Admiva. Especial de la Rep. )", + "Hong Kong (Territory of)", + ), ("HND", "J5", "HN", "Honduras ( Republica De )", "Honduras (Republic of)"), ("HRV", "HR", "HR", "Croacia (Republica De)", "Croatia (Republic of)"), ("HTI", "J3", "HT", "Haiti ( Republica De )", "Haiti (Republic of)"), @@ -99,13 +249,25 @@ seed = [ ("IDN", "J9", "ID", "Indonesia ( Republica De )", "Indonesia (Republic of)"), ("IND", "J8", "IN", "India ( Republica De)", "India (Republic of the)"), ("IRL", "K3", "IE", "Irlanda ( Republica De )", "Ireland (Republic of)"), - ("IRN", "K2", "IR", "Iran ( Republica Islamica Del )", "Iran (Islamic Republic of)"), + ( + "IRN", + "K2", + "IR", + "Iran ( Republica Islamica Del )", + "Iran (Islamic Republic of)", + ), ("IRQ", "K1", "IQ", "Irak ( Republica De )", "Iraq (Republic of)"), ("ISL", "K4", "IS", "Islandia ( Republica De )", "Iceland (Republic of)"), ("ISR", "K5", "IL", "Israel ( Estado De )", "Israel (State of)"), ("ITA", "K6", "IT", "Italia (Republica Italiana)", "Italy (Republic)"), ("JAM", "K7", "JM", "Jamaica", "Jamaica"), - ("JOR", "L1", "JO", "Jordania ( Reino Hachemita De )", "Jordan (Hachemite Kingdom of)"), + ( + "JOR", + "L1", + "JO", + "Jordania ( Reino Hachemita De )", + "Jordan (Hachemite Kingdom of)", + ), ("JPN", "K9", "JP", "Japon", "Japan"), ("KAZ", "KZ", "KZ", "Kazakhstan (Republica de)", "Kazakhstan"), ("KCD", "Z9", "PD", "Paises No Declarados", "Not Declared Countries"), @@ -113,49 +275,133 @@ seed = [ ("KGZ", "KG", "KG", "Kyrgyzstan (Republica Kirgyzia)", "Kyrgyzstan"), ("KHM", "D7", "KH", "Camboya (Reino de)", "Cambodia"), ("KIR", "L0", "KI", "Kiribati (Republica de)", "Kiribati"), - ("KNA", "S9", "KN", "San Cristobal Y Nieves (Fed. de)(San Kitts-Nevis)", "St. Christopher - Nevis"), + ( + "KNA", + "S9", + "KN", + "San Cristobal Y Nieves (Fed. de)(San Kitts-Nevis)", + "St. Christopher - Nevis", + ), ("KOR", "E8", "KR", "Corea (Republica De)(Corea del Sur)", "Korea Republic of"), ("KWT", "L3", "KW", "Kuwait (Estado de)", "kuwait"), - ("LAO", "L4", "LA", "Republica Democratica Popular Laos", "Laos (People's Democratic Republic of)"), + ( + "LAO", + "L4", + "LA", + "Republica Democratica Popular Laos", + "Laos (People's Democratic Republic of)", + ), ("LBN", "L7", "LB", "Libano (Republica de)", "Lebanon"), ("LBR", "L8", "LR", "Liberia ( Republica De )", "Liberia (Republic of)"), - ("LBY", "L9", "LY", "Libia (Jamahiriya Libia Araba Pop. Soc.)", "Lybia (Arab Jamahiriya)"), + ( + "LBY", + "L9", + "LY", + "Libia (Jamahiriya Libia Araba Pop. Soc.)", + "Lybia (Arab Jamahiriya)", + ), ("LCA", "T4", "LC", "Santa Lucia", "Saint Lucia"), ("LHM", "HM", "HM", "Islas Heard Y Mcdonald", "Heard & MacDonald Islands"), - ("LIE", "L5", "LI", "Liechtenstein (Principado de)", "Liechtenstein (Principated of)"), - ("LKA", "U4", "LK", "Sri Lanka ( Republica Democratica Soc.)", "Sri Lanka (Socialist Democratic Republic"), + ( + "LIE", + "L5", + "LI", + "Liechtenstein (Principado de)", + "Liechtenstein (Principated of)", + ), + ( + "LKA", + "U4", + "LK", + "Sri Lanka ( Republica Democratica Soc.)", + "Sri Lanka (Socialist Democratic Republic", + ), ("LSO", "L6", "LS", "Lesotho ( Reino De )", "Lesotho (Kingdom of)"), ("LTU", "Y2", "LT", "Lituania (Republica de)", "Lithuania"), - ("LUX", "M0", "LU", "Luxemburgo ( Gran Ducado De)", "Luxembourg (Great Ducated of)"), + ( + "LUX", + "M0", + "LU", + "Luxemburgo ( Gran Ducado De)", + "Luxembourg (Great Ducated of)", + ), ("LVA", "Y1", "LV", "Letonia (Republica de)", "Latvia"), ("MAC", "M1", "MO", "Macao", "Macau"), ("MAR", "M8", "MR", "Marruecos ( Reino De )", "Morocco (Kingdom of)"), ("MCO", "N0", "MC", "Monaco (Principado De)", "Monaco (Principated of)"), ("MDA", "MD", "MD", "Moldavia (Republica de)", "Moldova"), - ("MDG", "M2", "MG", "Madagascar ( Republica De)", "Madagascar (Democratic Republic of)"), + ( + "MDG", + "M2", + "MG", + "Madagascar ( Republica De)", + "Madagascar (Democratic Republic of)", + ), ("MDV", "M5", "MV", "Maldivas ( Republica De )", "Maldives (Republic of the)"), ("MEX", "N3", "MX", "Mexico (Estados Unidos Mexicanos)", "Mexico"), ("MHL", "MH", "MH", "Islas Marshall", "Marshall Islands"), - ("MKD", "MK", "MK", "Macedonia (Antigua Rep. Yugoslava De)", "Macedonia (Old Yugoslavian Republic)"), + ( + "MKD", + "MK", + "MK", + "Macedonia (Antigua Rep. Yugoslava De)", + "Macedonia (Old Yugoslavian Republic)", + ), ("MLI", "M6", "ML", "Mali ( Republica De )", "Mali (Republic of)"), ("MLT", "M7", "MT", "Malta ( Republica De )", "Malta and Gozo (Republic of)"), ("MMR", "C5", "MM", "Myanmar ( Union De )", "Myammar (Union of)"), ("MNE", "ME", "ME", "Montenegro", ""), ("MNG", "N4", "MN", "Mongolia", "Mongolia (People's Republic of)"), - ("MNP", "MP", "IM", "Islas Marianas Septentrionales", "Marianes Septentrional Islands"), - ("MOZ", "N6", "MZ", "Mozambique ( Republica De)", "Mozambique (People's Republic of)"), - ("MRT", "N2", "RT", "Mauritania ( Republica Islamica De )", "Mauritania (Islamic Republic of)"), + ( + "MNP", + "MP", + "IM", + "Islas Marianas Septentrionales", + "Marianes Septentrional Islands", + ), + ( + "MOZ", + "N6", + "MZ", + "Mozambique ( Republica De)", + "Mozambique (People's Republic of)", + ), + ( + "MRT", + "N2", + "RT", + "Mauritania ( Republica Islamica De )", + "Mauritania (Islamic Republic of)", + ), ("MSR", "N5", "MS", "Monserrat ( Isla )", "Montserrat Island"), - ("MTQ", "M9", "MQ", "Martinica (Departamento de) (Francia)", "Martinique (Department of)"), + ( + "MTQ", + "M9", + "MQ", + "Martinica (Departamento de) (Francia)", + "Martinique (Department of)", + ), ("MUS", "N1", "MU", "Mauricio ( Republica De )", "Mauritius (State of)"), ("MWI", "M4", "MW", "Malawi ( Republica De )", "Malawi (Republic of)"), ("MYS", "M3", "MY", "Malasia", "Malaysia (Federation of)"), ("NAM", "P0", "NA", "Namibia ( Republica De )", "Namibia (Republic of)"), - ("NCA", "P7", "TE", "Terr. Frances Ultramar Nueva Caledonia", "French Territory of New Caledonia"), + ( + "NCA", + "P7", + "TE", + "Terr. Frances Ultramar Nueva Caledonia", + "French Territory of New Caledonia", + ), ("NCL", "NC", "NC", "Nueva Caledonia (Terr.Frances de Ultramar)", "New Caledonia"), ("NER", "P2", "NE", "Niger ( Republica De)", "Niger (Federal Republic of)"), ("NFK", "P5", "NF", "Norfolk ( Isla )", "Norfolk Island"), - ("NGA", "P3", "NG", "Nigeria ( Republica Federal De)", "Nigeria (Federal Republic of)"), + ( + "NGA", + "P3", + "NG", + "Nigeria ( Republica Federal De)", + "Nigeria (Federal Republic of)", + ), ("NIC", "P1", "NI", "Nicaragua ( Republica De )", "Nicaragua (Republic of)"), ("NIU", "P4", "NU", "Nive ( Isla )", "Nive Island"), ("NOR", "P6", "NO", "Noruega ( Reino De )", "Norway (Kingdom of)"), @@ -163,27 +409,81 @@ seed = [ ("NRU", "N7", "NR", "Nauru", "Nauru"), ("NZL", "P9", "NZ", "Nueva Zelandia", "New Zealand"), ("OMN", "Q2", "OM", "Oman (Sultanato De )", "Oman (Sultanate of)"), - ("PAK", "Q7", "PK", "Pakistan ( Republica Islamica De )", "Pakistan (Islamic Republic of)"), + ( + "PAK", + "Q7", + "PK", + "Pakistan ( Republica Islamica De )", + "Pakistan (Islamic Republic of)", + ), ("PAN", "Q8", "PA", "Panama ( Republica De )", "Panama (Republic of)"), - ("PCN", "R3", "PN", "Pitcairns ( Islas Dependencia Britanica )", "Pitcairn Island (Brithish Dependence)"), + ( + "PCN", + "R3", + "PN", + "Pitcairns ( Islas Dependencia Britanica )", + "Pitcairn Island (Brithish Dependence)", + ), ("PER", "R2", "PE", "Peru ( Republica Del )", "Peru (Republic of )"), - ("PHL", "H3", "PH", "Filipinas ( Republica De Las )", "Philippines (Republic of the)"), - ("PIK", "Q3", "PI", "Pacifico Islas Del ( Admon. E.U.A. )", "Pacific Islands (U.S.A. Administration)"), + ( + "PHL", + "H3", + "PH", + "Filipinas ( Republica De Las )", + "Philippines (Republic of the)", + ), + ( + "PIK", + "Q3", + "PI", + "Pacifico Islas Del ( Admon. E.U.A. )", + "Pacific Islands (U.S.A. Administration)", + ), ("PLW", "PW", "PW", "Palau (Republica De)", "Palau (Republic of)"), - ("PNG", "P8", "PP", "Papua Nueva Guinea (Edo. Independiente de)", "Papua New Guinea (Independent State of)"), + ( + "PNG", + "P8", + "PP", + "Papua Nueva Guinea (Edo. Independiente de)", + "Papua New Guinea (Independent State of)", + ), ("POL", "R5", "PL", "Polonia ( Republica De )", "Poland (Republic of)"), - ("PRI", "R7", "PR", "Puerto Rico (Edo.Libre Asociado de la Com. de) Der", "Puerto Rico (Free Asociated State of)"), - ("PRK", "E9", "KP", "Corea ( Rep. Pop. Dem.de)(Corea del Norte)", "Korea (North)(People's Democratic Rep.of"), + ( + "PRI", + "R7", + "PR", + "Puerto Rico (Edo.Libre Asociado de la Com. de) Der", + "Puerto Rico (Free Asociated State of)", + ), + ( + "PRK", + "E9", + "KP", + "Corea ( Rep. Pop. Dem.de)(Corea del Norte)", + "Korea (North)(People's Democratic Rep.of", + ), ("PRT", "R6", "PT", "Portugal (Republica Portuguesa)", "Portugal (Republic of)"), ("PRY", "R1", "PY", "Paraguay ( Republica Del )", "Paraguay (Republic of)"), ("PSE", "PS", "PS", "Palestina", ""), ("PTY", "Z2", "ZO", "Zona Del Canal De Panama", "Zone of the Panama's Channel"), ("PYF", "R4", "PF", "Polinesia Francesa", "French Polynesia"), ("QAT", "R8", "QA", "Qatar ( Estado De )", "Qatar (State of)"), - ("REU", "S3", "RE", "Reunion (Departamento de la) ( Francia)", "Reunion Islands (French Department)"), + ( + "REU", + "S3", + "RE", + "Reunion (Departamento de la) ( Francia)", + "Reunion Islands (French Department)", + ), ("RKE", "E1", "RK", "Canal Islas del ( Islas Normandas )", "Channel Islands"), ("ROM", "S5", "RO", "Rumania", "Romania (Republic of)"), - ("RUH", "NT", "NT", "Zona Neutral Iraq-Arabia Saudita", "Neutral Zone of Iraq - Saudi Arabia"), + ( + "RUH", + "NT", + "NT", + "Zona Neutral Iraq-Arabia Saudita", + "Neutral Zone of Iraq - Saudi Arabia", + ), ("RUS", "RU", "RU", "Rusia (Federacion Rusa)", "Russia (Federation)"), ("RWA", "S6", "RW", "Republica Ruandesa", "Rwanda"), ("SAU", "B2", "SA", "Arabia Saudita ( Reino De )", "Saudi Arabia (Kingdom of)"), @@ -191,21 +491,51 @@ seed = [ ("SEN", "T6", "SN", "Senegal ( Republica Del )", "Senegal (Republic of the)"), ("SGP", "U1", "SG", "Singapur ( Republica De )", "Singapore (Republic of)"), ("SHN", "T3", "SH", "Santa Elena", "St. Helena"), - ("SJM", "SJ", "SJ", "Islas Svalbard Y Jan Mayen (Noruega)", "Svalbard & Jan Mayen Islands"), - ("SLB", "SB", "SB", "Islas Salomon (Com. Britanica de Naciones)", "Solomon Islands (Brithish Community)"), + ( + "SJM", + "SJ", + "SJ", + "Islas Svalbard Y Jan Mayen (Noruega)", + "Svalbard & Jan Mayen Islands", + ), + ( + "SLB", + "SB", + "SB", + "Islas Salomon (Com. Britanica de Naciones)", + "Solomon Islands (Brithish Community)", + ), ("SLE", "T8", "SL", "Sierra Leona ( Republica De )", "Sierra Leone (Republic of)"), ("SLV", "G5", "SV", "El Salvador ( Republica De )", "El Salvador (Republic of)"), - ("SMR", "T0", "SM", "San Marino (Serenisima Republica De)", "San Marino (Republic of"), + ( + "SMR", + "T0", + "SM", + "San Marino (Serenisima Republica De)", + "San Marino (Republic of", + ), ("SOM", "U3", "SO", "Somalia", "Somalia (Democratic Republic of)"), ("SPM", "T1", "PM", "San Pedro Y Miquelon", "St. Pierre and Miquelon"), ("SRB", "RS", "RS", "Republica de Serbia", ""), - ("STP", "T5", "ST", "Santo Tome Y Principe (Rep. Democratica de)", "Sao Tome and Principe (Dem. Rep.)"), + ( + "STP", + "T5", + "ST", + "Santo Tome Y Principe (Rep. Democratica de)", + "Sao Tome and Principe (Dem. Rep.)", + ), ("SUR", "U9", "SR", "Suriname ( Republica De )", "Surinam (Republic of)"), ("SVK", "SK", "SK", "Republica Eslovaca", "Slovakia (Republic)"), ("SVN", "SI", "SI", "Eslovenia (Republica De)", "Slovenia (Republic of)"), ("SWE", "U7", "SE", "Suecia ( Reino De )", "Sweden (Kingdom of)"), ("SWZ", "V0", "SZ", "Swazilandia ( Reino De )", "Swaziland (Kingdom of)"), - ("SYC", "T7", "SC", "Seychelles (Republica De Las)", "Seychelles (Republic of the)"), + ( + "SYC", + "T7", + "SC", + "Seychelles (Republica De Las)", + "Seychelles (Republic of the)", + ), ("SYR", "U2", "SY", "Siria ( Republica Arabe )", "Syrian Arab Republic"), ("TCA", "W3", "TC", "Turcas Y Caicos ( Islas )", "Turks and Caicos Islands"), ("TCD", "F4", "TD", "Chad ( Republica De )", "Chad (Republic of)"), @@ -216,30 +546,102 @@ seed = [ ("TKM", "TM", "TM", "Turkmenistan (Republica De)", "Turkmenistan (Republic of)"), ("TMP", "TP", "TP", "Timor Oriental", "East Timor"), ("TON", "TO", "TO", "Tonga (Reino De)", "Tonga (Kingdom of)"), - ("TTO", "W1", "TT", "Trinidad Y Tobago ( Republica De )", "Trinidad and Tobago (Republic of)"), + ( + "TTO", + "W1", + "TT", + "Trinidad Y Tobago ( Republica De )", + "Trinidad and Tobago (Republic of)", + ), ("TUN", "W2", "TN", "Tunez ( Republica De )", "Tunisia (Republic of)"), ("TUR", "W4", "TR", "Turquia ( Republica De )", "Turkey (Republic of)"), - ("TUV", "TV", "TV", "Tuvalu (Comunidad Britanica de Naciones)", "Tuvalu (Brithish Community of Nations)"), + ( + "TUV", + "TV", + "TV", + "Tuvalu (Comunidad Britanica de Naciones)", + "Tuvalu (Brithish Community of Nations)", + ), ("TWN", "F7", "TW", "Taiwan (Republica de China)", "Taiwan"), - ("TZA", "V2", "TZ", "Tanzania ( Republica Unida De )", "Tanzania United Republic of"), + ( + "TZA", + "V2", + "TZ", + "Tanzania ( Republica Unida De )", + "Tanzania United Republic of", + ), ("UGA", "W5", "UG", "Uganda ( Republica De )", "Uganda (Republic of)"), ("UKR", "UA", "UA", "Ucrania", "Ukraine"), - ("URY", "W7", "UY", "Uruguay ( Republica Oriental Del )", "Uruguay (Eastern Republic of the)"), + ( + "URY", + "W7", + "UY", + "Uruguay ( Republica Oriental Del )", + "Uruguay (Eastern Republic of the)", + ), ("USA", "G8", "US", "Estados Unidos de America", "United States of America"), ("UZB", "Y4", "UZ", "Uzbejistan (Republica de)", "Uzbekistan (Republic)"), - ("VCT", "T2", "VC", "San Vicente Y Las Granadinas", "St. Vincent and the Grenadines"), + ( + "VCT", + "T2", + "VC", + "San Vicente Y Las Granadinas", + "St. Vincent and the Grenadines", + ), ("VEN", "W8", "VE", "Venezuela ( Republica De )", "Venezuela (Republic of)"), ("VGB", "X2", "VG", "Virgenes Islas ( Britanicas )", "Virgin Islands (British)"), - ("VIR", "X3", "VI", "Virgenes Islas ( Norteamericanas )", "Virgin Islands (American)"), - ("VNM", "W9", "VN", "Vietnam ( Republica Socialista De )", "Vietnam (Socialist Republic of)"), + ( + "VIR", + "X3", + "VI", + "Virgenes Islas ( Norteamericanas )", + "Virgin Islands (American)", + ), + ( + "VNM", + "W9", + "VN", + "Vietnam ( Republica Socialista De )", + "Vietnam (Socialist Republic of)", + ), ("VUT", "Q1", "VU", "Vanuatu", "Vanuatu"), ("WLF", "WF", "WF", "Islas Wallis Y Futuna", "Wallis & Futuna Islands"), - ("WSM", "S8", "WS", "Samoa (Estado Independiente de)", "Western Samoa (Independent State)"), - ("XCH", "V3", "IO", "Territorios Britanicos Del Oceano Indico", "Brithish Territory of the Indic Ocean"), + ( + "WSM", + "S8", + "WS", + "Samoa (Estado Independiente de)", + "Western Samoa (Independent State)", + ), + ( + "XCH", + "V3", + "IO", + "Territorios Britanicos Del Oceano Indico", + "Brithish Territory of the Indic Ocean", + ), ("YEM", "YE", "YE", "Yemen (Republica De)", "Yemen (Republic of)"), - ("YUG", "X8", "YU", "Yugoslavia (Republica Federal de)", "Yugoslavia (Federal Republic of)"), - ("ZAF", "U5", "ZA", "Sudafrica ( Republica De ) Derogado", "South Africa (Republic of)"), + ( + "YUG", + "X8", + "YU", + "Yugoslavia (Republica Federal de)", + "Yugoslavia (Federal Republic of)", + ), + ( + "ZAF", + "U5", + "ZA", + "Sudafrica ( Republica De ) Derogado", + "South Africa (Republic of)", + ), ("ZMB", "Z1", "ZM", "Zambia ( Republica De )", "Zambia (Republic of)"), ("ZWE", "S4", "ZW", "Zimbabwe ( Republica De )", "Zimbabwe (Republic of)"), - ("ZYA", "J4", "NL", "Paises Bajos ( Reino De Los )(Holanda)", "Netherlands (Kingdom of)(Holand)"), -] \ No newline at end of file + ( + "ZYA", + "J4", + "NL", + "Paises Bajos ( Reino De Los )(Holanda)", + "Netherlands (Kingdom of)(Holand)", + ), +] diff --git a/backend/api/v1/modules/public/reference_data/countries/test_countries.py b/backend/api/v1/modules/public/reference_data/countries/test_countries.py index b0f3af67..c3b5f73a 100644 --- a/backend/api/v1/modules/public/reference_data/countries/test_countries.py +++ b/backend/api/v1/modules/public/reference_data/countries/test_countries.py @@ -7,6 +7,7 @@ app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_countries(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,20 +17,26 @@ def test_list_countries(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_country_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/countries/invalid_key", headers=headers) assert response.status_code == 404 + def test_create_country_forbidden(): response = client.post("/countries/", json={"m3_key": "TST", "description": "Test"}) assert response.status_code in (403, 405, 404) + def test_update_country_forbidden(): - response = client.put("/countries/TST", json={"m3_key": "TST", "description": "Test"}) + response = client.put( + "/countries/TST", json={"m3_key": "TST", "description": "Test"} + ) assert response.status_code in (403, 405, 404) + def test_delete_country_forbidden(): response = client.delete("/countries/TST") assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/currency_types/dto.py b/backend/api/v1/modules/public/reference_data/currency_types/dto.py index 6e9f108f..cf2b34ce 100644 --- a/backend/api/v1/modules/public/reference_data/currency_types/dto.py +++ b/backend/api/v1/modules/public/reference_data/currency_types/dto.py @@ -1,10 +1,10 @@ from pydantic import BaseModel, Field from pydantic import ConfigDict + class CurrencyTypeDTO(BaseModel): code: str = Field(..., min_length=1, max_length=3) currency_name: str country_description: str model_config = ConfigDict(from_attributes=True) - diff --git a/backend/api/v1/modules/public/reference_data/currency_types/models.py b/backend/api/v1/modules/public/reference_data/currency_types/models.py index bb0d119f..176babe9 100644 --- a/backend/api/v1/modules/public/reference_data/currency_types/models.py +++ b/backend/api/v1/modules/public/reference_data/currency_types/models.py @@ -4,15 +4,21 @@ from core.database import Base class CurrencyType(Base): - __tablename__ = "currency_types" #GTiposMoneda + __tablename__ = "currency_types" # GTiposMoneda __table_args__ = ( PrimaryKeyConstraint("code", name="currency_types_pkey"), - {"schema": "public"} # opcional + {"schema": "public"}, # opcional ) - code: Mapped[str] = mapped_column(String(3), nullable=False) # código ISO o clave de moneda - currency_name: Mapped[str] = mapped_column(String(15), nullable=False) # nombre de la moneda (por ejemplo: Peso, Dollar) - country_description: Mapped[str] = mapped_column(String(50)) # país asociado o descripción del país + code: Mapped[str] = mapped_column( + String(3), nullable=False + ) # código ISO o clave de moneda + currency_name: Mapped[str] = mapped_column( + String(15), nullable=False + ) # nombre de la moneda (por ejemplo: Peso, Dollar) + country_description: Mapped[str] = mapped_column( + String(50) + ) # país asociado o descripción del país def __repr__(self): return f"" diff --git a/backend/api/v1/modules/public/reference_data/currency_types/routes.py b/backend/api/v1/modules/public/reference_data/currency_types/routes.py index a878a7a8..d8fd6bec 100644 --- a/backend/api/v1/modules/public/reference_data/currency_types/routes.py +++ b/backend/api/v1/modules/public/reference_data/currency_types/routes.py @@ -1,4 +1,3 @@ - from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db @@ -11,13 +10,12 @@ from typing import Any, Dict router = APIRouter(prefix="/currency-types") - @router.get("/", response_model=Dict[str, Any]) async def list_currency_types( page: int = Query(1, ge=1, description="Número de página"), page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): skip = (page - 1) * page_size query = db.query(CurrencyType) @@ -27,23 +25,27 @@ async def list_currency_types( "items": [CurrencyTypeDTO.model_validate(obj) for obj in items], "total": total, "page": page, - "page_size": page_size + "page_size": page_size, } - + @router.get("/{code}", response_model=CurrencyTypeDTO) -async def get_currency_type(code: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): +async def get_currency_type( + code: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): obj = db.query(CurrencyType).filter(CurrencyType.code == code).first() if not obj: raise HTTPException(status_code=404, detail="Not found") return obj - + @router.post("/", response_model=CurrencyTypeDTO, status_code=201) async def create_currency_type( data: CurrencyTypeDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = CurrencyType(**data.dict()) db.add(obj) @@ -51,13 +53,13 @@ async def create_currency_type( db.refresh(obj) return obj - + @router.put("/{code}", response_model=CurrencyTypeDTO) async def update_currency_type( code: str, data: CurrencyTypeDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(CurrencyType).filter(CurrencyType.code == code).first() if not obj: @@ -68,12 +70,12 @@ async def update_currency_type( db.refresh(obj) return obj - + @router.delete("/{code}", status_code=204) async def delete_currency_type( code: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(CurrencyType).filter(CurrencyType.code == code).first() if not obj: diff --git a/backend/api/v1/modules/public/reference_data/currency_types/seed.py b/backend/api/v1/modules/public/reference_data/currency_types/seed.py index ecef1c54..3385ec31 100644 --- a/backend/api/v1/modules/public/reference_data/currency_types/seed.py +++ b/backend/api/v1/modules/public/reference_data/currency_types/seed.py @@ -93,4 +93,4 @@ seed = [ ("YUD", "DINAR", "YUGOSLAVIA"), ("ZAR", "RAND", "UNION SUDAFRICANA"), ("ZRZ", "FRANCO", "REPUBLICA DEMOCRATICA DEL CONGO"), -] \ No newline at end of file +] diff --git a/backend/api/v1/modules/public/reference_data/currency_types/test_currency_types.py b/backend/api/v1/modules/public/reference_data/currency_types/test_currency_types.py index 7123dc3c..123705b9 100644 --- a/backend/api/v1/modules/public/reference_data/currency_types/test_currency_types.py +++ b/backend/api/v1/modules/public/reference_data/currency_types/test_currency_types.py @@ -7,6 +7,7 @@ app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_currency_types(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,20 +17,28 @@ def test_list_currency_types(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_currency_type_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/currency-types/invalid_code", headers=headers) assert response.status_code == 404 + def test_create_currency_type_forbidden(): - response = client.post("/currency-types/", json={"code": "TST", "description": "Test"}) + response = client.post( + "/currency-types/", json={"code": "TST", "description": "Test"} + ) assert response.status_code in (403, 405, 404) + def test_update_currency_type_forbidden(): - response = client.put("/currency-types/TST", json={"code": "TST", "description": "Test"}) + response = client.put( + "/currency-types/TST", json={"code": "TST", "description": "Test"} + ) assert response.status_code in (403, 405, 404) + def test_delete_currency_type_forbidden(): response = client.delete("/currency-types/TST") assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/customs_sections/dto.py b/backend/api/v1/modules/public/reference_data/customs_sections/dto.py index 2bc56f20..3db9ccdc 100644 --- a/backend/api/v1/modules/public/reference_data/customs_sections/dto.py +++ b/backend/api/v1/modules/public/reference_data/customs_sections/dto.py @@ -1,9 +1,9 @@ from pydantic import BaseModel, Field from pydantic import ConfigDict + class CustomsSectionDTO(BaseModel): customs_code: str = Field(..., min_length=1, max_length=3) section_name: str model_config = ConfigDict(from_attributes=True) - diff --git a/backend/api/v1/modules/public/reference_data/customs_sections/models.py b/backend/api/v1/modules/public/reference_data/customs_sections/models.py index 2ee8a3b5..22e9e3bc 100644 --- a/backend/api/v1/modules/public/reference_data/customs_sections/models.py +++ b/backend/api/v1/modules/public/reference_data/customs_sections/models.py @@ -2,15 +2,16 @@ from sqlalchemy import String, PrimaryKeyConstraint from sqlalchemy.orm import mapped_column from core.database import Base + class CustomsSection(Base): - __tablename__ = "customs_sections" #GAduanaSec + __tablename__ = "customs_sections" # GAduanaSec __table_args__ = ( PrimaryKeyConstraint("customs_code", name="customs_code_pkey"), - {"schema": "public"} + {"schema": "public"}, ) customs_code = mapped_column(String(3), nullable=False) - section_name = mapped_column(String(255), nullable=False) + section_name = mapped_column(String(255), nullable=False) def __repr__(self): - return f"" \ No newline at end of file + return f"" diff --git a/backend/api/v1/modules/public/reference_data/customs_sections/routes.py b/backend/api/v1/modules/public/reference_data/customs_sections/routes.py index fdf636a6..8718ec1e 100644 --- a/backend/api/v1/modules/public/reference_data/customs_sections/routes.py +++ b/backend/api/v1/modules/public/reference_data/customs_sections/routes.py @@ -1,4 +1,3 @@ - from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db @@ -16,7 +15,7 @@ def list_customs_sections( page: int = Query(1, ge=1, description="Número de página"), page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): skip = (page - 1) * page_size query = db.query(CustomsSection) @@ -26,21 +25,31 @@ def list_customs_sections( "items": [CustomsSectionDTO.model_validate(obj) for obj in items], "total": total, "page": page, - "page_size": page_size + "page_size": page_size, } + @router.get("/{customs_code}", response_model=CustomsSectionDTO) -def get_customs_section(customs_code: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): - obj = db.query(CustomsSection).filter(CustomsSection.customs_code == customs_code).first() +def get_customs_section( + customs_code: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + obj = ( + db.query(CustomsSection) + .filter(CustomsSection.customs_code == customs_code) + .first() + ) if not obj: raise HTTPException(status_code=404, detail="Not found") return obj + @router.post("/", response_model=CustomsSectionDTO, status_code=201) def create_customs_section( data: CustomsSectionDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = CustomsSection(**data.dict()) db.add(obj) @@ -48,14 +57,19 @@ def create_customs_section( db.refresh(obj) return obj + @router.put("/{customs_code}", response_model=CustomsSectionDTO) def update_customs_section( customs_code: str, data: CustomsSectionDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): - obj = db.query(CustomsSection).filter(CustomsSection.customs_code == customs_code).first() + obj = ( + db.query(CustomsSection) + .filter(CustomsSection.customs_code == customs_code) + .first() + ) if not obj: raise HTTPException(status_code=404, detail="Not found") for field, value in data.dict().items(): @@ -64,13 +78,18 @@ def update_customs_section( db.refresh(obj) return obj + @router.delete("/{customs_code}", status_code=204) def delete_customs_section( customs_code: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): - obj = db.query(CustomsSection).filter(CustomsSection.customs_code == customs_code).first() + obj = ( + db.query(CustomsSection) + .filter(CustomsSection.customs_code == customs_code) + .first() + ) if not obj: raise HTTPException(status_code=404, detail="Not found") db.delete(obj) diff --git a/backend/api/v1/modules/public/reference_data/customs_sections/seed.py b/backend/api/v1/modules/public/reference_data/customs_sections/seed.py index c7448b43..e649d707 100644 --- a/backend/api/v1/modules/public/reference_data/customs_sections/seed.py +++ b/backend/api/v1/modules/public/reference_data/customs_sections/seed.py @@ -1,10 +1,13 @@ seed = [ - ("01", "AEROPUERTO INTERNACIONAL GENERAL JUAN N. ALVAREZ, ACAPULCO, GUERRERO."), + ("01", "AEROPUERTO INTERNACIONAL GENERAL JUAN N. ALVAREZ, ACAPULCO, GUERRERO."), ("010", "ACAPULCO, ACAPULCO DE JUAREZ, GUERRERO."), ("012", "AEROPUERTO INTERNACIONAL GENERAL JUAN N. ALVAREZ, ACAPULCO, GUERRERO."), ("020", "AGUA PRIETA, AGUA PRIETA, SONORA."), ("050", "SUBTENIENTE LOPEZ, SUBTENIENTE LOPEZ, QUINTANA ROO."), - ("051", "SUBTENIENTE LOPEZ II „CHACTEMAL“, OTHÓN P. BLANCO, CHETUMAL, QUINTANA ROO."), + ( + "051", + "SUBTENIENTE LOPEZ II „CHACTEMAL“, OTHÓN P. BLANCO, CHETUMAL, QUINTANA ROO.", + ), ("060", "CIUDAD DEL CARMEN, CIUDAD DEL CARMEN, CAMPECHE."), ("063", "SEYBAPLAYA, CHAMPOTON, CAMPECHE."), ("070", "CIUDAD JUAREZ, CIUDAD JUAREZ, CHIHUAHUA."), @@ -15,8 +18,14 @@ seed = [ ("080", "COATZACOALCOS, COATZACOALCOS, VERACRUZ."), ("110", "ENSENADA, ENSENADA, BAJA CALIFORNIA."), ("120", "GUAYMAS, GUAYMAS, SONORA."), - ("121", "AEROPUERTO INTERNACIONAL GENERAL IGNACIO PESQUEIRA GARCIA, HERMOSILLO, SONORA."), - ("123", "CIUDAD OBREGON ADYACENTE AL AEROPUERTO DE CIUDAD OBREGON, CAJEME, SONORA."), + ( + "121", + "AEROPUERTO INTERNACIONAL GENERAL IGNACIO PESQUEIRA GARCIA, HERMOSILLO, SONORA.", + ), + ( + "123", + "CIUDAD OBREGON ADYACENTE AL AEROPUERTO DE CIUDAD OBREGON, CAJEME, SONORA.", + ), ("140", "LA PAZ, LA PAZ, BAJA CALIFORNIA SUR."), ("142", "SAN JOSE DEL CABO, LOS CABOS, BAJA CALIFORNIA SUR."), ("143", "CABO SAN LUCAS, LOS CABOS, BAJA CALIFORNIA SUR."), @@ -25,7 +34,7 @@ seed = [ ("147", "PICHILINGÜE, LA PAZ, BAJA CALIFORNIA SUR."), ("160", "MANZANILLO, MANZANILLO, COLIMA."), ("161", "ARMERÍA, ARMERÍA, COLIMA."), - ("17", "AEROPUERTO INTERNACIONAL GENERAL SERVANDO CANALES, MATAMOROS, TAMAULIPAS."), + ("17", "AEROPUERTO INTERNACIONAL GENERAL SERVANDO CANALES, MATAMOROS, TAMAULIPAS."), ("170", "MATAMOROS, MATAMOROS, TAMAULIPAS."), ("171", "LUCIO BLANCO-LOS INDIOS, MATAMOROS, TAMAULIPAS."), ("172", "SECCION ADUANERA FERROVIARIA DE MATAMOROS."), @@ -36,22 +45,31 @@ seed = [ ("192", "LOS ALGODONES, MEXICALI, BAJA CALIFORNIA."), ("193", "SAN FELIPE, MEXICALI, BAJA CALIFORNIA."), ("200", "MÉXICO, CIUDAD DE MÉXICO."), - ("202", "IMPORTACION Y EXPORTACION DE CONTENEDORES, DELEGACION AZCAPOTZALCO, CIUDAD DE MÉXICO."), + ( + "202", + "IMPORTACION Y EXPORTACION DE CONTENEDORES, DELEGACION AZCAPOTZALCO, CIUDAD DE MÉXICO.", + ), ("220", "NACO, NACO, SONORA."), ("230", "NOGALES, NOGALES, SONORA."), ("231", "SASABE, SARIC, SONORA."), - ("24", "AEROPUERTO INTERNACIONAL DE NUEVO LAREDO „QUETZALCOATL“, NUEVO LAREDO, TAMAULIPAS."), + ( + "24", + "AEROPUERTO INTERNACIONAL DE NUEVO LAREDO „QUETZALCOATL“, NUEVO LAREDO, TAMAULIPAS.", + ), ("240", "NUEVO LAREDO, NUEVO LAREDO, TAMAULIPAS."), ("250", "OJINAGA, OJINAGA, CHIHUAHUA."), ("260", "PUERTO PALOMAS, PUERTO PALOMAS, CHIHUAHUA."), - ("27", "RIO ESCONDIDO, NAVA, COAHUILA."), + ("27", "RIO ESCONDIDO, NAVA, COAHUILA."), ("270", "PIEDRAS NEGRAS, PIEDRAS NEGRAS, COAHUILA."), ("271", "AEROPUERTO INTERNACIONAL PLAN DE GUADALUPE, RAMOS ARIZPE, COAHUILA."), ("280", "PROGRESO, PROGRESO, YUCATAN."), ("282", "AEROPUERTO INTERNACIONAL LIC. MANUEL CRESCENCIO REJON, MERIDA, YUCATAN."), ("300", "CIUDAD REYNOSA, CIUDAD REYNOSA, TAMAULIPAS."), ("302", "LAS FLORES, RIO BRAVO, TAMAULIPAS."), - ("304", "AEROPUERTO INTERNACIONAL GENERAL. LUCIO BLANCO, CIUDAD REYNOSA, TAMAULIPAS."), + ( + "304", + "AEROPUERTO INTERNACIONAL GENERAL. LUCIO BLANCO, CIUDAD REYNOSA, TAMAULIPAS.", + ), ("305", "RIO BRAVO-DONNA, RIO BRAVO, TAMAULIPAS."), ("306", "ANZALDUAS, CIUDAD REYNOSA, TAMAULIPAS."), ("310", "SALINA CRUZ, SALINA CRUZ, OAXACA."), @@ -59,7 +77,7 @@ seed = [ ("330", "SAN LUIS RIO COLORADO, SAN LUIS RIO COLORADO, SONORA."), ("340", "CIUDAD MIGUEL ALEMAN, CIUDAD MIGUEL ALEMAN, TAMAULIPAS."), ("342", "GUERRERO, GUERRERO, TAMAULIPAS."), - ("37", "AEROPUERTO INTERNACIONAL DE TAPACHULA, TAPACHULA, CHIAPAS."), + ("37", "AEROPUERTO INTERNACIONAL DE TAPACHULA, TAPACHULA, CHIAPAS."), ("370", "CIUDAD HIDALGO, CIUDAD HIDALGO, CHIAPAS."), ("372", "CIUDAD TALISMAN, TUXTLA CHICO, CHIAPAS."), ("375", "PUERTO CHIAPAS, TAPACHULA, CHIAPAS."), @@ -67,27 +85,42 @@ seed = [ ("380", "TAMPICO, TAMPICO, TAMAULIPAS."), ("390", "TECATE, TECATE, BAJA CALIFORNIA."), ("400", "TIJUANA, TIJUANA, BAJA CALIFORNIA."), - ("402", "AEROPUERTO INTERNACIONAL GENERAL ABELARDO L. RODRIGUEZ, TIJUANA, BAJA CALIFORNIA."), + ( + "402", + "AEROPUERTO INTERNACIONAL GENERAL ABELARDO L. RODRIGUEZ, TIJUANA, BAJA CALIFORNIA.", + ), ("420", "TUXPAN, TUXPAN DE RODRIGUEZ CANO, VERACRUZ."), ("421", "TUXPAN, TUXPAN, VERACRUZ."), ("430", "VERACRUZ, VERACRUZ, VERACRUZ."), - ("432", "AEROPUERTO INTERNACIONAL GENERAL HERIBERTO JARA CORONA, VERACRUZ, VERACRUZ."), + ( + "432", + "AEROPUERTO INTERNACIONAL GENERAL HERIBERTO JARA CORONA, VERACRUZ, VERACRUZ.", + ), ("440", "CIUDAD ACUÑA, CIUDAD ACUÑA, COAHUILA."), ("460", "TORREON, TORREON, COAHUILA."), ("461", "AEROPUERTO DE TORREÓN, COAHUILA DE ZARAGOZA."), ("462", "GOMEZ PALACIO, GOMEZ PALACIO, DURANGO."), ("463", "AEROPUERTO INTERNACIONAL GENERAL GUADALUPE VICTORIA, DURANGO, DURANGO."), ("470", "AEROPUERTO INTERNACIONAL DE LA CIUDAD DE MEXICO."), - ("471", "SATELITE, PARA IMPORTACION Y EXPORTACION POR VIA AEREA, AEROPUERTO INTERNACIONAL BENITO JUAREZ DE LA CIUDAD DE MEXICO."), - ("472", "CENTRO POSTAL MECANIZADO, POR VIA POSTAL Y POR TRAFICO AEREO, AEROPUERTO INTERNACIONAL BENITO JUAREZ DE LA CIUDAD DE MEXICO."), + ( + "471", + "SATELITE, PARA IMPORTACION Y EXPORTACION POR VIA AEREA, AEROPUERTO INTERNACIONAL BENITO JUAREZ DE LA CIUDAD DE MEXICO.", + ), + ( + "472", + "CENTRO POSTAL MECANIZADO, POR VIA POSTAL Y POR TRAFICO AEREO, AEROPUERTO INTERNACIONAL BENITO JUAREZ DE LA CIUDAD DE MEXICO.", + ), ("480", "GUADALAJARA, TLACOMULCO DE ZUÑIGA, JALISCO."), ("481", "PUERTO VALLARTA, PUERTO VALLARTA, JALISCO."), ("484", "TERMINAL INTERMODAL FERROVIARIA, GUADALAJARA, JALISCO."), - ("50", "SONORA, PITIQUITO, SONORA."), + ("50", "SONORA, PITIQUITO, SONORA."), ("500", "SONOYTA, SONOYTA, SONORA."), ("501", "SAN EMETERIO, GENERAL PLUTARCO ELIAS CALLES, SONORA."), ("510", "LAZARO CARDENAS, LAZARO CARDENAS, MICHOACAN."), - ("511", "AEROPUERTO INTERNACIONAL IXTAPA-ZIHUATANEJO, ZIHUATANEJO DE AZUETA, GUERRERO."), + ( + "511", + "AEROPUERTO INTERNACIONAL IXTAPA-ZIHUATANEJO, ZIHUATANEJO DE AZUETA, GUERRERO.", + ), ("520", "MONTERREY, GENERAL MARIANO ESCOBEDO, NUEVO LEON."), ("521", "AEROPUERTO INTERNACIONAL GENERAL MARIANO ESCOBEDO, APODACA, NUEVO LEON."), ("523", "SALINAS VICTORIA A (TERMINAL FERROVIARIA), SALINAS VICTORIA, NUEVO LEON."), @@ -102,14 +135,23 @@ seed = [ ("651", "SAN CAYETANO MORELOS, TOLUCA, ESTADO DE MÉXICO"), ("670", "CHIHUAHUA, CHIHUAHUA, CHIHUAHUA."), ("671", "PARQUE INDUSTRIAL LAS AMERICAS, CHIHUAHUA, CHIHUAHUA."), - ("672", "AEROPUERTO INTERNACIONAL GENERAL ROBERTO FIERRO VILLALOBOS, CHIHUAHUA, CHIHUAHUA."), - ("73", "CHICALOTE, SAN FRANCISCO DE LOS ROMO, AGUASCALIENTES."), + ( + "672", + "AEROPUERTO INTERNACIONAL GENERAL ROBERTO FIERRO VILLALOBOS, CHIHUAHUA, CHIHUAHUA.", + ), + ("73", "CHICALOTE, SAN FRANCISCO DE LOS ROMO, AGUASCALIENTES."), ("730", "AGUASCALIENTES, AGUASCALIENTES, AGUASCALIENTES."), ("731", "PARQUE MULTIMODAL INTERPUERTO, SAN LUIS POTOSI, SAN LUIS POTOSI."), ("732", "AEROPUERTO INTERNACIONAL GENERAL LEOBARDO C. RUIZ, EN CALERA ZACATECAS."), - ("733", "AEROPUERTO INTERNACIONAL PONCIANO ARRIAGA, SOLEDAD DE GRACIANO SANCHEZ, SAN LUIS POTOSI."), + ( + "733", + "AEROPUERTO INTERNACIONAL PONCIANO ARRIAGA, SOLEDAD DE GRACIANO SANCHEZ, SAN LUIS POTOSI.", + ), ("734", "LA PILA-VILLA, VILLA DE REYES, SAN LUIS POTOSI."), - ("735", "AEROPUERTO INTERNACIONAL LIC. JESUS TERAN PEREDO, AGUASCALIENTES, AGUASCALIENTES."), + ( + "735", + "AEROPUERTO INTERNACIONAL LIC. JESUS TERAN PEREDO, AGUASCALIENTES, AGUASCALIENTES.", + ), ("750", "PUEBLA, HEROICA PUEBLA DE ZARAGOZA, PUEBLA."), ("751", "CUERNAVACA, JIUTEPEC, MORELOS."), ("754", "AEROPUERTO INTERNACIONAL HERMANOS SERDAN, HUEJOTZINGO, PUEBLA."), @@ -117,9 +159,12 @@ seed = [ ("810", "ALTAMIRA, ALTAMIRA, TAMAULIPAS."), ("820", "CIUDAD CAMARGO, CIUDAD CAMARGO, TAMAULIPAS."), ("830", "DOS BOCAS, PARAISO, TABASCO."), - ("831", "AEROPUERTO INTERNACIONAL C.P.A. CARLOS ROVIROSA PEREZ, CIUDAD DE VILLAHERMOSA, CENTRO, TABASCO."), + ( + "831", + "AEROPUERTO INTERNACIONAL C.P.A. CARLOS ROVIROSA PEREZ, CIUDAD DE VILLAHERMOSA, CENTRO, TABASCO.", + ), ("834", "EL CEIBO, TENOSIQUE, TABASCO."), ("840", "GUANAJUATO, SILAO, GUANAJUATO."), ("841", "CELAYA, CELAYA, GUANAJUATO."), ("842", "AEROPUERTO INTERNACIONAL DE GUANAJUATO, SILAO, GUANAJUATO."), -] \ No newline at end of file +] diff --git a/backend/api/v1/modules/public/reference_data/customs_sections/test_customs_sections.py b/backend/api/v1/modules/public/reference_data/customs_sections/test_customs_sections.py index 2b000f46..1fd3924d 100644 --- a/backend/api/v1/modules/public/reference_data/customs_sections/test_customs_sections.py +++ b/backend/api/v1/modules/public/reference_data/customs_sections/test_customs_sections.py @@ -7,6 +7,7 @@ app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_customs_sections(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,20 +17,28 @@ def test_list_customs_sections(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_customs_section_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/customs-sections/invalid_code", headers=headers) assert response.status_code == 404 + def test_create_customs_section_forbidden(): - response = client.post("/customs-sections/", json={"customs_code": "TST", "description": "Test"}) + response = client.post( + "/customs-sections/", json={"customs_code": "TST", "description": "Test"} + ) assert response.status_code in (403, 405, 404) + def test_update_customs_section_forbidden(): - response = client.put("/customs-sections/TST", json={"customs_code": "TST", "description": "Test"}) + response = client.put( + "/customs-sections/TST", json={"customs_code": "TST", "description": "Test"} + ) assert response.status_code in (403, 405, 404) + def test_delete_customs_section_forbidden(): response = client.delete("/customs-sections/TST") assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/customs_warehouses/dto.py b/backend/api/v1/modules/public/reference_data/customs_warehouses/dto.py index 17711567..e2087b9f 100644 --- a/backend/api/v1/modules/public/reference_data/customs_warehouses/dto.py +++ b/backend/api/v1/modules/public/reference_data/customs_warehouses/dto.py @@ -1,10 +1,10 @@ from pydantic import BaseModel, Field from pydantic import ConfigDict + class CustomsWarehouseDTO(BaseModel): key: str = Field(..., min_length=1, max_length=3) customs: str fiscalized_warehouse: str model_config = ConfigDict(from_attributes=True) - diff --git a/backend/api/v1/modules/public/reference_data/customs_warehouses/models.py b/backend/api/v1/modules/public/reference_data/customs_warehouses/models.py index 5adcfe94..362b534f 100644 --- a/backend/api/v1/modules/public/reference_data/customs_warehouses/models.py +++ b/backend/api/v1/modules/public/reference_data/customs_warehouses/models.py @@ -2,16 +2,19 @@ from sqlalchemy import String, PrimaryKeyConstraint from sqlalchemy.orm import mapped_column, Mapped from core.database import Base + class CustomsWarehouse(Base): - __tablename__ = "customs_warehouses" #GRecintos + __tablename__ = "customs_warehouses" # GRecintos __table_args__ = ( PrimaryKeyConstraint("key", "customs", name="pk_customs_warehouse"), - {"schema": "public"} # opcional + {"schema": "public"}, # opcional ) - key: Mapped[str] = mapped_column(String(3), nullable=False) # clave del recinto - customs: Mapped[str] = mapped_column(String(100), nullable=False) # aduana asociada - fiscalized_warehouse: Mapped[str] = mapped_column(String(1000)) # recintos fiscalizados (valor legal) + key: Mapped[str] = mapped_column(String(3), nullable=False) # clave del recinto + customs: Mapped[str] = mapped_column(String(100), nullable=False) # aduana asociada + fiscalized_warehouse: Mapped[str] = mapped_column( + String(1000) + ) # recintos fiscalizados (valor legal) def __repr__(self): return f"" diff --git a/backend/api/v1/modules/public/reference_data/customs_warehouses/routes.py b/backend/api/v1/modules/public/reference_data/customs_warehouses/routes.py index 8f25ac54..928e5d03 100644 --- a/backend/api/v1/modules/public/reference_data/customs_warehouses/routes.py +++ b/backend/api/v1/modules/public/reference_data/customs_warehouses/routes.py @@ -1,4 +1,3 @@ - from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db @@ -16,7 +15,7 @@ def list_customs_warehouses( page: int = Query(1, ge=1, description="Número de página"), page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): skip = (page - 1) * page_size query = db.query(CustomsWarehouse) @@ -26,21 +25,32 @@ def list_customs_warehouses( "items": [CustomsWarehouseDTO.model_validate(obj) for obj in items], "total": total, "page": page, - "page_size": page_size + "page_size": page_size, } + @router.get("/{key}/{customs}", response_model=CustomsWarehouseDTO) -def get_customs_warehouse(key: str, customs: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): - obj = db.query(CustomsWarehouse).filter(CustomsWarehouse.key == key, CustomsWarehouse.customs == customs).first() +def get_customs_warehouse( + key: str, + customs: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): + obj = ( + db.query(CustomsWarehouse) + .filter(CustomsWarehouse.key == key, CustomsWarehouse.customs == customs) + .first() + ) if not obj: raise HTTPException(status_code=404, detail="Not found") return obj + @router.post("/", response_model=CustomsWarehouseDTO, status_code=201) def create_customs_warehouse( data: CustomsWarehouseDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = CustomsWarehouse(**data.dict()) db.add(obj) @@ -48,15 +58,20 @@ def create_customs_warehouse( db.refresh(obj) return obj + @router.put("/{key}/{customs}", response_model=CustomsWarehouseDTO) def update_customs_warehouse( key: str, customs: str, data: CustomsWarehouseDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): - obj = db.query(CustomsWarehouse).filter(CustomsWarehouse.key == key, CustomsWarehouse.customs == customs).first() + obj = ( + db.query(CustomsWarehouse) + .filter(CustomsWarehouse.key == key, CustomsWarehouse.customs == customs) + .first() + ) if not obj: raise HTTPException(status_code=404, detail="Not found") for field, value in data.dict().items(): @@ -65,14 +80,19 @@ def update_customs_warehouse( db.refresh(obj) return obj + @router.delete("/{key}/{customs}", status_code=204) def delete_customs_warehouse( key: str, customs: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): - obj = db.query(CustomsWarehouse).filter(CustomsWarehouse.key == key, CustomsWarehouse.customs == customs).first() + obj = ( + db.query(CustomsWarehouse) + .filter(CustomsWarehouse.key == key, CustomsWarehouse.customs == customs) + .first() + ) if not obj: raise HTTPException(status_code=404, detail="Not found") db.delete(obj) diff --git a/backend/api/v1/modules/public/reference_data/customs_warehouses/seed.py b/backend/api/v1/modules/public/reference_data/customs_warehouses/seed.py index 802dca3d..af899d8f 100644 --- a/backend/api/v1/modules/public/reference_data/customs_warehouses/seed.py +++ b/backend/api/v1/modules/public/reference_data/customs_warehouses/seed.py @@ -1,28 +1,68 @@ seed = [ ("1 ", "Acapulco", "Administración Portuaria Integral de Acapulco, S.A. de C.V."), - ("10 ", "Aeropuerto Internacional de la Ciudad de México", "Cargo Service Center de México, S.A. de C.V."), - ("12 ", "Aeropuerto Internacional de la Ciudad de México", "DHL Express México, S.A. de C.V."), - ("14 ", "Aeropuerto Internacional de la Ciudad de México", "Lufthansa Cargo Servicios Logísticos de México, S.A. de C.V."), + ( + "10 ", + "Aeropuerto Internacional de la Ciudad de México", + "Cargo Service Center de México, S.A. de C.V.", + ), + ( + "12 ", + "Aeropuerto Internacional de la Ciudad de México", + "DHL Express México, S.A. de C.V.", + ), + ( + "14 ", + "Aeropuerto Internacional de la Ciudad de México", + "Lufthansa Cargo Servicios Logísticos de México, S.A. de C.V.", + ), ("145", "México", "Ferrocarril y Terminal de Valle de México, S.A. de C.V."), ("146", "Veracruz", "Cargill de México, S.A. de C.V."), - ("147", "Aeropuerto Internacional de la Ciudad de México", "Braniff Transport Carga, S.A. de C.V."), - ("148", "Nuevo Laredo", "Inspecciones Fitosanitarias y Aduaneras de Nuevo Laredo, S.A. de C.V."), + ( + "147", + "Aeropuerto Internacional de la Ciudad de México", + "Braniff Transport Carga, S.A. de C.V.", + ), + ( + "148", + "Nuevo Laredo", + "Inspecciones Fitosanitarias y Aduaneras de Nuevo Laredo, S.A. de C.V.", + ), ("149", "Nuevo Laredo", "PG Servicios de Logística, S.C."), - ("15 ", "Aeropuerto Internacional de la Ciudad de México", "Tramitadores Asociados de Aerocarga, S.A. de C.V."), + ( + "15 ", + "Aeropuerto Internacional de la Ciudad de México", + "Tramitadores Asociados de Aerocarga, S.A. de C.V.", + ), ("150", "Piedras Negras", "Mercurio Cargo, S.A. de C.V."), ("151", "Colombia", "S.R. Asesores Aduanales de Nuevo Laredo, S.C."), - ("154", "Monterrey", "Federal Express Holdings (México) y Compañía, S.N.C. de C.V."), + ( + "154", + "Monterrey", + "Federal Express Holdings (México) y Compañía, S.N.C. de C.V.", + ), ("155", "Ciudad Hidalgo", "Corporativo de Servicios del Sureste, S.A. de C.V."), ("158", "Monterrey", "Aeropuerto de Monterrey, S.A. de C.V."), - ("16 ", "Aeropuerto Internacional de la Ciudad de México", "Transportación México Express, S.A. de C.V."), + ( + "16 ", + "Aeropuerto Internacional de la Ciudad de México", + "Transportación México Express, S.A. de C.V.", + ), ("160", "Manzanillo", "Frigorífico de Manzanillo, S.A. de C.V."), ("161", "Colombia", "Santos Esquivel y Compañía, S.C."), ("162", "Guadalajara", "Ferrocarril Mexicano, S.A. de C.V."), ("164", "Monterrey", "United Parcel Service de México, S.A. de C.V."), - ("165", "Aguascalientes", "Centros de Intercambio de Carga Express Estafeta, S.A. de C.V."), + ( + "165", + "Aguascalientes", + "Centros de Intercambio de Carga Express Estafeta, S.A. de C.V.", + ), ("166", "Altamira", "Administración Portuaria Integral de Altamira, S.A. de C.V."), ("167", "Ciudad Juárez", "Accel, Recinto Fiscalizado, S.A. de C.V."), - ("17 ", "Aeropuerto Internacional de la Ciudad de México", "United Parcel Service de México, S.A. de C.V."), + ( + "17 ", + "Aeropuerto Internacional de la Ciudad de México", + "United Parcel Service de México, S.A. de C.V.", + ), ("171", "Chihuahua", "Aeropuerto de Chihuahua, S.A. de C.V."), ("172", "Veracruz", "Servicios Especiales Portuarios, S.A. de C.V."), ("173", "Lázaro Cárdenas", "UTTSA, S.A. de C.V."), @@ -34,7 +74,11 @@ seed = [ ("179", "Altamira", "D.A. Hinojosa Terminal Multiusos, S.A. de C.V."), ("18 ", "Aeropuerto Internacional de la Ciudad de México", "Varig de México, S.A."), ("180", "Altamira", "Inmobiliaria Portuaria de Altamira, S.A. de C.V."), - ("182", "Veracruz", "Servicios, Maniobras y Almacenamientos de Veracruz, S.A. de C.V."), + ( + "182", + "Veracruz", + "Servicios, Maniobras y Almacenamientos de Veracruz, S.A. de C.V.", + ), ("184", "Progreso", "Terminal de Contenedores de Yucatán, S.A. de C.V."), ("186", "Nuevo Laredo", "Logis Servicios Comerciales, S.A. de C.V."), ("187", "Manzanillo", "Tecnoadministración del Pacífico, S.A. de C.V."), @@ -50,8 +94,16 @@ seed = [ ("203", "Altamira", "Grupo Castañeda, S.A. de C.V."), ("204", "Monterrey", "Ferrocarril Mexicano, S.A. de C.V."), ("210", "Querétaro", "Terminal Logistics, S.A. de C.V."), - ("211", "Aeropuerto Internacional de la Ciudad de México", "World Express Cargo de México, S.A. de C.V."), - ("212", "Piedras Negras", "Consultores de Logística en Comercio Exterior, S.A. de C.V."), + ( + "211", + "Aeropuerto Internacional de la Ciudad de México", + "World Express Cargo de México, S.A. de C.V.", + ), + ( + "212", + "Piedras Negras", + "Consultores de Logística en Comercio Exterior, S.A. de C.V.", + ), ("214", "Altamira", "Possehl México, S.A. de C.V."), ("215", "Tampico", "Refitam, S.A. de C.V."), ("217", "Veracruz", "SSA México, S.A. de C.V."), @@ -62,12 +114,20 @@ seed = [ ("222", "Matamoros", "Puerto Los Indios, S.A. de C.V."), ("223", "Monterrey", "DHL Express México, S.A. de C.V."), ("224", "Aguascalientes", "Nafta Rail, S.A. de C.V."), - ("225", "Altamira", "Integradora de Servicios, Transporte y Almacenaje, S.A. de C.V."), + ( + "225", + "Altamira", + "Integradora de Servicios, Transporte y Almacenaje, S.A. de C.V.", + ), ("226", "Nuevo Laredo", "DAF, Delivery After Frontier, S.A. de C.V."), ("227", "Matamoros", "Profesionales Mexicanos del Comercio Exterior, S.C."), ("228", "Guadalajara", "CLA Guadalajara, S.A. de C.V."), ("229", "Manzanillo", "Maniobras Integradas del Puerto, S.A. de C.V."), - ("23 ", "Coatzacoalcos", "Administración Portuaria Integral de Coatzacoalcos, S.A. de C.V."), + ( + "23 ", + "Coatzacoalcos", + "Administración Portuaria Integral de Coatzacoalcos, S.A. de C.V.", + ), ("230", "Querétaro", "Terminal Intermodal Logística de Hidalgo, S.A.P.I. de C.V."), ("231", "Lázaro Cárdenas", "Terminales Portuarias del Pacífico, S.A.P.I. de C.V."), ("232", "Lázaro Cárdenas", "Arcelormittal Portuarios, S.A. de C.V."), @@ -83,34 +143,74 @@ seed = [ ("26 ", "Colombia", "Mex Securit, S.A. de C.V."), ("27 ", "Ensenada", "Ensenada International Terminal, S.A. de C.V."), ("28 ", "Guadalajara", "Almacenadora GWTC, S.A. de C.V."), - ("29 ", "Guadalajara", "Federal Express Holdings (México) y Compañía, S.N.C. de C.V."), - ("3 ", "Aeropuerto Internacional de la Ciudad de México", "Aerovías de México, S.A. de C.V."), + ( + "29 ", + "Guadalajara", + "Federal Express Holdings (México) y Compañía, S.N.C. de C.V.", + ), + ( + "3 ", + "Aeropuerto Internacional de la Ciudad de México", + "Aerovías de México, S.A. de C.V.", + ), ("30 ", "Guaymas", "Administración Portuaria Integral de Guaymas, S.A. de C.V."), - ("31 ", "Lázaro Cárdenas", "Administración Portuaria Integral de Lázaro Cárdenas, S.A. de C.V."), + ( + "31 ", + "Lázaro Cárdenas", + "Administración Portuaria Integral de Lázaro Cárdenas, S.A. de C.V.", + ), ("33 ", "Lázaro Cárdenas", "Aarhuskarlshamn México, S.A. de C.V."), - ("35 ", "Manzanillo", "Administración Portuaria Integral de Manzanillo, S.A. de C.V."), + ( + "35 ", + "Manzanillo", + "Administración Portuaria Integral de Manzanillo, S.A. de C.V.", + ), ("36 ", "Manzanillo", "Comercializadora La Junta, S.A. de C.V."), ("38 ", "Manzanillo", "Operadora de la Cuenca del Pacífico, S.A. de C.V."), ("39 ", "Manzanillo", "SSA México, S.A. de C.V."), - ("4 ", "Aeropuerto Internacional de la Ciudad de México", "AAACESA Almacenes Fiscalizados, S.A. de C.V."), + ( + "4 ", + "Aeropuerto Internacional de la Ciudad de México", + "AAACESA Almacenes Fiscalizados, S.A. de C.V.", + ), ("40 ", "Manzanillo", "Terminal Internacional de Manzanillo, S.A. de C.V."), ("42 ", "Mazatlán", "Administración Portuaria Integral de Mazatlán, S.A. de C.V."), - ("43 ", "Mazatlán", "Administración Portuaria Integral de Topolobampo, S.A. de C.V."), + ( + "43 ", + "Mazatlán", + "Administración Portuaria Integral de Topolobampo, S.A. de C.V.", + ), ("44 ", "Monterrey", "Braniff Air Freight and Company, S.A. de C.V."), ("45 ", "Monterrey", "Kansas City Southern de México, S.A. de C.V."), ("46 ", "Nogales", "Servicios de Almacén Fiscalizado de Nogales, S.A. de C.V."), ("47 ", "Progreso", "Administración Portuaria Integral de Progreso, S.A. de C.V."), ("49 ", "Progreso", "Grupo de Desarrollo del Sureste, S.A. de C.V."), - ("5 ", "Aeropuerto Internacional de la Ciudad de México", "México Cargo Handling, S.A. de C.V."), + ( + "5 ", + "Aeropuerto Internacional de la Ciudad de México", + "México Cargo Handling, S.A. de C.V.", + ), ("50 ", "Progreso", "Multisur, S.A. de C.V."), ("51 ", "Querétaro", "Servicios Integrales y Desarrollo GMG, S.A. de C.V."), ("52 ", "Reynosa", "Recintos Fiscalizados de Noreste, S.A. de C.V."), - ("53 ", "Salina Cruz", "Administración Portuaria Integral de Salina Cruz, S.A. de C.V."), - ("54 ", "Cancún", "Administración Portuaria Integral de Quintana Roo, S.A. de C.V."), + ( + "53 ", + "Salina Cruz", + "Administración Portuaria Integral de Salina Cruz, S.A. de C.V.", + ), + ( + "54 ", + "Cancún", + "Administración Portuaria Integral de Quintana Roo, S.A. de C.V.", + ), ("56 ", "Toluca", "Braniff Air Freight and Company, S.A. de C.V."), ("57 ", "Toluca", "Federal Express Holdings (México) y Compañía, S.N.C. de C.V."), ("59 ", "Tuxpan", "Administración Portuaria Integral de Tuxpan, S.A. de C.V."), - ("6 ", "Aeropuerto Internacional de la Ciudad de México", "American Airlines de México, S.A. de C.V."), + ( + "6 ", + "Aeropuerto Internacional de la Ciudad de México", + "American Airlines de México, S.A. de C.V.", + ), ("60 ", "Tuxpan", "Fenoresinas, S.A. de C.V."), ("61 ", "Tuxpan", "Terminal Marítima de Tuxpan, S.A. de C.V."), ("62 ", "Tuxpan", "Terminales Marítimas Transunisa, S.A. de C.V."), @@ -118,8 +218,16 @@ seed = [ ("64 ", "Veracruz", "Almacenadora Golmex, S.A. de C.V."), ("66 ", "Veracruz", "CIF Almacenajes y Servicios, S.A. de C.V."), ("67 ", "Veracruz", "Corporación Integral de Comercio Exterior, S.A. de C.V."), - ("69 ", "Veracruz", "Internacional de Contenedores Asociados de Veracruz, S.A. de C.V."), - ("7 ", "Aeropuerto Internacional de la Ciudad de México", "Braniff Air Freight and Company, S.A. de C.V."), + ( + "69 ", + "Veracruz", + "Internacional de Contenedores Asociados de Veracruz, S.A. de C.V.", + ), + ( + "7 ", + "Aeropuerto Internacional de la Ciudad de México", + "Braniff Air Freight and Company, S.A. de C.V.", + ), ("71 ", "Veracruz", "Reparación Integral de Contenedores, S.A. de C.V."), ("73 ", "Veracruz", "Terminales de Cargas Especializadas, S.A. de C.V."), ("74 ", "Veracruz", "Vopak Terminals México, S.A. de C.V."), @@ -127,9 +235,17 @@ seed = [ ("76 ", "Manzanillo", "Cemex México, S.A. de C.V."), ("77 ", "Manzanillo", "Corporación Multimodal, S.A. de C.V."), ("78 ", "Ensenada", "Administración Portuaria Integral de Ensenada, S.A. de C.V."), - ("8 ", "Aeropuerto Internacional de la Ciudad de México", "Iberia de México, S.A."), + ( + "8 ", + "Aeropuerto Internacional de la Ciudad de México", + "Iberia de México, S.A.", + ), ("81 ", "Tuxpan", "Frigoríficos Especializados de Tuxpan, S.A. de C.V."), ("82 ", "Veracruz", "SSA México, S.A. de C.V."), - ("9 ", "Aeropuerto Internacional de la Ciudad de México", "Compañía Mexicana de Aviación, S.A. de C.V."), + ( + "9 ", + "Aeropuerto Internacional de la Ciudad de México", + "Compañía Mexicana de Aviación, S.A. de C.V.", + ), ("98 ", "Veracruz", "Corporación Portuaria de Veracruz, S.A. de C.V."), -] \ No newline at end of file +] diff --git a/backend/api/v1/modules/public/reference_data/customs_warehouses/test_customs_warehouses.py b/backend/api/v1/modules/public/reference_data/customs_warehouses/test_customs_warehouses.py index 315929c1..5cbb106f 100644 --- a/backend/api/v1/modules/public/reference_data/customs_warehouses/test_customs_warehouses.py +++ b/backend/api/v1/modules/public/reference_data/customs_warehouses/test_customs_warehouses.py @@ -7,6 +7,7 @@ app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_customs_warehouses(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,20 +17,32 @@ def test_list_customs_warehouses(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_customs_warehouse_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} - response = client.get("/customs-warehouses/invalid_key/invalid_customs", headers=headers) + response = client.get( + "/customs-warehouses/invalid_key/invalid_customs", headers=headers + ) assert response.status_code == 404 + def test_create_customs_warehouse_forbidden(): - response = client.post("/customs-warehouses/", json={"key": "TST", "customs": "TST", "description": "Test"}) + response = client.post( + "/customs-warehouses/", + json={"key": "TST", "customs": "TST", "description": "Test"}, + ) assert response.status_code in (403, 405, 404) + def test_update_customs_warehouse_forbidden(): - response = client.put("/customs-warehouses/TST/TST", json={"key": "TST", "customs": "TST", "description": "Test"}) + response = client.put( + "/customs-warehouses/TST/TST", + json={"key": "TST", "customs": "TST", "description": "Test"}, + ) assert response.status_code in (403, 405, 404) + def test_delete_customs_warehouse_forbidden(): response = client.delete("/customs-warehouses/TST/TST") assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/incoterms/dto.py b/backend/api/v1/modules/public/reference_data/incoterms/dto.py index 0ff49602..b95e485d 100644 --- a/backend/api/v1/modules/public/reference_data/incoterms/dto.py +++ b/backend/api/v1/modules/public/reference_data/incoterms/dto.py @@ -1,6 +1,7 @@ from pydantic import BaseModel, Field from pydantic import ConfigDict + class IncotermDTO(BaseModel): code: str = Field(..., min_length=1, max_length=5) description_es: str diff --git a/backend/api/v1/modules/public/reference_data/incoterms/models.py b/backend/api/v1/modules/public/reference_data/incoterms/models.py index 94ec4653..3e68e48e 100644 --- a/backend/api/v1/modules/public/reference_data/incoterms/models.py +++ b/backend/api/v1/modules/public/reference_data/incoterms/models.py @@ -2,11 +2,12 @@ from sqlalchemy import String, PrimaryKeyConstraint from sqlalchemy.orm import mapped_column, Mapped from core.database import Base + class Incoterm(Base): - __tablename__ = "incoterms" #GIncoterm + __tablename__ = "incoterms" # GIncoterm __table_args__ = ( PrimaryKeyConstraint("code", name="incoterms_pkey"), - {"schema": "public"} + {"schema": "public"}, ) code: Mapped[str] = mapped_column(String(5), nullable=False) diff --git a/backend/api/v1/modules/public/reference_data/incoterms/routes.py b/backend/api/v1/modules/public/reference_data/incoterms/routes.py index 894a1833..5ffec8f6 100644 --- a/backend/api/v1/modules/public/reference_data/incoterms/routes.py +++ b/backend/api/v1/modules/public/reference_data/incoterms/routes.py @@ -1,4 +1,3 @@ - from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db @@ -11,13 +10,12 @@ from typing import Any, Dict router = APIRouter(prefix="/incoterms") - @router.get("/", response_model=Dict[str, Any]) async def list_incoterms( page: int = Query(1, ge=1, description="Número de página"), page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): skip = (page - 1) * page_size query = db.query(Incoterm) @@ -27,23 +25,27 @@ async def list_incoterms( "items": [IncotermDTO.model_validate(obj) for obj in items], "total": total, "page": page, - "page_size": page_size + "page_size": page_size, } - + @router.get("/{key}", response_model=IncotermDTO) -async def get_incoterm(key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): +async def get_incoterm( + key: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): obj = db.query(Incoterm).filter(Incoterm.code == key).first() if not obj: raise HTTPException(status_code=404, detail="Not found") return IncotermDTO.model_validate(obj) - + @router.post("/", response_model=IncotermDTO, status_code=201) async def create_incoterm( data: IncotermDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = Incoterm(**data.model_dump()) db.add(obj) @@ -51,13 +53,13 @@ async def create_incoterm( db.refresh(obj) return IncotermDTO.model_validate(obj) - + @router.put("/{key}", response_model=IncotermDTO) async def update_incoterm( key: str, data: IncotermDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(Incoterm).filter(Incoterm.code == key).first() if not obj: @@ -68,12 +70,12 @@ async def update_incoterm( db.refresh(obj) return IncotermDTO.model_validate(obj) - + @router.delete("/{key}", status_code=204) async def delete_incoterm( key: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(Incoterm).filter(Incoterm.key == key).first() if not obj: diff --git a/backend/api/v1/modules/public/reference_data/incoterms/seed.py b/backend/api/v1/modules/public/reference_data/incoterms/seed.py index b7f9dbc6..0b9f87ce 100644 --- a/backend/api/v1/modules/public/reference_data/incoterms/seed.py +++ b/backend/api/v1/modules/public/reference_data/incoterms/seed.py @@ -10,4 +10,4 @@ seed = [ ("FOB", "PUERTO DE EMBARQUE CONVENIDO", "FREE ON BOARD"), ("CFR", "COSTO Y FLETE", "COST AND FREIGHT"), ("CIF", "COSTO, SEGURO Y FLETE", "COST, INSURANCE AND FREIGHT"), -] \ No newline at end of file +] diff --git a/backend/api/v1/modules/public/reference_data/incoterms/test_incoterms.py b/backend/api/v1/modules/public/reference_data/incoterms/test_incoterms.py index 865744d1..890951f2 100644 --- a/backend/api/v1/modules/public/reference_data/incoterms/test_incoterms.py +++ b/backend/api/v1/modules/public/reference_data/incoterms/test_incoterms.py @@ -7,6 +7,7 @@ app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_incoterms(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,20 +17,24 @@ def test_list_incoterms(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_incoterm_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/incoterms/invalid_key", headers=headers) assert response.status_code == 404 + def test_create_incoterm_forbidden(): response = client.post("/incoterms/", json={"key": "TST", "description": "Test"}) assert response.status_code in (403, 405, 404) + def test_update_incoterm_forbidden(): response = client.put("/incoterms/TST", json={"key": "TST", "description": "Test"}) assert response.status_code in (403, 405, 404) + def test_delete_incoterm_forbidden(): response = client.delete("/incoterms/TST") assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/invoice_types/dto.py b/backend/api/v1/modules/public/reference_data/invoice_types/dto.py index 3e3cd433..c6245814 100644 --- a/backend/api/v1/modules/public/reference_data/invoice_types/dto.py +++ b/backend/api/v1/modules/public/reference_data/invoice_types/dto.py @@ -2,6 +2,7 @@ from pydantic import BaseModel, Field from pydantic import ConfigDict from typing import Optional + class InvoiceTypeDTO(BaseModel): key: str = Field(..., min_length=1, max_length=5) description: str diff --git a/backend/api/v1/modules/public/reference_data/invoice_types/models.py b/backend/api/v1/modules/public/reference_data/invoice_types/models.py index 214aae00..6e0a6785 100644 --- a/backend/api/v1/modules/public/reference_data/invoice_types/models.py +++ b/backend/api/v1/modules/public/reference_data/invoice_types/models.py @@ -4,16 +4,20 @@ from core.database import Base class InvoiceType(Base): - __tablename__ = "invoice_types" #GTiposFactura + __tablename__ = "invoice_types" # GTiposFactura __table_args__ = ( PrimaryKeyConstraint("key", name="invoice_types_pkey"), - {"schema": "public"} # opcional + {"schema": "public"}, # opcional ) - key: Mapped[str] = mapped_column(String(5), nullable=False) # clave del tipo de factura - description: Mapped[str] = mapped_column(String(50), nullable=False) # descripción oficial (en español) - note: Mapped[str] = mapped_column(String(500)) # observación o comentario adicional - type: Mapped[str] = mapped_column(String(15)) # tipo + key: Mapped[str] = mapped_column( + String(5), nullable=False + ) # clave del tipo de factura + description: Mapped[str] = mapped_column( + String(50), nullable=False + ) # descripción oficial (en español) + note: Mapped[str] = mapped_column(String(500)) # observación o comentario adicional + type: Mapped[str] = mapped_column(String(15)) # tipo def __repr__(self): return f"" diff --git a/backend/api/v1/modules/public/reference_data/invoice_types/routes.py b/backend/api/v1/modules/public/reference_data/invoice_types/routes.py index 9cd997a1..9078ee13 100644 --- a/backend/api/v1/modules/public/reference_data/invoice_types/routes.py +++ b/backend/api/v1/modules/public/reference_data/invoice_types/routes.py @@ -1,4 +1,3 @@ - from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db @@ -16,7 +15,7 @@ def list_invoice_types( page: int = Query(1, ge=1, description="Número de página"), page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): skip = (page - 1) * page_size query = db.query(InvoiceType) @@ -26,21 +25,27 @@ def list_invoice_types( "items": [InvoiceTypeDTO.model_validate(obj) for obj in items], "total": total, "page": page, - "page_size": page_size + "page_size": page_size, } + @router.get("/{key}", response_model=InvoiceTypeDTO) -def get_invoice_type(key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): +def get_invoice_type( + key: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): obj = db.query(InvoiceType).filter(InvoiceType.key == key).first() if not obj: raise HTTPException(status_code=404, detail="Not found") return InvoiceTypeDTO.model_validate(obj) + @router.post("/", response_model=InvoiceTypeDTO, status_code=201) def create_invoice_type( data: InvoiceTypeDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = InvoiceType(**data.model_dump()) db.add(obj) @@ -48,12 +53,13 @@ def create_invoice_type( db.refresh(obj) return InvoiceTypeDTO.model_validate(obj) + @router.put("/{key}", response_model=InvoiceTypeDTO) def update_invoice_type( key: str, data: InvoiceTypeDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(InvoiceType).filter(InvoiceType.key == key).first() if not obj: @@ -64,11 +70,12 @@ def update_invoice_type( db.refresh(obj) return InvoiceTypeDTO.model_validate(obj) + @router.delete("/{key}", status_code=204) def delete_invoice_type( key: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(InvoiceType).filter(InvoiceType.key == key).first() if not obj: diff --git a/backend/api/v1/modules/public/reference_data/invoice_types/seed.py b/backend/api/v1/modules/public/reference_data/invoice_types/seed.py index 54d17b7a..dd8f0fcb 100644 --- a/backend/api/v1/modules/public/reference_data/invoice_types/seed.py +++ b/backend/api/v1/modules/public/reference_data/invoice_types/seed.py @@ -1,13 +1,38 @@ seed = [ ("DONAC", "DONACION", "", "AMBOS"), ("EXDEF", "EXPORTACION DEFINITIVA", "", "MATERIAL"), - ("MATDE", "MATERIA PRIMA O MATERIAL DEVUELTO", "ESTE PROCESO CONSISTE EN SOLO DESCARGAR LAS PARTES DADAS DE ALTA EN MATERIALES QUE SON RETORNADAS SIN NINGUNA MODIFICACION (A1)", "MATERIAL"), - ("NODES", "NO HACE DESCARGA", "ESTE PROCESO DE ACTUALIZACION CONSISTE EN EXPORTAR UNA MERCANCIA Y NO DESCARGAR, POR LO TANTO NO EXISTE REPORTE DE DESCARGAS Y NO AFECTA SALDOS.", "AMBOS"), - ("PTERM", "PRODUCTO TERMINADO Y VIRTUALES", "EL PRODUCTO TERMINADO Y VIRTUALES DESCARGARAN: 1) APARTIR DE LOS COMPONENTES DE CADA PRODUCTO TERMINADO REGISTRADO EN LAS PARTIDAS DE EXPORTACION. 2) POR PARTE, CON LAS OPCIONES DE PODER DESCARGAR POR SUSTITUTO Y POR CLASE EN CASO DE INSUFICIENCIAS DEL COMPONENTE.", "MATERIAL"), - ("REPAR", "REPARACION", "PROCESO QUE CONSISTE EN DOS ETAPAS: 1) DESCARGA EL PRODUCTO DE REPARACION QUE SE IMPORTO PARA REPARA, 2) DESCARGA EL LISTADO DE COMPONENTES QUE SE AGREGO AL PRODUCTO DE REPARACION", "MATERIAL"), + ( + "MATDE", + "MATERIA PRIMA O MATERIAL DEVUELTO", + "ESTE PROCESO CONSISTE EN SOLO DESCARGAR LAS PARTES DADAS DE ALTA EN MATERIALES QUE SON RETORNADAS SIN NINGUNA MODIFICACION (A1)", + "MATERIAL", + ), + ( + "NODES", + "NO HACE DESCARGA", + "ESTE PROCESO DE ACTUALIZACION CONSISTE EN EXPORTAR UNA MERCANCIA Y NO DESCARGAR, POR LO TANTO NO EXISTE REPORTE DE DESCARGAS Y NO AFECTA SALDOS.", + "AMBOS", + ), + ( + "PTERM", + "PRODUCTO TERMINADO Y VIRTUALES", + "EL PRODUCTO TERMINADO Y VIRTUALES DESCARGARAN: 1) APARTIR DE LOS COMPONENTES DE CADA PRODUCTO TERMINADO REGISTRADO EN LAS PARTIDAS DE EXPORTACION. 2) POR PARTE, CON LAS OPCIONES DE PODER DESCARGAR POR SUSTITUTO Y POR CLASE EN CASO DE INSUFICIENCIAS DEL COMPONENTE.", + "MATERIAL", + ), + ( + "REPAR", + "REPARACION", + "PROCESO QUE CONSISTE EN DOS ETAPAS: 1) DESCARGA EL PRODUCTO DE REPARACION QUE SE IMPORTO PARA REPARA, 2) DESCARGA EL LISTADO DE COMPONENTES QUE SE AGREGO AL PRODUCTO DE REPARACION", + "MATERIAL", + ), ("SCRAP", "SCRAP", "", "AMBOS"), - ("VEMEX", "VENTAS EN MEXICO", "ESTE PROCESO CONSISTE EN LA VENTA EN EL MERCADO NACIONAL DE LOS PRODUCTOS.", "AMBOS"), - ("VIRTU", "VIRTUALES", "", "MATERIAL"), + ( + "VEMEX", + "VENTAS EN MEXICO", + "ESTE PROCESO CONSISTE EN LA VENTA EN EL MERCADO NACIONAL DE LOS PRODUCTOS.", + "AMBOS", + ), + ("VIRTU", "VIRTUALES", "", "MATERIAL"), ("AFIJO", "ACTIVO FIJO", "", "ACTIVO FIJO"), ("REEXP", "REEXPEDICION", "", "ACTIVO FIJO"), ] diff --git a/backend/api/v1/modules/public/reference_data/invoice_types/test_invoice_types.py b/backend/api/v1/modules/public/reference_data/invoice_types/test_invoice_types.py index 1f2d2897..ce80a933 100644 --- a/backend/api/v1/modules/public/reference_data/invoice_types/test_invoice_types.py +++ b/backend/api/v1/modules/public/reference_data/invoice_types/test_invoice_types.py @@ -7,6 +7,7 @@ app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_invoice_types(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,20 +17,28 @@ def test_list_invoice_types(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_invoice_type_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/invoice-types/invalid_key", headers=headers) assert response.status_code == 404 + def test_create_invoice_type_forbidden(): - response = client.post("/invoice-types/", json={"key": "TST", "description": "Test"}) + response = client.post( + "/invoice-types/", json={"key": "TST", "description": "Test"} + ) assert response.status_code in (403, 405, 404) + def test_update_invoice_type_forbidden(): - response = client.put("/invoice-types/TST", json={"key": "TST", "description": "Test"}) + response = client.put( + "/invoice-types/TST", json={"key": "TST", "description": "Test"} + ) assert response.status_code in (403, 405, 404) + def test_delete_invoice_type_forbidden(): response = client.delete("/invoice-types/TST") assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/material_types/dto.py b/backend/api/v1/modules/public/reference_data/material_types/dto.py index f04e0d46..6619710b 100644 --- a/backend/api/v1/modules/public/reference_data/material_types/dto.py +++ b/backend/api/v1/modules/public/reference_data/material_types/dto.py @@ -1,10 +1,10 @@ from pydantic import BaseModel, Field from pydantic import ConfigDict + class MaterialTypeDTO(BaseModel): key: str = Field(..., min_length=1, max_length=10) type: str = Field(..., min_length=1, max_length=15) description: str model_config = ConfigDict(from_attributes=True) - diff --git a/backend/api/v1/modules/public/reference_data/material_types/models.py b/backend/api/v1/modules/public/reference_data/material_types/models.py index 5fc11239..a537f721 100644 --- a/backend/api/v1/modules/public/reference_data/material_types/models.py +++ b/backend/api/v1/modules/public/reference_data/material_types/models.py @@ -2,16 +2,19 @@ from sqlalchemy import String, PrimaryKeyConstraint from sqlalchemy.orm import mapped_column, Mapped from core.database import Base + class MaterialType(Base): - __tablename__ = "material_types" #STipoMat QTipoActFijo + __tablename__ = "material_types" # STipoMat QTipoActFijo __table_args__ = ( PrimaryKeyConstraint("key", name="material_types_pkey"), - {"schema": "public"} + {"schema": "public"}, ) - key: Mapped[str] = mapped_column(String(10), nullable=False) # clave del material - type: Mapped[str] = mapped_column(String(15), nullable=False) # tipo - description: Mapped[str] = mapped_column(String(256), nullable=False) # descripción oficial (en español) + key: Mapped[str] = mapped_column(String(10), nullable=False) # clave del material + type: Mapped[str] = mapped_column(String(15), nullable=False) # tipo + description: Mapped[str] = mapped_column( + String(256), nullable=False + ) # descripción oficial (en español) def __repr__(self): return f"" diff --git a/backend/api/v1/modules/public/reference_data/material_types/routes.py b/backend/api/v1/modules/public/reference_data/material_types/routes.py index dab0b4ec..2648b2da 100644 --- a/backend/api/v1/modules/public/reference_data/material_types/routes.py +++ b/backend/api/v1/modules/public/reference_data/material_types/routes.py @@ -1,4 +1,3 @@ - from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db @@ -11,13 +10,12 @@ from typing import Any, Dict router = APIRouter(prefix="/material-types") - @router.get("/", response_model=Dict[str, Any]) async def list_material_types( page: int = Query(1, ge=1, description="Número de página"), page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): skip = (page - 1) * page_size query = db.query(MaterialType) @@ -27,23 +25,27 @@ async def list_material_types( "items": [MaterialTypeDTO.model_validate(obj) for obj in items], "total": total, "page": page, - "page_size": page_size + "page_size": page_size, } - + @router.get("/{key}", response_model=MaterialTypeDTO) -async def get_material_type(key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): +async def get_material_type( + key: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): obj = db.query(MaterialType).filter(MaterialType.key == key).first() if not obj: raise HTTPException(status_code=404, detail="Not found") return obj - + @router.post("/", response_model=MaterialTypeDTO, status_code=201) async def create_material_type( data: MaterialTypeDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = MaterialType(**data.dict()) db.add(obj) @@ -51,13 +53,13 @@ async def create_material_type( db.refresh(obj) return obj - + @router.put("/{key}", response_model=MaterialTypeDTO) async def update_material_type( key: str, data: MaterialTypeDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(MaterialType).filter(MaterialType.key == key).first() if not obj: @@ -68,12 +70,12 @@ async def update_material_type( db.refresh(obj) return obj - + @router.delete("/{key}", status_code=204) async def delete_material_type( key: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(MaterialType).filter(MaterialType.key == key).first() if not obj: diff --git a/backend/api/v1/modules/public/reference_data/material_types/seed.py b/backend/api/v1/modules/public/reference_data/material_types/seed.py index 3f8699d8..55c85356 100644 --- a/backend/api/v1/modules/public/reference_data/material_types/seed.py +++ b/backend/api/v1/modules/public/reference_data/material_types/seed.py @@ -32,5 +32,5 @@ seed = [ ("MAQEQ", "MAQUINARIA Y EQUIPO", "ACTIVO FIJO"), ("MAQUI", "MAQUINARIA", "ACTIVO FIJO"), ("REFAC", "REFACCIONES", "ACTIVO FIJO"), - ("TERR", "TERRRENOS" , "ACTIVO FIJO"), -] \ No newline at end of file + ("TERR", "TERRRENOS", "ACTIVO FIJO"), +] diff --git a/backend/api/v1/modules/public/reference_data/material_types/test_material_types.py b/backend/api/v1/modules/public/reference_data/material_types/test_material_types.py index 796b1971..c9bf825f 100644 --- a/backend/api/v1/modules/public/reference_data/material_types/test_material_types.py +++ b/backend/api/v1/modules/public/reference_data/material_types/test_material_types.py @@ -7,6 +7,7 @@ app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_material_types(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,20 +17,28 @@ def test_list_material_types(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_material_type_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/material-types/invalid_key", headers=headers) assert response.status_code == 404 + def test_create_material_type_forbidden(): - response = client.post("/material-types/", json={"key": "TST", "description": "Test"}) + response = client.post( + "/material-types/", json={"key": "TST", "description": "Test"} + ) assert response.status_code in (403, 405, 404) + def test_update_material_type_forbidden(): - response = client.put("/material-types/TST", json={"key": "TST", "description": "Test"}) + response = client.put( + "/material-types/TST", json={"key": "TST", "description": "Test"} + ) assert response.status_code in (403, 405, 404) + def test_delete_material_type_forbidden(): response = client.delete("/material-types/TST") assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/payment_methods/dto.py b/backend/api/v1/modules/public/reference_data/payment_methods/dto.py index c98ff8b9..8ce56d6a 100644 --- a/backend/api/v1/modules/public/reference_data/payment_methods/dto.py +++ b/backend/api/v1/modules/public/reference_data/payment_methods/dto.py @@ -1,9 +1,9 @@ from pydantic import BaseModel, Field from pydantic import ConfigDict + class PaymentMethodDTO(BaseModel): key: str = Field(..., min_length=1, max_length=2) description: str model_config = ConfigDict(from_attributes=True) - diff --git a/backend/api/v1/modules/public/reference_data/payment_methods/models.py b/backend/api/v1/modules/public/reference_data/payment_methods/models.py index 9f50a00f..3162c7e1 100644 --- a/backend/api/v1/modules/public/reference_data/payment_methods/models.py +++ b/backend/api/v1/modules/public/reference_data/payment_methods/models.py @@ -2,11 +2,12 @@ from sqlalchemy import String, PrimaryKeyConstraint from sqlalchemy.orm import mapped_column, Mapped from core.database import Base + class PaymentMethod(Base): - __tablename__ = "payment_methods" #GFormaPago + __tablename__ = "payment_methods" # GFormaPago __table_args__ = ( PrimaryKeyConstraint("key", name="payment_methods_pkey"), - {"schema": "public"} # opcional + {"schema": "public"}, # opcional ) key: Mapped[str] = mapped_column(String(2), nullable=False) diff --git a/backend/api/v1/modules/public/reference_data/payment_methods/routes.py b/backend/api/v1/modules/public/reference_data/payment_methods/routes.py index ba27a187..3624878d 100644 --- a/backend/api/v1/modules/public/reference_data/payment_methods/routes.py +++ b/backend/api/v1/modules/public/reference_data/payment_methods/routes.py @@ -1,4 +1,3 @@ - from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db @@ -16,7 +15,7 @@ def list_payment_methods( page: int = Query(1, ge=1, description="Número de página"), page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): skip = (page - 1) * page_size query = db.query(PaymentMethod) @@ -26,21 +25,27 @@ def list_payment_methods( "items": [PaymentMethodDTO.model_validate(obj) for obj in items], "total": total, "page": page, - "page_size": page_size + "page_size": page_size, } + @router.get("/{key}", response_model=PaymentMethodDTO) -def get_payment_method(key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): +def get_payment_method( + key: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): obj = db.query(PaymentMethod).filter(PaymentMethod.key == key).first() if not obj: raise HTTPException(status_code=404, detail="Not found") return obj + @router.post("/", response_model=PaymentMethodDTO, status_code=201) def create_payment_method( data: PaymentMethodDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = PaymentMethod(**data.dict()) db.add(obj) @@ -48,12 +53,13 @@ def create_payment_method( db.refresh(obj) return obj + @router.put("/{key}", response_model=PaymentMethodDTO) def update_payment_method( key: str, data: PaymentMethodDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(PaymentMethod).filter(PaymentMethod.key == key).first() if not obj: @@ -64,11 +70,12 @@ def update_payment_method( db.refresh(obj) return obj + @router.delete("/{key}", status_code=204) def delete_payment_method( key: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(PaymentMethod).filter(PaymentMethod.key == key).first() if not obj: diff --git a/backend/api/v1/modules/public/reference_data/payment_methods/seed.py b/backend/api/v1/modules/public/reference_data/payment_methods/seed.py index b385f170..6ca6b29d 100644 --- a/backend/api/v1/modules/public/reference_data/payment_methods/seed.py +++ b/backend/api/v1/modules/public/reference_data/payment_methods/seed.py @@ -10,7 +10,10 @@ seed = [ ("18", "ESTIMULO FISCAL."), ("19", "OTROS MEDIOS DE GARANTIA."), ("2", "FIANZA."), - ("20", "DEROGADA. --- (PAGO CONFORME AL ARTICULO 7 DE LA LEY DE INGRESOS DE LA FEDERACION, VIGENTE)"), + ( + "20", + "DEROGADA. --- (PAGO CONFORME AL ARTICULO 7 DE LA LEY DE INGRESOS DE LA FEDERACION, VIGENTE)", + ), ("21", "CRÉDITO EN IVA E IEPS."), ("22", "GARANTÍA EN IVA E IEPS."), ("4", "DEPOSITO EN CUENTA ADUANERA."), @@ -19,4 +22,4 @@ seed = [ ("7", "CARGO A PARTIDA PRESUPUESTAL GOBIERNO FEDERAL."), ("8", "FRANQUICIA."), ("9", "EXENTO DE PAGO."), -] \ No newline at end of file +] diff --git a/backend/api/v1/modules/public/reference_data/payment_methods/test_payment_methods.py b/backend/api/v1/modules/public/reference_data/payment_methods/test_payment_methods.py index a16785a1..c0ca8296 100644 --- a/backend/api/v1/modules/public/reference_data/payment_methods/test_payment_methods.py +++ b/backend/api/v1/modules/public/reference_data/payment_methods/test_payment_methods.py @@ -7,6 +7,7 @@ app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_payment_methods(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,20 +17,28 @@ def test_list_payment_methods(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_payment_method_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/payment-methods/invalid_key", headers=headers) assert response.status_code == 404 + def test_create_payment_method_forbidden(): - response = client.post("/payment-methods/", json={"key": "TST", "description": "Test"}) + response = client.post( + "/payment-methods/", json={"key": "TST", "description": "Test"} + ) assert response.status_code in (403, 405, 404) + def test_update_payment_method_forbidden(): - response = client.put("/payment-methods/TST", json={"key": "TST", "description": "Test"}) + response = client.put( + "/payment-methods/TST", json={"key": "TST", "description": "Test"} + ) assert response.status_code in (403, 405, 404) + def test_delete_payment_method_forbidden(): response = client.delete("/payment-methods/TST") assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/pedimento_codes/dto.py b/backend/api/v1/modules/public/reference_data/pedimento_codes/dto.py index 20a1053f..d9e918dd 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_codes/dto.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_codes/dto.py @@ -2,9 +2,9 @@ from pydantic import BaseModel, Field from pydantic import ConfigDict from typing import Optional + class PedimentoCodeDTO(BaseModel): code: str = Field(..., min_length=1, max_length=3) description: str model_config = ConfigDict(from_attributes=True) - diff --git a/backend/api/v1/modules/public/reference_data/pedimento_codes/models.py b/backend/api/v1/modules/public/reference_data/pedimento_codes/models.py index a0e99555..8c951c07 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_codes/models.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_codes/models.py @@ -5,25 +5,23 @@ from core.database import Base if TYPE_CHECKING: from ..code_pedimento_regimens.models import CodePedimentoRegimen - + class PedimentoCode(Base): __tablename__ = "pedimento_codes" # GClavePed __table_args__ = ( PrimaryKeyConstraint("code", name="pedimento_codes_pkey"), - {"schema": "public"} # esquema del anexo 22 + {"schema": "public"}, # esquema del anexo 22 ) - code: Mapped[str] = mapped_column(String(3), nullable=False) + code: Mapped[str] = mapped_column(String(3), nullable=False) description: Mapped[str] = mapped_column(String(250), nullable=False) # Relación con los regímenes asociados - #GClavePedRegimen - regimens: Mapped[List['CodePedimentoRegimen']] = relationship( - "CodePedimentoRegimen", - uselist=True, - back_populates="pedimento" + # GClavePedRegimen + regimens: Mapped[List["CodePedimentoRegimen"]] = relationship( + "CodePedimentoRegimen", uselist=True, back_populates="pedimento" ) def __repr__(self): - return f"" \ No newline at end of file + return f"" diff --git a/backend/api/v1/modules/public/reference_data/pedimento_codes/routes.py b/backend/api/v1/modules/public/reference_data/pedimento_codes/routes.py index 34097e0b..e27d3a66 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_codes/routes.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_codes/routes.py @@ -1,4 +1,3 @@ - from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db @@ -16,7 +15,7 @@ def list_pedimento_codes( page: int = Query(1, ge=1, description="Número de página"), page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): skip = (page - 1) * page_size query = db.query(PedimentoCode) @@ -26,21 +25,27 @@ def list_pedimento_codes( "items": [PedimentoCodeDTO.model_validate(obj) for obj in items], "total": total, "page": page, - "page_size": page_size + "page_size": page_size, } + @router.get("/{code}", response_model=PedimentoCodeDTO) -def get_pedimento_code(code: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): +def get_pedimento_code( + code: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): obj = db.query(PedimentoCode).filter(PedimentoCode.code == code).first() if not obj: raise HTTPException(status_code=404, detail="Not found") return obj + @router.post("/", response_model=PedimentoCodeDTO, status_code=201) def create_pedimento_code( data: PedimentoCodeDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = PedimentoCode(**data.dict()) db.add(obj) @@ -48,12 +53,13 @@ def create_pedimento_code( db.refresh(obj) return obj + @router.put("/{code}", response_model=PedimentoCodeDTO) def update_pedimento_code( code: str, data: PedimentoCodeDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(PedimentoCode).filter(PedimentoCode.code == code).first() if not obj: @@ -64,11 +70,12 @@ def update_pedimento_code( db.refresh(obj) return obj + @router.delete("/{code}", status_code=204) def delete_pedimento_code( code: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(PedimentoCode).filter(PedimentoCode.code == code).first() if not obj: diff --git a/backend/api/v1/modules/public/reference_data/pedimento_codes/seed.py b/backend/api/v1/modules/public/reference_data/pedimento_codes/seed.py index dace16cd..1afbcea6 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_codes/seed.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_codes/seed.py @@ -3,27 +3,69 @@ seed = [ ("A3", "REGULARIZACION DE MERCANCIAS (IMPORTACION DEFINITIVA)."), ("A4", "INTRODUCCION PARA DEPOSITO FISCAL (AGD)."), ("A5", "INTRODUCCION A DEPOSITO FISCAL EN LOCAL AUTORIZADO."), - ("A6", "IMPORTACIÓN TEMPORAL DE BIENES DE ACTIVO FIJO POR PARTE DE EMPRESAS CON PITEX."), - ("AD", "IMPORTACIÓN TEMPORAL DE MERCANCIAS DESTINADAS A CONVENCIONES Y CONGRESOS INTERNACIONALES (ARTICULO 106, FRACCION III, INCISO A) DE LA LEY)."), + ( + "A6", + "IMPORTACIÓN TEMPORAL DE BIENES DE ACTIVO FIJO POR PARTE DE EMPRESAS CON PITEX.", + ), + ( + "AD", + "IMPORTACIÓN TEMPORAL DE MERCANCIAS DESTINADAS A CONVENCIONES Y CONGRESOS INTERNACIONALES (ARTICULO 106, FRACCION III, INCISO A) DE LA LEY).", + ), ("AF", "IMPORTACION TEMPORAL DE BIENES DE ACTIVO FIJO (IMMEX)."), - ("AJ", "IMPORTACION Y EXPORTACION TEMPORAL DE ENVASES DE MERCANCIAS (ARTICULOS 106, FRACCION II, INCISO B) Y 116, FRACCION II, INCISO A) DE LA LEY)."), - ("BA", "IMPORTACION Y EXPORTACION TEMPORAL DE BIENES PARA SER RETORNADOS EN SU MISMO ESTADO. (ARTICULO 106, FRACCIONES II, INCISOS A) Y C), Y IV, INCISO B) DE LA LEY)."), + ( + "AJ", + "IMPORTACION Y EXPORTACION TEMPORAL DE ENVASES DE MERCANCIAS (ARTICULOS 106, FRACCION II, INCISO B) Y 116, FRACCION II, INCISO A) DE LA LEY).", + ), + ( + "BA", + "IMPORTACION Y EXPORTACION TEMPORAL DE BIENES PARA SER RETORNADOS EN SU MISMO ESTADO. (ARTICULO 106, FRACCIONES II, INCISOS A) Y C), Y IV, INCISO B) DE LA LEY).", + ), ("BB", "EXPORTACION, IMPORTACION Y RETORNOS VIRTUALES."), - ("BC", "IMPORTACION Y EXPORTACION TEMPORAL DE MERCANCIAS DESTINADAS A EVENTOS CULTURALES O DEPORTIVOS (ARTICULO 106, FRACCION III, INCISO B DE LA LEY)."), - ("BD", "IMPORTACION Y EXPORTACION TEMPORAL DE EQUIPO PARA FILMACION (ARTICULOS 106, FRACCION III, INCISO C) Y 116, FRACCION II INCISO D) DE LA LEY)."), - ("BE", "IMPORTACION Y EXPORTACION TEMPORAL DE VEHICULOS DE PRUEBA (ARTICULO 106, FRACCION III, INCISO D) DE LA LEY)."), - ("BF", "EXPORTACION TEMPORAL DE MERCANCIAS DESTINADAS A EXPOSICIONES, CONVENCIONES O EVENTOS CULTURALES O DEPORTIVOS (ARTICULO 116, FRACCION III DE LA LEY)."), - ("BH", "IMPORTACION TEMPORAL DE CONTENEDORES, AVIONES, HELICOPTEROS, EMBARCACIONES Y CARROS DE FERROCARRIL (ARTICULO 106, FRACCION V, INCISOS A), B) Y E) DE LA LEY)."), + ( + "BC", + "IMPORTACION Y EXPORTACION TEMPORAL DE MERCANCIAS DESTINADAS A EVENTOS CULTURALES O DEPORTIVOS (ARTICULO 106, FRACCION III, INCISO B DE LA LEY).", + ), + ( + "BD", + "IMPORTACION Y EXPORTACION TEMPORAL DE EQUIPO PARA FILMACION (ARTICULOS 106, FRACCION III, INCISO C) Y 116, FRACCION II INCISO D) DE LA LEY).", + ), + ( + "BE", + "IMPORTACION Y EXPORTACION TEMPORAL DE VEHICULOS DE PRUEBA (ARTICULO 106, FRACCION III, INCISO D) DE LA LEY).", + ), + ( + "BF", + "EXPORTACION TEMPORAL DE MERCANCIAS DESTINADAS A EXPOSICIONES, CONVENCIONES O EVENTOS CULTURALES O DEPORTIVOS (ARTICULO 116, FRACCION III DE LA LEY).", + ), + ( + "BH", + "IMPORTACION TEMPORAL DE CONTENEDORES, AVIONES, HELICOPTEROS, EMBARCACIONES Y CARROS DE FERROCARRIL (ARTICULO 106, FRACCION V, INCISOS A), B) Y E) DE LA LEY).", + ), ("BI", "IMPORTACION TEMPORAL (ARTICULO 106, FRACCION III, INCISO E) DE LA LEY)."), - ("BM", "EXPORTACION TEMPORAL DE MERCANCIAS PARA SU TRANSFORMACION, ELABORACION O REPARACION (ARTICULO 117 DE LA LEY)."), - ("BO", "EXPORTACION TEMPORAL PARA REPARACION O SUSTITUCION Y RETORNO AL PAIS (IMMEX, RFE U OPERADOR ECONOMICO AUTORIZADO."), - ("BP", "IMPORTACION Y EXPORTACION TEMPORAL DE MUESTRAS O MUESTRARIOS (ARTICULOS 106, FRACCION II, INCISO D) Y 116, FRACCION II, INCISO C) DE LA LEY)."), + ( + "BM", + "EXPORTACION TEMPORAL DE MERCANCIAS PARA SU TRANSFORMACION, ELABORACION O REPARACION (ARTICULO 117 DE LA LEY).", + ), + ( + "BO", + "EXPORTACION TEMPORAL PARA REPARACION O SUSTITUCION Y RETORNO AL PAIS (IMMEX, RFE U OPERADOR ECONOMICO AUTORIZADO.", + ), + ( + "BP", + "IMPORTACION Y EXPORTACION TEMPORAL DE MUESTRAS O MUESTRARIOS (ARTICULOS 106, FRACCION II, INCISO D) Y 116, FRACCION II, INCISO C) DE LA LEY).", + ), ("BR", "EXPORTACION TEMPORAL Y RETORNO DE MERCANCIAS FUNGIBLES."), - ("C1", "IMPORTACION DEFINITIVA A LA FRANJA FRONTERIZA NORTE Y REGION FRONTERIZA AL AMPARO DEL „DECRETO DE LA FRANJA O REGION FRONTERIZA“ (DOF 24/12/2008 Y SUS POSTERIORES MODIFICACIONES)."), + ( + "C1", + "IMPORTACION DEFINITIVA A LA FRANJA FRONTERIZA NORTE Y REGION FRONTERIZA AL AMPARO DEL „DECRETO DE LA FRANJA O REGION FRONTERIZA“ (DOF 24/12/2008 Y SUS POSTERIORES MODIFICACIONES).", + ), ("C3", "EXTRACCION DE DEPOSITO FISCAL DE FRANJA O REGION FRONTERIZA (AGD)."), ("CT", "PEDIMENTO COMPLEMENTARIO."), ("D1", "RETORNO POR SUSTITUCION."), - ("E1", "EXTRACCION DE DEPOSITO FISCAL DE BIENES QUE SERAN SUJETOS A TRANSFORMACION, ELABORACION O REPARACION (AGD)."), + ( + "E1", + "EXTRACCION DE DEPOSITO FISCAL DE BIENES QUE SERAN SUJETOS A TRANSFORMACION, ELABORACION O REPARACION (AGD).", + ), ("E2", "EXTRACCION DE DEPOSITO FISCAL DE BIENES DE ACTIVO FIJO (AGD)."), ("E3", "EXTRACCION DE DEPOSITO FISCAL EN LOCAL AUTORIZADO (INSUMOS)."), ("E4", "EXTRACCION DE DEPOSITO FISCAL EN LOCAL AUTORIZADO (ACTIVO FIJO)."), @@ -31,49 +73,112 @@ seed = [ ("F3", "EXTRACCION DE DEPOSITO FISCAL (IA)."), ("F4", "CAMBIO DE REGIMEN DE INSUMOS O DE MERCANCIA EXPORTADA TEMPORALMENTE."), ("F5", "CAMBIO DE REGIMEN DE MERCANCÍAS DE IMPORTACIÓN TEMPORAL A DEFINITIVA."), - ("F8", "INTRODUCCION Y EXTRACCION DE DEPOSITO FISCAL DE MERCANCIAS NACIONALES O NACIONALIZADAS EN TIENDAS LIBRES DE IMPUESTOS (DUTY FREE)."), - ("F9", "INTRODUCCION Y EXTRACCION DE DEPOSITO FISCAL DE MERCANCIAS EXTRANJERAS PARA EXPOSICION Y VENTA DE MERCANCIAS EN TIENDAS LIBRES DE IMPUESTOS (DUTY FREE)."), + ( + "F8", + "INTRODUCCION Y EXTRACCION DE DEPOSITO FISCAL DE MERCANCIAS NACIONALES O NACIONALIZADAS EN TIENDAS LIBRES DE IMPUESTOS (DUTY FREE).", + ), + ( + "F9", + "INTRODUCCION Y EXTRACCION DE DEPOSITO FISCAL DE MERCANCIAS EXTRANJERAS PARA EXPOSICION Y VENTA DE MERCANCIAS EN TIENDAS LIBRES DE IMPUESTOS (DUTY FREE).", + ), ("G1", "EXTRACCION DE DEPOSITO FISCAL (AGD)."), - ("G2", "EXTRACCION DE DEPOSITO FISCAL EN LOCAL AUTORIZADO PARA SU IMPORTACION DEFINITIVA."), - ("G6", "INFORME DE EXTRACCION DE DEPOSITO FISCAL DE MERCANCIAS NACIONALES O NACIONALIZADAS VENDIDAS EN TIENDAS LIBRES DE IMPUESTOS (DUTY FREE)."), - ("G7", "INFORME DE EXTRACCION DE DEPOSITO FISCAL DE MERCANCIAS EXTRANJERAS VENDIDAS EN TIENDAS LIBRES DE IMPUESTOS (DUTY FREE)."), + ( + "G2", + "EXTRACCION DE DEPOSITO FISCAL EN LOCAL AUTORIZADO PARA SU IMPORTACION DEFINITIVA.", + ), + ( + "G6", + "INFORME DE EXTRACCION DE DEPOSITO FISCAL DE MERCANCIAS NACIONALES O NACIONALIZADAS VENDIDAS EN TIENDAS LIBRES DE IMPUESTOS (DUTY FREE).", + ), + ( + "G7", + "INFORME DE EXTRACCION DE DEPOSITO FISCAL DE MERCANCIAS EXTRANJERAS VENDIDAS EN TIENDAS LIBRES DE IMPUESTOS (DUTY FREE).", + ), ("G8", "REINCORPORAR AL MERCADO NACIONAL (RFE)."), - ("G9", "TRANSFERENCIA DE MERCANCÍAS DE RECINTO FISCALIZADO ESTRATEGICO NO COLINDANTE CON LA ADUANA (RETIRO VIRTUAL PARA IMPORTACIÓN DEFINTIVA POR RESIDENTES EN TERRITORIO NACIONAL)."), + ( + "G9", + "TRANSFERENCIA DE MERCANCÍAS DE RECINTO FISCALIZADO ESTRATEGICO NO COLINDANTE CON LA ADUANA (RETIRO VIRTUAL PARA IMPORTACIÓN DEFINTIVA POR RESIDENTES EN TERRITORIO NACIONAL).", + ), ("GC", "GLOBAL COMPLEMENTARIO."), ("H1", "RETORNO DE MERCANCIAS EN SU MISMO ESTADO."), ("H8", "RETORNO DE ENVASES."), - ("I1", "IMPORTACION, EXPORTACION Y RETORNO DE MERCANCIAS ELABORADAS, TRANSFORMADAS O REPARADAS."), - ("IN", "IMPORTACION TEMPORAL DE BIENES QUE SERAN SUJETOS A TRANSFORMACION, ELABORACION O REPARACION (IMMEX)."), - ("J3", "RETORNO Y EXPORTACION DE INSUMOS ELABORADOS O TRANSFORMADOS EN RECINTO FISCALIZADO."), + ( + "I1", + "IMPORTACION, EXPORTACION Y RETORNO DE MERCANCIAS ELABORADAS, TRANSFORMADAS O REPARADAS.", + ), + ( + "IN", + "IMPORTACION TEMPORAL DE BIENES QUE SERAN SUJETOS A TRANSFORMACION, ELABORACION O REPARACION (IMMEX).", + ), + ( + "J3", + "RETORNO Y EXPORTACION DE INSUMOS ELABORADOS O TRANSFORMADOS EN RECINTO FISCALIZADO.", + ), ("J4", "RETORNO DE MERCANCIAS EXTRANJERAS (RFE)."), ("K1", "DESISTIMIENTO DE REGIMEN Y RETORNO DE MERCANCIAS POR DEVOLUCION."), ("K2", "EXTRACCION DE DEPOSITO FISCAL POR DESISTIMIENTO O TRANSFERENCIAS (AGD)."), - ("K3", "EXTRACCION DE DEPOSITO FISCAL EN LOCAL AUTORIZADO PARA RETORNO O TRANSFERENCIA."), + ( + "K3", + "EXTRACCION DE DEPOSITO FISCAL EN LOCAL AUTORIZADO PARA RETORNO O TRANSFERENCIA.", + ), ("L1", "PEQUEÑA IMPORTACION DEFINITIVA."), ("M1", "INTRODUCCION Y EXPORTACION DE INSUMOS."), ("M2", "INTRODUCCION Y EXPORTACION DE MAQUINARIA Y EQUIPO."), ("M3", "INTRODUCCION DE MERCANCIAS (RFE)."), ("M4", "INTRODUCCION DE ACTIVO FIJO (RFE)."), ("M5", "INTRODUCCION DE MERCANCIA NACIONAL O NACIONALIZADA (RFE)."), - ("P1", "REEXPEDICION DE MERCANCIAS DE FRANJA FRONTERIZA O REGION FRONTERIZA AL INTERIOR DEL PAIS."), + ( + "P1", + "REEXPEDICION DE MERCANCIAS DE FRANJA FRONTERIZA O REGION FRONTERIZA AL INTERIOR DEL PAIS.", + ), ("R1", "RECTIFICACION DE PEDIMENTOS."), ("RT", "RETORNO DE MERCANCIAS (IMMEX)."), - ("S2", "IMPORTACION Y EXPORTACION DE MERCANCIAS PARA RETORNAR EN SU MISMO ESTADO (ARTICULO 86 DE LA LEY)."), + ( + "S2", + "IMPORTACION Y EXPORTACION DE MERCANCIAS PARA RETORNAR EN SU MISMO ESTADO (ARTICULO 86 DE LA LEY).", + ), ("T1", "IMPORTACION Y EXPORTACION POR EMPRESAS DE MENSAJERIA."), ("T3", "TRANSITO INTERNO."), ("T6", "TRANSITO INTERNACIONAL POR TERRITORIO EXTRANJERO."), ("T7", "TRANSITO INTERNACIONAL POR TERRITORIO NACIONAL."), ("T9", "TRANSITO INTERNACIONAL DE TRANSMIGRANTES."), - ("V1", "TRANSFERENCIAS DE MERCANCIAS (IMPORTACION TEMPORAL VIRTUAL; INTRODUCCION VIRTUAL A DEPOSITO FISCAL O A RECINTO FISCALIZADO ESTRATEGICO; RETORNO VIRTUAL; EXPORTACION VIRTUAL DE PROVEEDORES NACIONALES)."), - ("V2", "TRANSFERENCIAS DE MERCANCIAS IMPORTADAS CON CUENTA ADUANERA (EXPORTACION E IMPORTACION VIRTUAL)."), - ("V3", "EXTRACCION DE DEPOSITO FISCAL DE BIENES PARA SU RETORNO O EXPORTACION VIRTUAL (IA)."), - ("V4", "RETORNO VIRTUAL DERIVADO DE LA CONSTANCIA DE TRANSFERENCIA DE MERCANCIAS (IA)."), - ("V5", "TRANSFERENCIAS DE MERCANCIAS DE EMPRESAS CERTIFICADAS (RETORNO VIRTUAL PARA IMPORTACION DEFINITIVA)."), - ("V6", "TRANSFERENCIAS DE MERCANCIAS SUJETAS A CUPO (IMPORTACION DEFINITIVA Y RETORNO VIRTUAL)."), - ("V7", "TRANSFERENCIAS DEL SECTOR AZUCARERO (EXPORTACION VIRTUAL E IMPORTACION TEMPORAL VIRTUAL)."), - ("V8", "TRANSFERENCIA DE MERCANCIAS EN DEPOSITO FISCAL PARA LA EXPOSICION Y VENTA DE MERCANCIAS EXTRANJERAS, NACIONALES Y NACIONALIZADAS DE TIENDAS LIBRES DE IMPUESTOS (DUTY FREE)."), + ( + "V1", + "TRANSFERENCIAS DE MERCANCIAS (IMPORTACION TEMPORAL VIRTUAL; INTRODUCCION VIRTUAL A DEPOSITO FISCAL O A RECINTO FISCALIZADO ESTRATEGICO; RETORNO VIRTUAL; EXPORTACION VIRTUAL DE PROVEEDORES NACIONALES).", + ), + ( + "V2", + "TRANSFERENCIAS DE MERCANCIAS IMPORTADAS CON CUENTA ADUANERA (EXPORTACION E IMPORTACION VIRTUAL).", + ), + ( + "V3", + "EXTRACCION DE DEPOSITO FISCAL DE BIENES PARA SU RETORNO O EXPORTACION VIRTUAL (IA).", + ), + ( + "V4", + "RETORNO VIRTUAL DERIVADO DE LA CONSTANCIA DE TRANSFERENCIA DE MERCANCIAS (IA).", + ), + ( + "V5", + "TRANSFERENCIAS DE MERCANCIAS DE EMPRESAS CERTIFICADAS (RETORNO VIRTUAL PARA IMPORTACION DEFINITIVA).", + ), + ( + "V6", + "TRANSFERENCIAS DE MERCANCIAS SUJETAS A CUPO (IMPORTACION DEFINITIVA Y RETORNO VIRTUAL).", + ), + ( + "V7", + "TRANSFERENCIAS DEL SECTOR AZUCARERO (EXPORTACION VIRTUAL E IMPORTACION TEMPORAL VIRTUAL).", + ), + ( + "V8", + "TRANSFERENCIA DE MERCANCIAS EN DEPOSITO FISCAL PARA LA EXPOSICION Y VENTA DE MERCANCIAS EXTRANJERAS, NACIONALES Y NACIONALIZADAS DE TIENDAS LIBRES DE IMPUESTOS (DUTY FREE).", + ), ("V9", "TRANSFERENCIAS DE MERCANCIAS POR DONACION"), ("VD", "VIRTUALES DIVERSOS."), - ("VF", "IMPORTACION DEFINITIVA DE VEHICULOS USADOS A LA FRANJA O REGION FRONTERIZA NORTE."), - ("VU", "IMPORTACION DEFINITIVA DE VEHICULOS USADOS.") -] \ No newline at end of file + ( + "VF", + "IMPORTACION DEFINITIVA DE VEHICULOS USADOS A LA FRANJA O REGION FRONTERIZA NORTE.", + ), + ("VU", "IMPORTACION DEFINITIVA DE VEHICULOS USADOS."), +] diff --git a/backend/api/v1/modules/public/reference_data/pedimento_codes/test_pedimento_codes.py b/backend/api/v1/modules/public/reference_data/pedimento_codes/test_pedimento_codes.py index 92f9ebc4..28914df1 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_codes/test_pedimento_codes.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_codes/test_pedimento_codes.py @@ -7,6 +7,7 @@ app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_pedimento_codes(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,20 +17,28 @@ def test_list_pedimento_codes(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_pedimento_code_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/pedimento-codes/invalid_code", headers=headers) assert response.status_code == 404 + def test_create_pedimento_code_forbidden(): - response = client.post("/pedimento-codes/", json={"code": "TST", "description": "Test"}) + response = client.post( + "/pedimento-codes/", json={"code": "TST", "description": "Test"} + ) assert response.status_code in (403, 405, 404) + def test_update_pedimento_code_forbidden(): - response = client.put("/pedimento-codes/TST", json={"code": "TST", "description": "Test"}) + response = client.put( + "/pedimento-codes/TST", json={"code": "TST", "description": "Test"} + ) assert response.status_code in (403, 405, 404) + def test_delete_pedimento_code_forbidden(): response = client.delete("/pedimento-codes/TST") assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/pedimento_regimens/dto.py b/backend/api/v1/modules/public/reference_data/pedimento_regimens/dto.py index 3a0673d8..d5ae9e01 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_regimens/dto.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_regimens/dto.py @@ -2,6 +2,7 @@ from pydantic import BaseModel, Field from pydantic import ConfigDict from typing import List + class RegimenPedimentoDTO(BaseModel): code: str = Field(..., min_length=1, max_length=3) description: str diff --git a/backend/api/v1/modules/public/reference_data/pedimento_regimens/models.py b/backend/api/v1/modules/public/reference_data/pedimento_regimens/models.py index bcd60665..4c9e7f95 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_regimens/models.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_regimens/models.py @@ -6,22 +6,25 @@ from core.database import Base if TYPE_CHECKING: from ..code_pedimento_regimens.models import CodePedimentoRegimen + class RegimenPedimento(Base): - __tablename__ = "pedimento_regimens" #GRegimenPed + __tablename__ = "pedimento_regimens" # GRegimenPed __table_args__ = ( PrimaryKeyConstraint("code", name="pedimento_regimens_pkey"), - {"schema": "public"} + {"schema": "public"}, ) - code: Mapped[str] = mapped_column(String(3), nullable=False) # código tipo "01", "31" - description: Mapped[str] = mapped_column(String(100), nullable=False) # nombre legal en español + code: Mapped[str] = mapped_column( + String(3), nullable=False + ) # código tipo "01", "31" + description: Mapped[str] = mapped_column( + String(100), nullable=False + ) # nombre legal en español # Relación con Claves de Pedimento - #GClavePedRegimen - claves_pedimento: Mapped[List['CodePedimentoRegimen']] = relationship( - "CodePedimentoRegimen", - uselist=True, - back_populates="regimen" + # GClavePedRegimen + claves_pedimento: Mapped[List["CodePedimentoRegimen"]] = relationship( + "CodePedimentoRegimen", uselist=True, back_populates="regimen" ) def __repr__(self): diff --git a/backend/api/v1/modules/public/reference_data/pedimento_regimens/routes.py b/backend/api/v1/modules/public/reference_data/pedimento_regimens/routes.py index 593d7a53..1a3b1bcc 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_regimens/routes.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_regimens/routes.py @@ -1,4 +1,3 @@ - from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db @@ -16,7 +15,7 @@ def list_pedimento_regimens( page: int = Query(1, ge=1, description="Número de página"), page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): skip = (page - 1) * page_size query = db.query(RegimenPedimento) @@ -26,21 +25,27 @@ def list_pedimento_regimens( "items": [RegimenPedimentoDTO.model_validate(obj) for obj in items], "total": total, "page": page, - "page_size": page_size + "page_size": page_size, } + @router.get("/{key}", response_model=RegimenPedimentoDTO) -def get_pedimento_regimen(key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): +def get_pedimento_regimen( + key: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): obj = db.query(RegimenPedimento).filter(RegimenPedimento.code == key).first() if not obj: raise HTTPException(status_code=404, detail="Not found") return RegimenPedimentoDTO.model_validate(obj) + @router.post("/", response_model=RegimenPedimentoDTO, status_code=201) def create_pedimento_regimen( data: RegimenPedimentoDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = RegimenPedimento(**data.model_dump()) db.add(obj) @@ -48,12 +53,13 @@ def create_pedimento_regimen( db.refresh(obj) return RegimenPedimentoDTO.model_validate(obj) + @router.put("/{key}", response_model=RegimenPedimentoDTO) def update_pedimento_regimen( key: str, data: RegimenPedimentoDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(RegimenPedimento).filter(RegimenPedimento.code == key).first() if not obj: @@ -64,11 +70,12 @@ def update_pedimento_regimen( db.refresh(obj) return RegimenPedimentoDTO.model_validate(obj) + @router.delete("/{key}", status_code=204) def delete_pedimento_regimen( key: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(RegimenPedimento).filter(RegimenPedimento.code == key).first() if not obj: diff --git a/backend/api/v1/modules/public/reference_data/pedimento_regimens/seed.py b/backend/api/v1/modules/public/reference_data/pedimento_regimens/seed.py index f154928e..386ea1d1 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_regimens/seed.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_regimens/seed.py @@ -1,12 +1,18 @@ seed = [ - ("DFI", "DEPOSITO FISCAL."), - ("ETE", "TEMPORALES DE EXPORTACION PARA ELABORACION, TRANSFORMACION O REPARACION."), - ("ETR", "TEMPORALES DE EXPORTACION PARA RETORNAR AL PAIS EN EL MISMO ESTADO."), - ("EXD", "DEFINITIVO DE EXPORTACIÓN."), - ("IMD", "DEFINITIVO DE IMPORTACIÓN."), - ("ITE", "TEMPORALES DE IMPORTACION PARA ELABORACION, TRANSFORMACION O REPARACION PARA EMPRESAS CON PROGRAMA I"), - ("ITR", "TEMPORALES DE IMPORTACION PARA RETORNAR AL EXTRANJERO EN EL MISMO ESTADO."), - ("RFE", "ELABORACION, TRANSFORMACION O REPARACION EN RECINTO FISCALIZADO."), - ("RFS", "RECINTO FISCALIZADO ESTRATEGICO."), - ("TRA", "TRANSITOS.") -] \ No newline at end of file + ("DFI", "DEPOSITO FISCAL."), + ("ETE", "TEMPORALES DE EXPORTACION PARA ELABORACION, TRANSFORMACION O REPARACION."), + ("ETR", "TEMPORALES DE EXPORTACION PARA RETORNAR AL PAIS EN EL MISMO ESTADO."), + ("EXD", "DEFINITIVO DE EXPORTACIÓN."), + ("IMD", "DEFINITIVO DE IMPORTACIÓN."), + ( + "ITE", + "TEMPORALES DE IMPORTACION PARA ELABORACION, TRANSFORMACION O REPARACION PARA EMPRESAS CON PROGRAMA I", + ), + ( + "ITR", + "TEMPORALES DE IMPORTACION PARA RETORNAR AL EXTRANJERO EN EL MISMO ESTADO.", + ), + ("RFE", "ELABORACION, TRANSFORMACION O REPARACION EN RECINTO FISCALIZADO."), + ("RFS", "RECINTO FISCALIZADO ESTRATEGICO."), + ("TRA", "TRANSITOS."), +] diff --git a/backend/api/v1/modules/public/reference_data/pedimento_regimens/test_pedimento_regimens.py b/backend/api/v1/modules/public/reference_data/pedimento_regimens/test_pedimento_regimens.py index b1ceaf54..ac9dfb9a 100644 --- a/backend/api/v1/modules/public/reference_data/pedimento_regimens/test_pedimento_regimens.py +++ b/backend/api/v1/modules/public/reference_data/pedimento_regimens/test_pedimento_regimens.py @@ -7,6 +7,7 @@ app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_pedimento_regimens(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,20 +17,28 @@ def test_list_pedimento_regimens(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_pedimento_regimen_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/pedimento-regimens/invalid_key", headers=headers) assert response.status_code == 404 + def test_create_pedimento_regimen_forbidden(): - response = client.post("/pedimento-regimens/", json={"code": "TST", "description": "Test"}) + response = client.post( + "/pedimento-regimens/", json={"code": "TST", "description": "Test"} + ) assert response.status_code in (403, 405, 404) + def test_update_pedimento_regimen_forbidden(): - response = client.put("/pedimento-regimens/TST", json={"code": "TST", "description": "Test"}) + response = client.put( + "/pedimento-regimens/TST", json={"code": "TST", "description": "Test"} + ) assert response.status_code in (403, 405, 404) + def test_delete_pedimento_regimen_forbidden(): response = client.delete("/pedimento-regimens/TST") assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/router.py b/backend/api/v1/modules/public/reference_data/router.py index e8a350a4..22208602 100644 --- a/backend/api/v1/modules/public/reference_data/router.py +++ b/backend/api/v1/modules/public/reference_data/router.py @@ -2,6 +2,7 @@ Router principal de API v1 Agrega todos los módulos de la aplicación """ + from fastapi import APIRouter from .pedimento_codes.routes import router as pedimento_codes_router @@ -26,20 +27,86 @@ from .incoterms.routes import router as incoterms_router router = APIRouter() # Registrar módulos -router.include_router(pedimento_codes_router, prefix="/refrence_data", tags=["public / refrence_data / pedimento_codes"]) -router.include_router(payment_methods_router, prefix="/refrence_data", tags=["public / refrence_data / payment_methods"]) -router.include_router(containers_router, prefix="/refrence_data", tags=["public / refrence_data / containers"]) -router.include_router(countries_router, prefix="/refrence_data", tags=["public / refrence_data / countries"]) -router.include_router(material_types_router, prefix="/refrence_data", tags=["public / refrence_data / material_types"]) -router.include_router(currency_types_router, prefix="/refrence_data", tags=["public / refrence_data / currency_types"]) -router.include_router(states_router, prefix="/refrence_data", tags=["public / refrence_data / states"]) -router.include_router(transport_types_router, prefix="/refrence_data", tags=["public / refrence_data / transport_types"]) -router.include_router(customs_warehouses_router, prefix="/refrence_data", tags=["public / refrence_data / customs_warehouses"]) -router.include_router(valuation_methods_router, prefix="/refrence_data", tags=["public / refrence_data / valuation_methods"]) -router.include_router(sectors_router, prefix="/refrence_data", tags=["public / public / refrence_data / sectors"]) -router.include_router(transport_modes_router, prefix="/refrence_data", tags=["public / refrence_data / transport_modes"]) -router.include_router(customs_sections_router, prefix="/refrence_data", tags=["public / refrence_data / customs_sections"]) -router.include_router(invoice_types_router, prefix="/refrence_data", tags=["public / refrence_data / invoice_types"]) -router.include_router(code_pedimento_regimens_router, prefix="/refrence_data", tags=["public / refrence_data / code_pedimento_regimens"]) -router.include_router(pedimento_regimens_router, prefix="/refrence_data", tags=["public / refrence_data / pedimento_regimens"]) -router.include_router(incoterms_router, prefix="/refrence_data", tags=["public / refrence_data / incoterms"]) \ No newline at end of file +router.include_router( + pedimento_codes_router, + prefix="/refrence_data", + tags=["public / refrence_data / pedimento_codes"], +) +router.include_router( + payment_methods_router, + prefix="/refrence_data", + tags=["public / refrence_data / payment_methods"], +) +router.include_router( + containers_router, + prefix="/refrence_data", + tags=["public / refrence_data / containers"], +) +router.include_router( + countries_router, + prefix="/refrence_data", + tags=["public / refrence_data / countries"], +) +router.include_router( + material_types_router, + prefix="/refrence_data", + tags=["public / refrence_data / material_types"], +) +router.include_router( + currency_types_router, + prefix="/refrence_data", + tags=["public / refrence_data / currency_types"], +) +router.include_router( + states_router, prefix="/refrence_data", tags=["public / refrence_data / states"] +) +router.include_router( + transport_types_router, + prefix="/refrence_data", + tags=["public / refrence_data / transport_types"], +) +router.include_router( + customs_warehouses_router, + prefix="/refrence_data", + tags=["public / refrence_data / customs_warehouses"], +) +router.include_router( + valuation_methods_router, + prefix="/refrence_data", + tags=["public / refrence_data / valuation_methods"], +) +router.include_router( + sectors_router, + prefix="/refrence_data", + tags=["public / public / refrence_data / sectors"], +) +router.include_router( + transport_modes_router, + prefix="/refrence_data", + tags=["public / refrence_data / transport_modes"], +) +router.include_router( + customs_sections_router, + prefix="/refrence_data", + tags=["public / refrence_data / customs_sections"], +) +router.include_router( + invoice_types_router, + prefix="/refrence_data", + tags=["public / refrence_data / invoice_types"], +) +router.include_router( + code_pedimento_regimens_router, + prefix="/refrence_data", + tags=["public / refrence_data / code_pedimento_regimens"], +) +router.include_router( + pedimento_regimens_router, + prefix="/refrence_data", + tags=["public / refrence_data / pedimento_regimens"], +) +router.include_router( + incoterms_router, + prefix="/refrence_data", + tags=["public / refrence_data / incoterms"], +) diff --git a/backend/api/v1/modules/public/reference_data/sectors/dto.py b/backend/api/v1/modules/public/reference_data/sectors/dto.py index 2f92ae9d..770ed965 100644 --- a/backend/api/v1/modules/public/reference_data/sectors/dto.py +++ b/backend/api/v1/modules/public/reference_data/sectors/dto.py @@ -1,10 +1,10 @@ from pydantic import BaseModel, Field from pydantic import ConfigDict + class SectorDTO(BaseModel): key: str = Field(..., min_length=1, max_length=8) description: str authorized: int model_config = ConfigDict(from_attributes=True) - diff --git a/backend/api/v1/modules/public/reference_data/sectors/models.py b/backend/api/v1/modules/public/reference_data/sectors/models.py index 6a3666f0..9e292ee3 100644 --- a/backend/api/v1/modules/public/reference_data/sectors/models.py +++ b/backend/api/v1/modules/public/reference_data/sectors/models.py @@ -2,16 +2,21 @@ from sqlalchemy import String, SmallInteger, PrimaryKeyConstraint from sqlalchemy.orm import mapped_column, Mapped from core.database import Base + class Sector(Base): - __tablename__ = "sectors" #GSectores + __tablename__ = "sectors" # GSectores __table_args__ = ( PrimaryKeyConstraint("key", name="sectors_pkey"), - {"schema": "public"} # opcional + {"schema": "public"}, # opcional ) - key: Mapped[str] = mapped_column(String(8), nullable=False) # clave del sector - description: Mapped[str] = mapped_column(String(150), nullable=False) # descripción oficial (en español) - authorized: Mapped[SmallInteger] = mapped_column(SmallInteger) # 1 = autorizado, 0 = no autorizado + key: Mapped[str] = mapped_column(String(8), nullable=False) # clave del sector + description: Mapped[str] = mapped_column( + String(150), nullable=False + ) # descripción oficial (en español) + authorized: Mapped[SmallInteger] = mapped_column( + SmallInteger + ) # 1 = autorizado, 0 = no autorizado def __repr__(self): return f"" diff --git a/backend/api/v1/modules/public/reference_data/sectors/routes.py b/backend/api/v1/modules/public/reference_data/sectors/routes.py index 9ba896c6..3c9839f6 100644 --- a/backend/api/v1/modules/public/reference_data/sectors/routes.py +++ b/backend/api/v1/modules/public/reference_data/sectors/routes.py @@ -1,4 +1,3 @@ - from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db @@ -16,7 +15,7 @@ def list_sectors( page: int = Query(1, ge=1, description="Número de página"), page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): skip = (page - 1) * page_size query = db.query(Sector) @@ -26,21 +25,27 @@ def list_sectors( "items": [SectorDTO.model_validate(obj) for obj in items], "total": total, "page": page, - "page_size": page_size + "page_size": page_size, } + @router.get("/{key}", response_model=SectorDTO) -def get_sector(key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): +def get_sector( + key: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): obj = db.query(Sector).filter(Sector.key == key).first() if not obj: raise HTTPException(status_code=404, detail="Not found") return obj + @router.post("/", response_model=SectorDTO, status_code=201) def create_sector( data: SectorDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = Sector(**data.dict()) db.add(obj) @@ -48,12 +53,13 @@ def create_sector( db.refresh(obj) return obj + @router.put("/{key}", response_model=SectorDTO) def update_sector( key: str, data: SectorDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(Sector).filter(Sector.key == key).first() if not obj: @@ -64,11 +70,12 @@ def update_sector( db.refresh(obj) return obj + @router.delete("/{key}", status_code=204) def delete_sector( key: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(Sector).filter(Sector.key == key).first() if not obj: diff --git a/backend/api/v1/modules/public/reference_data/sectors/seed.py b/backend/api/v1/modules/public/reference_data/sectors/seed.py index d1165734..9eb39401 100644 --- a/backend/api/v1/modules/public/reference_data/sectors/seed.py +++ b/backend/api/v1/modules/public/reference_data/sectors/seed.py @@ -1,8 +1,16 @@ seed = [ ("I", "INDUSTRIA ELECTRICA", "0"), ("II", "INDUSTRIA ELECTRONICA", "0"), - ("IIa", "PARA LOS BIENES A QUE SE REFIERE LA FRACCION II, INCISO a) o b), DE ARTICULO 4to DE ESTE DECRETO.", "0"), - ("IIb", "PARA LOS BIENES A QUE SE REFIERE LA FRACCION II, INCISO b), DE ARTICULO 4to DE ESTE DECRETO.", "0"), + ( + "IIa", + "PARA LOS BIENES A QUE SE REFIERE LA FRACCION II, INCISO a) o b), DE ARTICULO 4to DE ESTE DECRETO.", + "0", + ), + ( + "IIb", + "PARA LOS BIENES A QUE SE REFIERE LA FRACCION II, INCISO b), DE ARTICULO 4to DE ESTE DECRETO.", + "0", + ), ("III", "INDUSTRIA DEL MUEBLE", "0"), ("IV", "INDUSTRIA DEL JUGUETE, JUEGOS DE RECREO Y ARTICULOS DEPORTIVOS", "0"), ("IX", "INDUSTRIA DE MAQUINARIA AGRICOLA", "0"), @@ -18,9 +26,21 @@ seed = [ ("XIX", "INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES", "0"), ("XIXa", "INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES", "0"), ("XIXb", "INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES", "0"), - ("XV", "INDUSTRIA DEL TRANSPORTE, EXCEPTO EL SECTOR DE LA INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES", "0"), - ("XVa", "INDUSTRIA DEL TRANSPORTE, EXCEPTO EL SECTOR DE LA INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES.", "0"), - ("XVb", "INDUSTRIA DEL TRANSPORTE, EXCEPTO EL SECTOR DE LA INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES.", "0"), + ( + "XV", + "INDUSTRIA DEL TRANSPORTE, EXCEPTO EL SECTOR DE LA INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES", + "0", + ), + ( + "XVa", + "INDUSTRIA DEL TRANSPORTE, EXCEPTO EL SECTOR DE LA INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES.", + "0", + ), + ( + "XVb", + "INDUSTRIA DEL TRANSPORTE, EXCEPTO EL SECTOR DE LA INDUSTRIA AUTOMOTRIZ Y DE AUTOPARTES.", + "0", + ), ("XVI", "INDUSTRIA DEL PAPEL Y CARTON", "0"), ("XVII", "INDUSTRIA DE LA MADERA", "0"), ("XVIII", "INDUSTRIA DEL CUERO Y PIELES", "0"), @@ -32,4 +52,4 @@ seed = [ ("XXe", "INDUSTRIA TEXTIL Y DE LA CONFECCION", "0"), ("XXI", "INDUSTRIA DE CHOCOLATES, DULCES Y SIMILARES", "0"), ("XXII", "INDUSTRIA DEL CAFE", "0"), -] \ No newline at end of file +] diff --git a/backend/api/v1/modules/public/reference_data/sectors/test_sectors.py b/backend/api/v1/modules/public/reference_data/sectors/test_sectors.py index 2a59d667..c81e205c 100644 --- a/backend/api/v1/modules/public/reference_data/sectors/test_sectors.py +++ b/backend/api/v1/modules/public/reference_data/sectors/test_sectors.py @@ -7,6 +7,7 @@ app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_sectors(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,20 +17,24 @@ def test_list_sectors(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_sector_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/sectors/invalid_key", headers=headers) assert response.status_code == 404 + def test_create_sector_forbidden(): response = client.post("/sectors/", json={"key": "TST", "description": "Test"}) assert response.status_code in (403, 405, 404) + def test_update_sector_forbidden(): response = client.put("/sectors/TST", json={"key": "TST", "description": "Test"}) assert response.status_code in (403, 405, 404) + def test_delete_sector_forbidden(): response = client.delete("/sectors/TST") assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/states/dto.py b/backend/api/v1/modules/public/reference_data/states/dto.py index b4a8e219..b54bd4b5 100644 --- a/backend/api/v1/modules/public/reference_data/states/dto.py +++ b/backend/api/v1/modules/public/reference_data/states/dto.py @@ -2,6 +2,7 @@ from pydantic import BaseModel, Field from pydantic import ConfigDict from typing import Optional + class StateDTO(BaseModel): m3_key: str = Field(..., min_length=1, max_length=3) description: str @@ -9,4 +10,3 @@ class StateDTO(BaseModel): ame_key: Optional[str] = None model_config = ConfigDict(from_attributes=True) - diff --git a/backend/api/v1/modules/public/reference_data/states/models.py b/backend/api/v1/modules/public/reference_data/states/models.py index 64729fcb..c1800828 100644 --- a/backend/api/v1/modules/public/reference_data/states/models.py +++ b/backend/api/v1/modules/public/reference_data/states/models.py @@ -3,15 +3,18 @@ from sqlalchemy import String, PrimaryKeyConstraint from sqlalchemy.orm import mapped_column, Mapped from core.database import Base + class State(Base): - __tablename__ = "states" #GEstados + __tablename__ = "states" # GEstados __table_args__ = ( - PrimaryKeyConstraint('m3_key', 'description', name='states_pkey'), - {"schema": "public"} + PrimaryKeyConstraint("m3_key", "description", name="states_pkey"), + {"schema": "public"}, ) m3_key: Mapped[str] = mapped_column(String(3), nullable=False) - description: Mapped[str] = mapped_column(String(50), nullable=False) # valor legal en español + description: Mapped[str] = mapped_column( + String(50), nullable=False + ) # valor legal en español mex_key: Mapped[Optional[str]] = mapped_column(String(3)) ame_key: Mapped[Optional[str]] = mapped_column(String(2)) diff --git a/backend/api/v1/modules/public/reference_data/states/routes.py b/backend/api/v1/modules/public/reference_data/states/routes.py index e9cf3cf4..f5859ea4 100644 --- a/backend/api/v1/modules/public/reference_data/states/routes.py +++ b/backend/api/v1/modules/public/reference_data/states/routes.py @@ -1,4 +1,3 @@ - from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db @@ -11,13 +10,12 @@ from typing import Any, Dict router = APIRouter(prefix="/states") - @router.get("/", response_model=Dict[str, Any]) async def list_states( page: int = Query(1, ge=1, description="Número de página"), page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): skip = (page - 1) * page_size query = db.query(State) @@ -27,23 +25,27 @@ async def list_states( "items": [StateDTO.model_validate(obj) for obj in items], "total": total, "page": page, - "page_size": page_size + "page_size": page_size, } - + @router.get("/{m3_key}", response_model=StateDTO) -async def get_state(m3_key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): +async def get_state( + m3_key: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): obj = db.query(State).filter(State.m3_key == m3_key).first() if not obj: raise HTTPException(status_code=404, detail="Not found") return obj - + @router.post("/", response_model=StateDTO, status_code=201) async def create_state( data: StateDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = State(**data.dict()) @@ -52,13 +54,13 @@ async def create_state( db.refresh(obj) return obj - + @router.put("/{m3_key}", response_model=StateDTO) async def update_state( m3_key: str, data: StateDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(State).filter(State.m3_key == m3_key).first() @@ -70,12 +72,12 @@ async def update_state( db.refresh(obj) return obj - + @router.delete("/{m3_key}", status_code=204) async def delete_state( m3_key: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(State).filter(State.m3_key == m3_key).first() diff --git a/backend/api/v1/modules/public/reference_data/states/seed.py b/backend/api/v1/modules/public/reference_data/states/seed.py index c7513c89..a8eefa57 100644 --- a/backend/api/v1/modules/public/reference_data/states/seed.py +++ b/backend/api/v1/modules/public/reference_data/states/seed.py @@ -1,3 +1 @@ -seed = [ - -] \ No newline at end of file +seed = [] diff --git a/backend/api/v1/modules/public/reference_data/states/test_states.py b/backend/api/v1/modules/public/reference_data/states/test_states.py index 27003749..3719c88b 100644 --- a/backend/api/v1/modules/public/reference_data/states/test_states.py +++ b/backend/api/v1/modules/public/reference_data/states/test_states.py @@ -7,6 +7,7 @@ app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_states(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,20 +17,24 @@ def test_list_states(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_state_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/states/invalid_key", headers=headers) assert response.status_code == 404 + def test_create_state_forbidden(): response = client.post("/states/", json={"key": "TST", "description": "Test"}) assert response.status_code in (403, 405, 404) + def test_update_state_forbidden(): response = client.put("/states/TST", json={"key": "TST", "description": "Test"}) assert response.status_code in (403, 405, 404) + def test_delete_state_forbidden(): response = client.delete("/states/TST") assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/transport_modes/dto.py b/backend/api/v1/modules/public/reference_data/transport_modes/dto.py index 5ba8b9c4..71f8b688 100644 --- a/backend/api/v1/modules/public/reference_data/transport_modes/dto.py +++ b/backend/api/v1/modules/public/reference_data/transport_modes/dto.py @@ -1,9 +1,9 @@ from pydantic import BaseModel, Field from pydantic import ConfigDict + class TransportModeDTO(BaseModel): key: str = Field(..., min_length=1, max_length=3) name: str model_config = ConfigDict(from_attributes=True) - diff --git a/backend/api/v1/modules/public/reference_data/transport_modes/models.py b/backend/api/v1/modules/public/reference_data/transport_modes/models.py index 0158224a..daccbf2b 100644 --- a/backend/api/v1/modules/public/reference_data/transport_modes/models.py +++ b/backend/api/v1/modules/public/reference_data/transport_modes/models.py @@ -2,14 +2,15 @@ from sqlalchemy import String, PrimaryKeyConstraint from sqlalchemy.orm import mapped_column, Mapped from core.database import Base + class TransportMode(Base): - __tablename__ = "transport_modes" #GModTransporte + __tablename__ = "transport_modes" # GModTransporte __table_args__ = ( PrimaryKeyConstraint("key", name="transport_modes_pkey"), - {"schema": "public"} # opcional + {"schema": "public"}, # opcional ) - key: Mapped[str] = mapped_column(String(3), nullable=False) + key: Mapped[str] = mapped_column(String(3), nullable=False) name: Mapped[str] = mapped_column(String(30), nullable=False) def __repr__(self): diff --git a/backend/api/v1/modules/public/reference_data/transport_modes/routes.py b/backend/api/v1/modules/public/reference_data/transport_modes/routes.py index 24d0406e..efba117e 100644 --- a/backend/api/v1/modules/public/reference_data/transport_modes/routes.py +++ b/backend/api/v1/modules/public/reference_data/transport_modes/routes.py @@ -1,4 +1,3 @@ - from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db @@ -11,12 +10,11 @@ from typing import Any, Dict router = APIRouter(prefix="/transport-modes") - @router.get("/", response_model=Dict[str, Any]) async def list_transport_modes( page: int = Query(1, ge=1, description="Número de página"), page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), - db: Session = Depends(get_core_db) + db: Session = Depends(get_core_db), ): skip = (page - 1) * page_size query = db.query(TransportMode) @@ -26,10 +24,10 @@ async def list_transport_modes( "items": [TransportModeDTO.model_validate(obj) for obj in items], "total": total, "page": page, - "page_size": page_size + "page_size": page_size, } - + @router.get("/{key}", response_model=TransportModeDTO) async def get_transport_mode(key: str, db: Session = Depends(get_core_db)): obj = db.query(TransportMode).filter(TransportMode.key == key).first() @@ -37,12 +35,12 @@ async def get_transport_mode(key: str, db: Session = Depends(get_core_db)): raise HTTPException(status_code=404, detail="Not found") return obj - + @router.post("/", response_model=TransportModeDTO, status_code=201) async def create_transport_mode( data: TransportModeDTO, db: Session = Depends(get_core_db), - user=Depends(get_current_user) + user=Depends(get_current_user), ): obj = TransportMode(**data.dict()) db.add(obj) @@ -50,13 +48,13 @@ async def create_transport_mode( db.refresh(obj) return obj - + @router.put("/{key}", response_model=TransportModeDTO) async def update_transport_mode( key: str, data: TransportModeDTO, db: Session = Depends(get_core_db), - user=Depends(get_current_user) + user=Depends(get_current_user), ): obj = db.query(TransportMode).filter(TransportMode.key == key).first() if not obj: @@ -67,12 +65,10 @@ async def update_transport_mode( db.refresh(obj) return obj - + @router.delete("/{key}", status_code=204) async def delete_transport_mode( - key: str, - db: Session = Depends(get_core_db), - user=Depends(get_current_user) + key: str, db: Session = Depends(get_core_db), user=Depends(get_current_user) ): obj = db.query(TransportMode).filter(TransportMode.key == key).first() if not obj: diff --git a/backend/api/v1/modules/public/reference_data/transport_modes/seed.py b/backend/api/v1/modules/public/reference_data/transport_modes/seed.py index 55b396bd..6790d279 100644 --- a/backend/api/v1/modules/public/reference_data/transport_modes/seed.py +++ b/backend/api/v1/modules/public/reference_data/transport_modes/seed.py @@ -9,4 +9,4 @@ seed = [ ("40", "AIR"), ("41", "AIR CONTAINER"), ("50", "MAIL"), -] \ No newline at end of file +] diff --git a/backend/api/v1/modules/public/reference_data/transport_modes/test_transport_modes.py b/backend/api/v1/modules/public/reference_data/transport_modes/test_transport_modes.py index 34bdb2ca..4ef760dc 100644 --- a/backend/api/v1/modules/public/reference_data/transport_modes/test_transport_modes.py +++ b/backend/api/v1/modules/public/reference_data/transport_modes/test_transport_modes.py @@ -7,6 +7,7 @@ app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_transport_modes(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,20 +17,28 @@ def test_list_transport_modes(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_transport_mode_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/transport-modes/invalid_key", headers=headers) assert response.status_code == 404 + def test_create_transport_mode_forbidden(): - response = client.post("/transport-modes/", json={"key": "TST", "description": "Test"}) + response = client.post( + "/transport-modes/", json={"key": "TST", "description": "Test"} + ) assert response.status_code in (403, 405, 404) + def test_update_transport_mode_forbidden(): - response = client.put("/transport-modes/TST", json={"key": "TST", "description": "Test"}) + response = client.put( + "/transport-modes/TST", json={"key": "TST", "description": "Test"} + ) assert response.status_code in (403, 405, 404) + def test_delete_transport_mode_forbidden(): response = client.delete("/transport-modes/TST") assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/transport_types/dto.py b/backend/api/v1/modules/public/reference_data/transport_types/dto.py index 573fc0b3..f9367e0c 100644 --- a/backend/api/v1/modules/public/reference_data/transport_types/dto.py +++ b/backend/api/v1/modules/public/reference_data/transport_types/dto.py @@ -1,9 +1,9 @@ from pydantic import BaseModel, Field from pydantic import ConfigDict + class TransportTypeDTO(BaseModel): transport_code: str = Field(..., min_length=1, max_length=2) description: str model_config = ConfigDict(from_attributes=True) - diff --git a/backend/api/v1/modules/public/reference_data/transport_types/models.py b/backend/api/v1/modules/public/reference_data/transport_types/models.py index 97072835..fff49b2d 100644 --- a/backend/api/v1/modules/public/reference_data/transport_types/models.py +++ b/backend/api/v1/modules/public/reference_data/transport_types/models.py @@ -4,14 +4,18 @@ from core.database import Base class TransportType(Base): - __tablename__ = "transport_types" #GTiposTransporte + __tablename__ = "transport_types" # GTiposTransporte __table_args__ = ( PrimaryKeyConstraint("transport_code", name="transport_types_pkey"), - {"schema": "public"} + {"schema": "public"}, ) - transport_code: Mapped[str] = mapped_column(String(2), nullable=False) # código SAT o interno - description: Mapped[str] = mapped_column(String(100), nullable=False) # descripción del medio de transporte + transport_code: Mapped[str] = mapped_column( + String(2), nullable=False + ) # código SAT o interno + description: Mapped[str] = mapped_column( + String(100), nullable=False + ) # descripción del medio de transporte def __repr__(self): return f"" diff --git a/backend/api/v1/modules/public/reference_data/transport_types/routes.py b/backend/api/v1/modules/public/reference_data/transport_types/routes.py index 737416a7..a9a5da23 100644 --- a/backend/api/v1/modules/public/reference_data/transport_types/routes.py +++ b/backend/api/v1/modules/public/reference_data/transport_types/routes.py @@ -1,4 +1,3 @@ - from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db @@ -15,7 +14,7 @@ router = APIRouter(prefix="/transport-types") def list_transport_types( page: int = Query(1, ge=1, description="Número de página"), page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), - db: Session = Depends(get_core_db) + db: Session = Depends(get_core_db), ): skip = (page - 1) * page_size query = db.query(TransportType) @@ -25,21 +24,27 @@ def list_transport_types( "items": [TransportTypeDTO.model_validate(obj) for obj in items], "total": total, "page": page, - "page_size": page_size + "page_size": page_size, } + @router.get("/{transport_code}", response_model=TransportTypeDTO) def get_transport_type(transport_code: str, db: Session = Depends(get_core_db)): - obj = db.query(TransportType).filter(TransportType.transport_code == transport_code).first() + obj = ( + db.query(TransportType) + .filter(TransportType.transport_code == transport_code) + .first() + ) if not obj: raise HTTPException(status_code=404, detail="Not found") return obj + @router.post("/", response_model=TransportTypeDTO, status_code=201) def create_transport_type( data: TransportTypeDTO, db: Session = Depends(get_core_db), - user=Depends(get_current_user) + user=Depends(get_current_user), ): obj = TransportType(**data.dict()) db.add(obj) @@ -47,14 +52,19 @@ def create_transport_type( db.refresh(obj) return obj + @router.put("/{transport_code}", response_model=TransportTypeDTO) def update_transport_type( transport_code: str, data: TransportTypeDTO, db: Session = Depends(get_core_db), - user=Depends(get_current_user) + user=Depends(get_current_user), ): - obj = db.query(TransportType).filter(TransportType.transport_code == transport_code).first() + obj = ( + db.query(TransportType) + .filter(TransportType.transport_code == transport_code) + .first() + ) if not obj: raise HTTPException(status_code=404, detail="Not found") for field, value in data.dict().items(): @@ -63,13 +73,18 @@ def update_transport_type( db.refresh(obj) return obj + @router.delete("/{transport_code}", status_code=204) def delete_transport_type( transport_code: str, db: Session = Depends(get_core_db), - user=Depends(get_current_user) + user=Depends(get_current_user), ): - obj = db.query(TransportType).filter(TransportType.transport_code == transport_code).first() + obj = ( + db.query(TransportType) + .filter(TransportType.transport_code == transport_code) + .first() + ) if not obj: raise HTTPException(status_code=404, detail="Not found") db.delete(obj) diff --git a/backend/api/v1/modules/public/reference_data/transport_types/seed.py b/backend/api/v1/modules/public/reference_data/transport_types/seed.py index d59b3d33..5868b5c6 100644 --- a/backend/api/v1/modules/public/reference_data/transport_types/seed.py +++ b/backend/api/v1/modules/public/reference_data/transport_types/seed.py @@ -19,4 +19,4 @@ seed = [ ("RV", "Recreation Vehicle (RV)"), ("TR", "Semi Tracker"), ("TV", "Van"), -] \ No newline at end of file +] diff --git a/backend/api/v1/modules/public/reference_data/transport_types/test_transport_types.py b/backend/api/v1/modules/public/reference_data/transport_types/test_transport_types.py index 11256a89..850dc680 100644 --- a/backend/api/v1/modules/public/reference_data/transport_types/test_transport_types.py +++ b/backend/api/v1/modules/public/reference_data/transport_types/test_transport_types.py @@ -7,6 +7,7 @@ app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_transport_types(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,20 +17,28 @@ def test_list_transport_types(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_transport_type_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/transport-types/invalid_code", headers=headers) assert response.status_code == 404 + def test_create_transport_type_forbidden(): - response = client.post("/transport-types/", json={"transport_code": "TST", "description": "Test"}) + response = client.post( + "/transport-types/", json={"transport_code": "TST", "description": "Test"} + ) assert response.status_code in (403, 405, 404) + def test_update_transport_type_forbidden(): - response = client.put("/transport-types/TST", json={"transport_code": "TST", "description": "Test"}) + response = client.put( + "/transport-types/TST", json={"transport_code": "TST", "description": "Test"} + ) assert response.status_code in (403, 405, 404) + def test_delete_transport_type_forbidden(): response = client.delete("/transport-types/TST") assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/reference_data/valuation_methods/dto.py b/backend/api/v1/modules/public/reference_data/valuation_methods/dto.py index a69400a0..297fb569 100644 --- a/backend/api/v1/modules/public/reference_data/valuation_methods/dto.py +++ b/backend/api/v1/modules/public/reference_data/valuation_methods/dto.py @@ -1,9 +1,9 @@ from pydantic import BaseModel, Field from pydantic import ConfigDict + class ValuationMethodDTO(BaseModel): key: str = Field(..., min_length=1, max_length=2) description: str model_config = ConfigDict(from_attributes=True) - diff --git a/backend/api/v1/modules/public/reference_data/valuation_methods/models.py b/backend/api/v1/modules/public/reference_data/valuation_methods/models.py index 7a6aa20d..73fc9a81 100644 --- a/backend/api/v1/modules/public/reference_data/valuation_methods/models.py +++ b/backend/api/v1/modules/public/reference_data/valuation_methods/models.py @@ -2,14 +2,15 @@ from sqlalchemy import String, PrimaryKeyConstraint from sqlalchemy.orm import mapped_column, Mapped from core.database import Base + class ValuationMethod(Base): - __tablename__ = "valuation_methods" #GMetValor + __tablename__ = "valuation_methods" # GMetValor __table_args__ = ( PrimaryKeyConstraint("key", name="valuation_methods_pkey"), - {"schema": "public"} + {"schema": "public"}, ) - key: Mapped[str] = mapped_column(String(2), nullable=False) + key: Mapped[str] = mapped_column(String(2), nullable=False) description: Mapped[str] = mapped_column(String(200), nullable=False) def __repr__(self): diff --git a/backend/api/v1/modules/public/reference_data/valuation_methods/routes.py b/backend/api/v1/modules/public/reference_data/valuation_methods/routes.py index 2296a7fc..614ef0a0 100644 --- a/backend/api/v1/modules/public/reference_data/valuation_methods/routes.py +++ b/backend/api/v1/modules/public/reference_data/valuation_methods/routes.py @@ -1,4 +1,3 @@ - from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.database import get_core_db @@ -11,13 +10,12 @@ from typing import Any, Dict router = APIRouter(prefix="/valuation-methods") - @router.get("/", response_model=Dict[str, Any]) async def list_valuation_methods( page: int = Query(1, ge=1, description="Número de página"), page_size: int = Query(50, ge=1, le=100, description="Tamaño de página"), db: Session = Depends(get_core_db), - current_user: dict = Depends(get_current_user) + current_user: dict = Depends(get_current_user), ): skip = (page - 1) * page_size query = db.query(ValuationMethod) @@ -27,23 +25,27 @@ async def list_valuation_methods( "items": [ValuationMethodDTO.model_validate(obj) for obj in items], "total": total, "page": page, - "page_size": page_size + "page_size": page_size, } - + @router.get("/{key}", response_model=ValuationMethodDTO) -async def get_valuation_method(key: str, db: Session = Depends(get_core_db), current_user: dict = Depends(get_current_user)): +async def get_valuation_method( + key: str, + db: Session = Depends(get_core_db), + current_user: dict = Depends(get_current_user), +): obj = db.query(ValuationMethod).filter(ValuationMethod.key == key).first() if not obj: raise HTTPException(status_code=404, detail="Not found") return obj - + @router.post("/", response_model=ValuationMethodDTO, status_code=201) async def create_valuation_method( data: ValuationMethodDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = ValuationMethod(**data.dict()) db.add(obj) @@ -51,13 +53,13 @@ async def create_valuation_method( db.refresh(obj) return obj - + @router.put("/{key}", response_model=ValuationMethodDTO) async def update_valuation_method( key: str, data: ValuationMethodDTO, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(ValuationMethod).filter(ValuationMethod.key == key).first() if not obj: @@ -68,12 +70,12 @@ async def update_valuation_method( db.refresh(obj) return obj - + @router.delete("/{key}", status_code=204) async def delete_valuation_method( key: str, db: Session = Depends(get_core_db), - current_user: dict = Depends(has_role("admin")) + current_user: dict = Depends(has_role("admin")), ): obj = db.query(ValuationMethod).filter(ValuationMethod.key == key).first() if not obj: diff --git a/backend/api/v1/modules/public/reference_data/valuation_methods/seed.py b/backend/api/v1/modules/public/reference_data/valuation_methods/seed.py index 3f861c6b..1542540a 100644 --- a/backend/api/v1/modules/public/reference_data/valuation_methods/seed.py +++ b/backend/api/v1/modules/public/reference_data/valuation_methods/seed.py @@ -6,4 +6,4 @@ seed = [ ("4", "VALOR DE PRECIO UNITARIO DE VENTA."), ("5", "VALOR RECONSTRUIDO."), ("6", "ULTIMO RECURSO"), -] \ No newline at end of file +] diff --git a/backend/api/v1/modules/public/reference_data/valuation_methods/test_valuation_methods.py b/backend/api/v1/modules/public/reference_data/valuation_methods/test_valuation_methods.py index e239d9e6..cf5f015a 100644 --- a/backend/api/v1/modules/public/reference_data/valuation_methods/test_valuation_methods.py +++ b/backend/api/v1/modules/public/reference_data/valuation_methods/test_valuation_methods.py @@ -7,6 +7,7 @@ app = FastAPI() app.include_router(router) client = TestClient(app) + @pytest.mark.usefixtures("client", "access_token") def test_list_valuation_methods(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} @@ -16,20 +17,28 @@ def test_list_valuation_methods(client, access_token): assert "page" in response.json() assert "page_size" in response.json() + @pytest.mark.usefixtures("client", "access_token") def test_get_valuation_method_not_found(client, access_token): headers = {"Authorization": f"Bearer {access_token}"} response = client.get("/valuation-methods/invalid_key", headers=headers) assert response.status_code == 404 + def test_create_valuation_method_forbidden(): - response = client.post("/valuation-methods/", json={"key": "TST", "description": "Test"}) + response = client.post( + "/valuation-methods/", json={"key": "TST", "description": "Test"} + ) assert response.status_code in (403, 405, 404) + def test_update_valuation_method_forbidden(): - response = client.put("/valuation-methods/TST", json={"key": "TST", "description": "Test"}) + response = client.put( + "/valuation-methods/TST", json={"key": "TST", "description": "Test"} + ) assert response.status_code in (403, 405, 404) + def test_delete_valuation_method_forbidden(): response = client.delete("/valuation-methods/TST") assert response.status_code in (403, 405, 404) diff --git a/backend/api/v1/modules/public/router.py b/backend/api/v1/modules/public/router.py index bcaec4b1..22589835 100644 --- a/backend/api/v1/modules/public/router.py +++ b/backend/api/v1/modules/public/router.py @@ -2,6 +2,7 @@ Router principal de API v1 Agrega todos los módulos de la aplicación """ + from fastapi import APIRouter from .reference_data.router import router as reference_data_router diff --git a/backend/api/v1/router.py b/backend/api/v1/router.py index 5c80eefe..5d7dc302 100644 --- a/backend/api/v1/router.py +++ b/backend/api/v1/router.py @@ -2,6 +2,7 @@ Router principal de API v1 Agrega todos los módulos de la aplicación """ + from fastapi import APIRouter # Importar routers de módulos @@ -15,12 +16,9 @@ router = APIRouter() router.include_router(a76_router) router.include_router(public_router) + # Health check @router.get("/status") def status(): """Health check de la API""" - return { - "status": "ok", - "version": "1.0.0", - "api": "v1" - } + return {"status": "ok", "version": "1.0.0", "api": "v1"} diff --git a/backend/core/__init__.py b/backend/core/__init__.py index c882a1d3..840b9ef2 100644 --- a/backend/core/__init__.py +++ b/backend/core/__init__.py @@ -1,6 +1,7 @@ """ Core module - Configuración y utilidades centrales de la aplicación """ + from .config import settings from .database import ( Base, @@ -8,14 +9,14 @@ from .database import ( get_async_core_db, get_tenant_db, init_db, - init_async_db + init_async_db, ) from .security import ( verify_token, get_current_user, get_current_active_user, has_role, - get_tenant_from_token + get_tenant_from_token, ) __all__ = [ diff --git a/backend/core/config.py b/backend/core/config.py index 83d290ad..887bf0b6 100644 --- a/backend/core/config.py +++ b/backend/core/config.py @@ -1,26 +1,27 @@ """ Configuración centralizada de la aplicación usando Pydantic Settings """ + from pydantic_settings import BaseSettings, SettingsConfigDict from typing import List class Settings(BaseSettings): """Configuración de la aplicación""" - + # Application APP_NAME: str = "Anexo76" APP_VERSION: str = "1.0.0" DEBUG: bool = True ENVIRONMENT: str = "development" - + # Database - Core (Shared) CORE_DB_HOST: str = "postgres-a76" CORE_DB_PORT: int = 5432 CORE_DB_NAME: str = "anexo76_core" CORE_DB_USER: str = "postgres" CORE_DB_PASSWORD: str = "postgres" - + # Keycloak KEYCLOAK_SERVER_URL: str = "http://localhost:8080" KEYCLOAK_REALM: str = "master" @@ -28,34 +29,32 @@ class Settings(BaseSettings): KEYCLOAK_CLIENT_SECRET: str = "" KEYCLOAK_ADMIN_USERNAME: str = "admin" KEYCLOAK_ADMIN_PASSWORD: str = "admin" - + # Security SECRET_KEY: str = "change-this-secret-key-in-production" ALGORITHM: str = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 - + # CORS CORS_ORIGINS: str = "http://localhost:5173,http://localhost:3000" - + # License LICENSE_CHECK_ENABLED: bool = True - + model_config = SettingsConfigDict( - env_file=".env", - case_sensitive=True, - extra="ignore" + env_file=".env", case_sensitive=True, extra="ignore" ) - + @property def core_database_url(self) -> str: """URL de conexión a la base de datos core""" return f"postgresql://{self.CORE_DB_USER}:{self.CORE_DB_PASSWORD}@{self.CORE_DB_HOST}:{self.CORE_DB_PORT}/{self.CORE_DB_NAME}" - + @property def async_core_database_url(self) -> str: """URL de conexión asíncrona a la base de datos core""" return f"postgresql+asyncpg://{self.CORE_DB_USER}:{self.CORE_DB_PASSWORD}@{self.CORE_DB_HOST}:{self.CORE_DB_PORT}/{self.CORE_DB_NAME}" - + @property def cors_origins_list(self) -> List[str]: """Lista de orígenes CORS permitidos""" diff --git a/backend/core/database.py b/backend/core/database.py index 6c107270..bd1ec4e2 100644 --- a/backend/core/database.py +++ b/backend/core/database.py @@ -3,6 +3,7 @@ Configuración de base de datos con soporte multi-tenant - Base de datos compartida (core_db) para tenants pequeños/medianos - Bases de datos dedicadas para clientes enterprise """ + from sqlalchemy import create_engine from sqlalchemy.orm import declarative_base from sqlalchemy.orm import sessionmaker, Session @@ -20,14 +21,10 @@ core_engine = create_engine( pool_pre_ping=True, pool_size=10, max_overflow=20, - echo=settings.DEBUG + echo=settings.DEBUG, ) -CoreSessionLocal = sessionmaker( - autocommit=False, - autoflush=False, - bind=core_engine -) +CoreSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=core_engine) # Engine asíncrono para operaciones async async_core_engine = create_async_engine( @@ -35,13 +32,11 @@ async_core_engine = create_async_engine( pool_pre_ping=True, pool_size=10, max_overflow=20, - echo=settings.DEBUG + echo=settings.DEBUG, ) AsyncCoreSessionLocal = async_sessionmaker( - async_core_engine, - class_=AsyncSession, - expire_on_commit=False + async_core_engine, class_=AsyncSession, expire_on_commit=False ) # Cache de engines para tenants con BD dedicada @@ -74,33 +69,32 @@ async def get_async_core_db() -> AsyncGenerator[AsyncSession, None]: def get_tenant_engine(tenant_id: int, db_config: dict): """ Obtiene o crea un engine para un tenant con BD dedicada - + Args: tenant_id: ID del tenant db_config: Configuración de BD {host, port, name, user, password} - + Returns: Engine de SQLAlchemy para el tenant """ if tenant_id not in _tenant_engines: db_url = f"postgresql://{db_config['user']}:{db_config['password']}@{db_config['host']}:{db_config['port']}/{db_config['name']}" _tenant_engines[tenant_id] = create_engine( - db_url, - pool_pre_ping=True, - pool_size=5, - max_overflow=10 + db_url, pool_pre_ping=True, pool_size=5, max_overflow=10 ) return _tenant_engines[tenant_id] @contextmanager -def get_tenant_db(tenant_id: int, db_config: Optional[dict] = None) -> Generator[Session, None, None]: +def get_tenant_db( + tenant_id: int, db_config: Optional[dict] = None +) -> Generator[Session, None, None]: """ Context manager para obtener sesión de BD de un tenant específico - + Si db_config es None, usa la BD core (compartida) Si db_config está presente, usa la BD dedicada del tenant - + Uso: with get_tenant_db(tenant_id, config) as db: # operaciones con db @@ -113,7 +107,7 @@ def get_tenant_db(tenant_id: int, db_config: Optional[dict] = None) -> Generator engine = get_tenant_engine(tenant_id, db_config) SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) db = SessionLocal() - + try: yield db finally: diff --git a/backend/core/middleware.py b/backend/core/middleware.py index 557e98d4..f57fc711 100644 --- a/backend/core/middleware.py +++ b/backend/core/middleware.py @@ -4,6 +4,7 @@ Middleware personalizado para Anexo76 - Gestión de multi-tenancy - Logging de requests """ + from fastapi import Request, HTTPException from starlette.middleware.base import BaseHTTPMiddleware from typing import Callable @@ -22,58 +23,59 @@ class TenantMiddleware(BaseHTTPMiddleware): """ Middleware para identificar y validar el tenant en cada request """ - + async def dispatch(self, request: Request, call_next: Callable): # Rutas públicas que no requieren tenant # Permitir acceso sin autenticación a rutas de documentación y salud - doc_prefixes = [ - "/api/redoc", - "/api/openapi.json" - ] - public_prefixes = [ - "/api/v1/auth", - "/api/v1/status", - "/api/health", - "/api/" - ] + doc_prefixes = ["/api/redoc", "/api/openapi.json"] + public_prefixes = ["/api/v1/auth", "/api/v1/status", "/api/health", "/api/"] path = request.url.path # Permitir cualquier subruta de docs/redoc/openapi.json (por ejemplo, /api/docs, /api/docs/, /api/docs/oauth2-redirect) - if any(path == prefix or path.startswith(prefix + "/") for prefix in doc_prefixes): + if any( + path == prefix or path.startswith(prefix + "/") for prefix in doc_prefixes + ): return await call_next(request) # Permitir rutas públicas exactas o con prefijo - if any(path == prefix or (prefix != "/" and path.startswith(prefix)) for prefix in public_prefixes): + if any( + path == prefix or (prefix != "/" and path.startswith(prefix)) + for prefix in public_prefixes + ): return await call_next(request) - + # Extraer token y obtener tenant auth_header = request.headers.get("Authorization") - + if not auth_header or not auth_header.startswith("Bearer "): - raise HTTPException(status_code=401, detail="Missing or invalid authorization header") - + raise HTTPException( + status_code=401, detail="Missing or invalid authorization header" + ) + token = auth_header.split(" ")[1] - + try: user_info = verify_token(token) tenant_id = get_tenant_from_token(user_info) - + # ⚠️ NOTA: tenant_id puede ser None para usuarios SSO que aún no tienen tenant asignado # En ese caso, el endpoint específico deberá manejarlo if not tenant_id: - logger.warning(f"⚠️ Token sin tenant_id para usuario: {user_info.get('sub', 'unknown')}") + logger.warning( + f"⚠️ Token sin tenant_id para usuario: {user_info.get('sub', 'unknown')}" + ) # No lanzamos error aquí, dejamos que el endpoint decida qué hacer - + # Agregar tenant_id al state del request (puede ser None) request.state.tenant_id = tenant_id request.state.user_info = user_info - + except HTTPException: # Re-lanzar HTTPException directamente raise except Exception as e: logger.error(f"❌ Tenant validation error: {str(e)}") raise HTTPException(status_code=401, detail="Invalid authentication") - + response = await call_next(request) return response @@ -82,58 +84,60 @@ class LicenseValidationMiddleware(BaseHTTPMiddleware): """ Middleware para validar la licencia del tenant antes de procesar requests """ - + async def dispatch(self, request: Request, call_next: Callable): if not settings.LICENSE_CHECK_ENABLED: return await call_next(request) - + # Rutas que no requieren validación de licencia exempt_paths = [ - "/api/docs", + "/api/docs", "/api/redoc", - "/openapi.json", - "/api/v1/auth", - "/api/v1/auth", + "/openapi.json", + "/api/v1/auth", + "/api/v1/auth", "/api/v1/status", "/api/v1/status", "/api/health", - "/api/" + "/api/", ] - + # Verificar si la ruta está exenta (comparación exacta o prefijo) is_exempt = False for path in exempt_paths: - if request.url.path == path or (path != "/" and request.url.path.startswith(path)): + if request.url.path == path or ( + path != "/" and request.url.path.startswith(path) + ): is_exempt = True break - + if is_exempt: return await call_next(request) - + # Obtener tenant_id del request state (debe ser seteado por TenantMiddleware) tenant_id = getattr(request.state, "tenant_id", None) - + if not tenant_id: return await call_next(request) # Dejamos que TenantMiddleware maneje esto - + # Validar licencia db = CoreSessionLocal() try: # Importar aquí para evitar imports circulares from api.v1.modules.a76.licenses.service import LicenseService - + license_service = LicenseService(db) license_info = license_service.validate_license(tenant_id) - + if not license_info["is_valid"]: raise HTTPException( status_code=402, - detail=f"License validation failed: {license_info['reason']}" + detail=f"License validation failed: {license_info['reason']}", ) - + # Agregar info de licencia al request state request.state.license_info = license_info - + except HTTPException: raise except Exception as e: @@ -141,7 +145,7 @@ class LicenseValidationMiddleware(BaseHTTPMiddleware): raise HTTPException(status_code=500, detail="License validation error") finally: db.close() - + response = await call_next(request) return response @@ -150,15 +154,15 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware): """ Middleware para logging de requests """ - + async def dispatch(self, request: Request, call_next: Callable): start_time = time.time() - + # Log request logger.info(f"Request: {request.method} {request.url.path}") - + response = await call_next(request) - + # Log response process_time = time.time() - start_time logger.info( @@ -166,8 +170,8 @@ class RequestLoggingMiddleware(BaseHTTPMiddleware): f"Status: {response.status_code} " f"Duration: {process_time:.3f}s" ) - + # Agregar header con tiempo de procesamiento response.headers["X-Process-Time"] = str(process_time) - + return response diff --git a/backend/core/security.py b/backend/core/security.py index 96f29405..b72b9fca 100644 --- a/backend/core/security.py +++ b/backend/core/security.py @@ -1,11 +1,13 @@ """ Utilidades de seguridad y autenticación con Keycloak """ + from fastapi import HTTPException, Security, Depends from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from keycloak import KeycloakOpenID from jose import jwt, JWTError from typing import Optional, Dict, Any +from sqlalchemy.orm import Session from .config import settings import logging from sqlalchemy.orm import Session @@ -19,7 +21,7 @@ keycloak_openid = KeycloakOpenID( server_url=settings.KEYCLOAK_SERVER_URL, client_id=settings.KEYCLOAK_CLIENT_ID, realm_name=settings.KEYCLOAK_REALM, - client_secret_key=settings.KEYCLOAK_CLIENT_SECRET + client_secret_key=settings.KEYCLOAK_CLIENT_SECRET, ) # Security scheme @@ -29,13 +31,13 @@ security = HTTPBearer() def verify_token(token: str) -> Dict[str, Any]: """ Verifica y decodifica un token JWT de Keycloak - + Args: token: Token JWT - + Returns: Payload del token decodificado - + Raises: HTTPException: Si el token es inválido """ @@ -46,45 +48,30 @@ def verify_token(token: str) -> Dict[str, Any]: + keycloak_openid.public_key() + "\n-----END PUBLIC KEY-----" ) - + # Decodificar y verificar token - options = { - "verify_signature": True, - "verify_aud": False, - "verify_exp": True - } - + options = {"verify_signature": True, "verify_aud": False, "verify_exp": True} + decoded_token = jwt.decode( - token, - KEYCLOAK_PUBLIC_KEY, - algorithms=["RS256"], - options=options + token, KEYCLOAK_PUBLIC_KEY, algorithms=["RS256"], options=options ) - + return decoded_token - + except JWTError as e: logger.error(f"Token verification failed: {str(e)}") - raise HTTPException( - status_code=401, - detail="Could not validate credentials" - ) + raise HTTPException(status_code=401, detail="Could not validate credentials") except Exception as e: logger.error(f"Unexpected error during token verification: {str(e)}") - raise HTTPException( - status_code=401, - detail="Authentication error" - ) + raise HTTPException(status_code=401, detail="Authentication error") async def get_current_user( credentials: HTTPAuthorizationCredentials = Security(security), - db: Session = Depends(get_core_db) ) -> Dict[str, Any]: """ Dependency para obtener el usuario actual desde el token JWT - Enriquecido con tenant_id y company_id desde la tabla user_tenant - + Uso en FastAPI: current_user: dict = Depends(get_current_user) """ @@ -109,7 +96,7 @@ async def get_current_user( async def get_current_active_user( - current_user: Dict[str, Any] = Depends(get_current_user) + current_user: Dict[str, Any] = Depends(get_current_user), ) -> Dict[str, Any]: """ Dependency para obtener usuario activo (puede incluir validaciones adicionales) @@ -122,121 +109,127 @@ async def get_current_active_user( def has_role(required_role: str): """ Decorator/Dependency para verificar roles de usuario - + Uso: @router.get("/admin") async def admin_endpoint(user = Depends(has_role("admin"))): ... """ + async def role_checker( - current_user: Dict[str, Any] = Depends(get_current_user) + current_user: Dict[str, Any] = Depends(get_current_user), ) -> Dict[str, Any]: user_roles = current_user.get("realm_access", {}).get("roles", []) - + if required_role not in user_roles: raise HTTPException( status_code=403, - detail=f"User does not have required role: {required_role}" + detail=f"User does not have required role: {required_role}", ) - + return current_user - + return role_checker def get_tenant_from_token(user_info: Dict[str, Any]) -> Optional[int]: """ Extrae el tenant_id del token JWT - + El tenant_id puede estar en diferentes lugares según configuración de Keycloak: - En claims personalizados - En el realm - En atributos del usuario """ # Intentar obtener de claims personalizados - tenant_id = user_info.get("tenant_id") - + tenant_id = user_info.get("tenant_id") if not tenant_id: # Intentar obtener de atributos tenant_id = user_info.get("attributes", {}).get("tenant_id") - + if tenant_id: return int(tenant_id) - + return None -def validate_company_access( - company_id: int, - current_user: Dict[str, Any] -) -> bool: +def validate_company_access(db: Session, company_id: int, current_user: Dict[str, Any]) -> bool: """ Valida que el usuario tenga acceso a la compañía solicitada - + Args: company_id: ID de la compañía a la que se quiere acceder current_user: Información del usuario actual desde el token - + Returns: True si el usuario tiene acceso, False en caso contrario - + Nota: - Por ahora solo verifica que el tenant_id del usuario coincida con el company_id. - Se puede extender para validar permisos específicos por compañía. + Verifica que la compañía pertenezca al tenant del usuario consultando la BD. """ - tenant_id = get_tenant_from_token(current_user) + tenant_id = get_tenant_from_token(current_user) + # Si no hay tenant_id en el token, denegar acceso if not tenant_id: return False - - # Validar que el company_id pertenezca al tenant del usuario - # Por ahora asumimos que company_id == tenant_id - # Esto se puede modificar si hay una tabla de relación tenant-company - return tenant_id == company_id -def validate_access_to_resource( - company_id: int, - current_user: dict = Depends(get_current_user) -) -> bool: + # Consultar si la compañía pertenece al tenant + try: + from api.v1.modules.a76.company.models import Company + + company = db.query(Company).filter( + Company.id == company_id, + Company.tenant_id == tenant_id + ).first() + + return company is not None + finally: + db.close() + + +def validate_access_to_resource(db: Session, company_id: int, current_user: Dict[str, Any]) -> int: """ Valida que el usuario tenga acceso a un recurso específico basado en company_id y regresa el tenant_id - + Args: company_id: company_id asociado al recurso current_user: Información del usuario actual desde el token - + Returns: - True si el usuario tiene acceso, False en caso contrario + tenant_id si el usuario tiene acceso + + Raises: + HTTPException: Si no hay tenant_id o no tiene acceso """ - + tenant_id = get_tenant_from_token(current_user) if not tenant_id: raise HTTPException(status_code=400, detail="Tenant ID not found in token") - - if not validate_company_access(company_id, current_user): + + if not validate_company_access(db, company_id, current_user): raise HTTPException(status_code=403, detail="Access denied to this company") - + # Validar que el tenant_id del usuario coincida con el del recurso return tenant_id class KeycloakClient: """Cliente para interactuar con Keycloak Admin API""" - + def __init__(self): self.openid = keycloak_openid - + def create_user(self, email: str, password: str, tenant_id: int, **kwargs): """Crea un usuario en Keycloak""" # Implementar lógica para crear usuario usando keycloak admin pass - + def assign_role(self, user_id: str, role: str): """Asigna un rol a un usuario""" pass - + def create_tenant_realm(self, tenant_name: str): """Crea un realm para un nuevo tenant""" pass diff --git a/backend/main.py b/backend/main.py index fb95f89c..26ce5330 100644 --- a/backend/main.py +++ b/backend/main.py @@ -2,6 +2,7 @@ Anexo76 - Aplicación SaaS para gestión de comercio exterior Backend API con FastAPI + Keycloak + SQLAlchemy """ + from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware import logging @@ -10,7 +11,7 @@ from core.config import settings from core.middleware import ( TenantMiddleware, LicenseValidationMiddleware, - RequestLoggingMiddleware + RequestLoggingMiddleware, ) from core.database import init_db from api.v1.router import router as api_v1_router @@ -18,7 +19,7 @@ from api.v1.router import router as api_v1_router # Configurar logging logging.basicConfig( level=logging.INFO if not settings.DEBUG else logging.DEBUG, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", ) logger = logging.getLogger(__name__) @@ -33,6 +34,7 @@ app = FastAPI( openapi_url="/api/openapi.json" if settings.DEBUG else None, ) + # Inicializar la base de datos @app.on_event("startup") async def on_startup(): @@ -41,6 +43,7 @@ async def on_startup(): init_db() logger.info("Base de datos inicializada correctamente.") + # Configurar CORS app.add_middleware( CORSMiddleware, @@ -60,6 +63,7 @@ app.add_middleware(TenantMiddleware) # Registrar routers app.include_router(api_v1_router, prefix="/api/v1") + @app.get("/api/") async def root(): """Root endpoint""" @@ -67,14 +71,11 @@ async def root(): "name": "Anexo76 API", "version": settings.APP_VERSION, "status": "running", - "docs": "/api/docs" if settings.DEBUG else "disabled in production" + "docs": "/api/docs" if settings.DEBUG else "disabled in production", } @app.get("/api/health") async def health_check(): """Health check endpoint""" - return { - "status": "healthy", - "environment": settings.ENVIRONMENT - } + return {"status": "healthy", "environment": settings.ENVIRONMENT} diff --git a/backend/requirements.txt b/backend/requirements.txt index 61753420..9f13d289 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -36,3 +36,4 @@ pytest-cov==7.0.0 black==25.9.0 flake8==7.3.0 mypy==1.18.2 +pylint==4.0.2 diff --git a/frontend/src/lib/api/dashboard/a76/pedimentos.ts b/frontend/src/lib/api/dashboard/a76/pedimentos.ts index f8875433..875a3d56 100644 --- a/frontend/src/lib/api/dashboard/a76/pedimentos.ts +++ b/frontend/src/lib/api/dashboard/a76/pedimentos.ts @@ -80,9 +80,10 @@ export const pedimentosApi = { * @param page - Número de página (por defecto 1) * @param pageSize - Tamaño de página (por defecto 50) * @param filters - Filtros opcionales + * @param companyId - ID de la compañía (por defecto 1) */ - list: (page = 1, pageSize = 50, filters?: PedimentoFilters) => { - let url = `/v1/a76/pedimentos?page=${page}&page_size=${pageSize}`; + list: (page = 1, pageSize = 50, filters?: PedimentoFilters, companyId = 1) => { + let url = `/v1/a76/pedimentos?company_id=${companyId}&page=${page}&page_size=${pageSize}`; if (filters?.status) { url += `&status=${encodeURIComponent(filters.status)}`; @@ -100,27 +101,31 @@ export const pedimentosApi = { /** * Obtiene un pedimento por ID * @param id - ID del pedimento + * @param companyId - ID de la compañía (por defecto 1) */ - get: (id: number) => api.get(`/v1/a76/pedimentos/${id}`), + get: (id: number, companyId = 1) => api.get(`/v1/a76/pedimentos/${id}?company_id=${companyId}`), /** * Crea un nuevo pedimento * @param data - Datos del pedimento a crear + * @param companyId - ID de la compañía (por defecto 1) */ - create: (data: CreatePedimentoData) => - api.post('/v1/a76/pedimentos', data), + create: (data: CreatePedimentoData, companyId = 1) => + api.post(`/v1/a76/pedimentos?company_id=${companyId}`, data), /** * Actualiza un pedimento existente * @param id - ID del pedimento a actualizar * @param data - Datos a actualizar + * @param companyId - ID de la compañía (por defecto 1) */ - update: (id: number, data: UpdatePedimentoData) => - api.put(`/v1/a76/pedimentos/${id}`, data), + update: (id: number, data: UpdatePedimentoData, companyId = 1) => + api.put(`/v1/a76/pedimentos/${id}?company_id=${companyId}`, data), /** * Elimina un pedimento * @param id - ID del pedimento a eliminar + * @param companyId - ID de la compañía (por defecto 1) */ - delete: (id: number) => api.delete(`/v1/a76/pedimentos/${id}`) + delete: (id: number, companyId = 1) => api.delete(`/v1/a76/pedimentos/${id}?company_id=${companyId}`) }; diff --git a/frontend/src/lib/auth.ts b/frontend/src/lib/auth.ts index 17bca381..c15e97b6 100644 --- a/frontend/src/lib/auth.ts +++ b/frontend/src/lib/auth.ts @@ -173,8 +173,11 @@ export const initKeycloak = async (): Promise => { } }; +// Variable para rastrear el tenant anterior +let previousTenantId: number | undefined = undefined; + /** - * Actualiza el estado de autenticación + * Actualiza el estado de autenticación con los datos de Keycloak */ const updateAuthState = async () => { if (!keycloakInstance?.authenticated) { @@ -189,19 +192,36 @@ const updateAuthState = async () => { const roles = tokenParsed?.realm_access?.roles || []; const tenantId = tokenParsed?.tenant_id || tokenParsed?.attributes?.tenant_id; + const newTenantId = tenantId ? parseInt(tenantId) : undefined; + + // Detectar si cambió el tenant + const tenantChanged = previousTenantId !== undefined && previousTenantId !== newTenantId; const user: User = { id: profile.id || '', username: profile.username || '', email: profile.email, name: `${profile.firstName || ''} ${profile.lastName || ''}`.trim(), - tenantId: tenantId ? parseInt(tenantId) : undefined, + tenantId: newTenantId, roles }; authStore.setAuthenticated(true); authStore.setUser(user); authStore.setToken(token); + + // Si cambió el tenant, limpiar el store de compañías + if (tenantChanged && browser) { + try { + const { companyStore } = await import('./stores/company.svelte'); + companyStore.clear(); + } catch (error) { + console.error('Error al limpiar store de compañías:', error); + } + } + + // Actualizar el tenant anterior + previousTenantId = newTenantId; } catch (error) { console.error('Error actualizando estado de autenticación:', error); authStore.reset(); @@ -361,6 +381,14 @@ export const logout = async () => { } } + // Limpiar store de compañías + try { + const { companyStore } = await import('./stores/company.svelte'); + companyStore.clear(); + } catch (error) { + console.error('Error al limpiar store de compañías:', error); + } + // Limpiar estado local authStore.reset(); localStorage.removeItem('access_token'); diff --git a/frontend/src/lib/components/sidebar/app-sidebar.svelte b/frontend/src/lib/components/sidebar/app-sidebar.svelte index b723c4a8..7333a6a7 100644 --- a/frontend/src/lib/components/sidebar/app-sidebar.svelte +++ b/frontend/src/lib/components/sidebar/app-sidebar.svelte @@ -35,7 +35,7 @@ - + diff --git a/frontend/src/lib/components/sidebar/team-switcher.svelte b/frontend/src/lib/components/sidebar/team-switcher.svelte index 47b41332..ae5870d7 100644 --- a/frontend/src/lib/components/sidebar/team-switcher.svelte +++ b/frontend/src/lib/components/sidebar/team-switcher.svelte @@ -3,14 +3,18 @@ import * as Sidebar from "$lib/components/ui/sidebar/index.js"; import { useSidebar } from "$lib/components/ui/sidebar/index.js"; import ChevronsUpDownIcon from "@lucide/svelte/icons/chevrons-up-down"; - import PlusIcon from "@lucide/svelte/icons/plus"; + import BuildingIcon from "@lucide/svelte/icons/building"; + import CheckIcon from "@lucide/svelte/icons/check"; + import { companyStore } from "$lib/stores/company.svelte"; - // This should be `Component` after @lucide/svelte updates types - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let { teams }: { teams: { name: string; logo: any; plan: string }[] } = $props(); const sidebar = useSidebar(); - let activeTeam = $state(teams[0]); + // Inicializar el store cuando se monta el componente + $effect(() => { + if (companyStore.companies.length === 0) { + companyStore.initialize(); + } + }); @@ -26,15 +30,27 @@
- + {#if companyStore.activeCompany?.logo} + {companyStore.activeCompany.name} + {:else} + + {/if}
- {activeTeam.name} + {companyStore.activeCompany?.name || 'Seleccionar compañía'} - {activeTeam.plan} + {#if companyStore.activeCompany?.rfc} + + {companyStore.activeCompany.rfc} + + {/if}
- + {/snippet} @@ -44,25 +60,50 @@ side={sidebar.isMobile ? "bottom" : "right"} sideOffset={4} > - Teams - {#each teams as team, index (team.name)} - (activeTeam = team)} class="gap-2 p-2"> -
- -
- {team.name} - ⌘{index + 1} + + Mis Compañías + + + {#if companyStore.loading} + + Cargando... - {/each} - - -
- -
-
Add team
-
+ {:else if companyStore.companies.length === 0} + + No hay compañías disponibles + + {:else} + {#each companyStore.companies as company, index (company.id)} + companyStore.setActiveCompany(company)} + class="gap-2 p-2 cursor-pointer" + > +
+ {#if company.logo} + {company.name} + {:else} + + {/if} +
+
+ {company.name} + {#if company.rfc} + {company.rfc} + {/if} +
+ {#if companyStore.activeCompany?.id === company.id} + + {/if} + {#if index < 9} + ⌘{index + 1} + {/if} +
+ {/each} + {/if} diff --git a/frontend/src/lib/components/ui/tooltip/index.ts b/frontend/src/lib/components/ui/tooltip/index.ts index 313a7f06..aacf780b 100644 --- a/frontend/src/lib/components/ui/tooltip/index.ts +++ b/frontend/src/lib/components/ui/tooltip/index.ts @@ -2,9 +2,10 @@ import { Tooltip as TooltipPrimitive } from "bits-ui"; import Trigger from "./tooltip-trigger.svelte"; import Content from "./tooltip-content.svelte"; -const Root = TooltipPrimitive.Root; -const Provider = TooltipPrimitive.Provider; -const Portal = TooltipPrimitive.Portal; +// Handle SSR safely +const Root = TooltipPrimitive?.Root ?? (class {} as any); +const Provider = TooltipPrimitive?.Provider ?? (class {} as any); +const Portal = TooltipPrimitive?.Portal ?? (class {} as any); export { Root, diff --git a/frontend/src/lib/stores/company.svelte.ts b/frontend/src/lib/stores/company.svelte.ts new file mode 100644 index 00000000..7933c59a --- /dev/null +++ b/frontend/src/lib/stores/company.svelte.ts @@ -0,0 +1,122 @@ +/** + * Store para manejar la compañía activa del usuario + * Permite cambiar entre las compañías que pertenecen al tenant + */ + +interface Company { + id: number; + name: string; + rfc?: string; + logo?: string; + tenant_id: number; +} + +class CompanyStore { + private _activeCompany = $state(null); + private _companies = $state([]); + private _loading = $state(false); + + get activeCompany() { + return this._activeCompany; + } + + get companies() { + return this._companies; + } + + get loading() { + return this._loading; + } + + /** + * Carga las compañías del tenant del usuario desde el backend + */ + async loadCompanies() { + this._loading = true; + try { + const response = await fetch('/api/company/my-companies'); + if (response.ok) { + this._companies = await response.json(); + + // Si hay compañías y no hay una activa, seleccionar la primera + if (this._companies.length > 0 && !this._activeCompany) { + this.setActiveCompany(this._companies[0]); + } + } else { + console.error('Error loading companies:', response.statusText); + } + } catch (error) { + console.error('Error loading companies:', error); + } finally { + this._loading = false; + } + } + + /** + * Establece la compañía activa + */ + setActiveCompany(company: Company) { + this._activeCompany = company; + + // Guardar en localStorage para persistencia + if (typeof window !== 'undefined') { + localStorage.setItem('activeCompanyId', company.id.toString()); + } + + // Guardar en cookie para acceso desde el servidor (SSR) + if (typeof document !== 'undefined') { + document.cookie = `active_company_id=${company.id}; path=/; max-age=${60 * 60 * 24 * 30}; SameSite=Lax`; + } + + // Despachar evento personalizado para que otros componentes reaccionen + if (typeof window !== 'undefined') { + window.dispatchEvent(new CustomEvent('companyChanged', { + detail: { companyId: company.id } + })); + } + } + + /** + * Restaura la compañía activa desde localStorage + */ + restoreActiveCompany() { + if (typeof window !== 'undefined') { + const savedId = localStorage.getItem('activeCompanyId'); + if (savedId && this._companies.length > 0) { + const company = this._companies.find(c => c.id === parseInt(savedId)); + if (company) { + this._activeCompany = company; + } + } + } + } + + /** + * Limpia el store (útil al cambiar de tenant o cerrar sesión) + */ + clear() { + this._activeCompany = null; + this._companies = []; + this._loading = false; + + // Limpiar localStorage + if (typeof window !== 'undefined') { + localStorage.removeItem('activeCompanyId'); + } + + // Limpiar cookie + if (typeof document !== 'undefined') { + document.cookie = 'active_company_id=; path=/; max-age=0'; + } + } + + /** + * Inicializa el store cargando las compañías + */ + async initialize() { + await this.loadCompanies(); + this.restoreActiveCompany(); + } +} + +export const companyStore = new CompanyStore(); diff --git a/frontend/src/routes/api/company/my-companies/+server.ts b/frontend/src/routes/api/company/my-companies/+server.ts new file mode 100644 index 00000000..af6ec0fb --- /dev/null +++ b/frontend/src/routes/api/company/my-companies/+server.ts @@ -0,0 +1,43 @@ +/** + * API route proxy para obtener las compañías del usuario + */ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; + +export const GET: RequestHandler = async ({ cookies, fetch }) => { + const token = cookies.get('access_token'); + + if (!token) { + return json({ error: 'No authenticated' }, { status: 401 }); + } + + // Configurar la URL de la API + let apiUrl = process.env.INTERNAL_API_URL; + if (!apiUrl) { + apiUrl = process.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}/`; + + try { + const response = await fetch(`${baseUrl}v1/a76/company/my-companies`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + return json({ error: 'Failed to fetch companies' }, { status: response.status }); + } + + const companies = await response.json(); + return json(companies); + } catch (error) { + console.error('Error fetching companies:', error); + return json({ error: 'Internal server error' }, { status: 500 }); + } +}; diff --git a/frontend/src/routes/dashboard/pedimentos/+page.server.ts b/frontend/src/routes/dashboard/pedimentos/+page.server.ts index e69dd3c9..1d50c83f 100644 --- a/frontend/src/routes/dashboard/pedimentos/+page.server.ts +++ b/frontend/src/routes/dashboard/pedimentos/+page.server.ts @@ -22,7 +22,38 @@ export const load: PageServerLoad = async ({ fetch, cookies }) => { // Normalizar la URL const baseUrl = apiUrl?.endsWith('/') ? apiUrl : `${apiUrl}/`; - const response = await fetch(`${baseUrl}v1/a76/pedimentos?page=1&page_size=50`, { + // Obtener el company_id de la cookie o usar la primera disponible + let companyId = cookies.get('active_company_id'); + + // Si no hay companyId en cookie, obtener las compañías del usuario y usar la primera + if (!companyId) { + const companiesResponse = await fetch(`${baseUrl}v1/a76/company/my-companies`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); + + if (companiesResponse.ok) { + const companies = await companiesResponse.json(); + if (companies.length > 0) { + companyId = companies[0].id.toString(); + } + } + } + + // Si aún no hay companyId, mostrar error + if (!companyId) { + return { + items: [], + total: 0, + page: 1, + page_size: 50, + error: 'No se encontró una compañía seleccionada' + }; + } + + const response = await fetch(`${baseUrl}v1/a76/pedimentos?company_id=${companyId}&page=1&page_size=50`, { headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' diff --git a/scripts/init_first_time.sh b/scripts/init_first_time.sh index 23ce645a..67aeb7f3 100755 --- a/scripts/init_first_time.sh +++ b/scripts/init_first_time.sh @@ -534,8 +534,8 @@ COMPANY_EXISTS=$(echo "$COMPANY_EXISTS" | xargs) if [ "$COMPANY_EXISTS" = "0" ]; then PGPASSWORD="${POSTGRES_PASSWORD}" psql -h "${POSTGRES_HOST}" -p "${POSTGRES_PORT}" -U "${POSTGRES_USER}" -d "${POSTGRES_DB}" <