feat: Implement client and provider management service

- Added service layer for handling client and provider operations including creation, retrieval, updating, and deletion.
- Introduced DTOs for data transfer and validation.
- Implemented filtering and pagination for client/provider listing.
- Added logging for better traceability of operations.

feat: Create parts management module

- Developed a complete module for managing parts/components including creation, retrieval, updating, and deletion.
- Introduced DTOs for parts with detailed attributes and validation.
- Implemented search and filtering capabilities for parts based on various criteria.
- Added endpoints for regulatory information retrieval and parts statistics.
- Integrated logging for error handling and operational insights.
This commit is contained in:
2025-11-06 20:29:18 -06:00
parent 5c9dbacfdf
commit ef5c40af92
24 changed files with 169 additions and 516 deletions

1
.gitignore vendored
View File

@@ -15,7 +15,6 @@ downloads/
eggs/ eggs/
.eggs/ .eggs/
lib64/ lib64/
parts/
sdist/ sdist/
var/ var/
wheels/ wheels/

View File

@@ -13,7 +13,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic. # revision identifiers, used by Alembic.
revision: str = '03b786378f94' revision: str = '03b786378f94'
down_revision: Union[str, Sequence[str], None] = '7937209f9718' down_revision: Union[str, Sequence[str], None] = '54f2046774d0'
branch_labels: Union[str, Sequence[str], None] = None branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None depends_on: Union[str, Sequence[str], None] = None

View File

@@ -22,7 +22,7 @@ def upgrade() -> None:
"""Upgrade schema - Create only new A76 tables.""" """Upgrade schema - Create only new A76 tables."""
# Create new A76 tables only (skip existing tenants, licenses, license_usage) # Create new A76 tables only (skip existing tenants, licenses, license_usage)
op.create_table('gclient_provider', op.create_table('client_provider',
sa.Column('client_id', sa.String(length=8), nullable=False), sa.Column('client_id', sa.String(length=8), nullable=False),
sa.Column('type_nat_foreign', sa.String(length=1), nullable=True), sa.Column('type_nat_foreign', sa.String(length=1), nullable=True),
sa.Column('name', sa.String(length=256), nullable=True), sa.Column('name', sa.String(length=256), nullable=True),
@@ -81,7 +81,7 @@ def upgrade() -> None:
schema='a76' schema='a76'
) )
op.create_table('gclasses', op.create_table('classes',
sa.Column('client_key', sa.Integer(), nullable=False), sa.Column('client_key', sa.Integer(), nullable=False),
sa.Column('class_code', sa.String(length=8), nullable=False), sa.Column('class_code', sa.String(length=8), nullable=False),
sa.Column('description_spanish', sa.String(length=500), nullable=True), sa.Column('description_spanish', sa.String(length=500), nullable=True),
@@ -98,7 +98,7 @@ def upgrade() -> None:
schema='a76' schema='a76'
) )
op.create_table('gparts', op.create_table('parts',
sa.Column('client_key', sa.Integer(), nullable=False), sa.Column('client_key', sa.Integer(), nullable=False),
sa.Column('part_number', sa.String(length=49), nullable=False), sa.Column('part_number', sa.String(length=49), nullable=False),
sa.Column('fraction', sa.String(length=10), nullable=True), sa.Column('fraction', sa.String(length=10), nullable=True),
@@ -151,7 +151,7 @@ def upgrade() -> None:
sa.Column('email', sa.String(length=100), nullable=True), sa.Column('email', sa.String(length=100), nullable=True),
sa.Column('contact', sa.String(length=50), nullable=True), sa.Column('contact', sa.String(length=50), nullable=True),
sa.Column('reference', sa.String(length=250), nullable=True), sa.Column('reference', sa.String(length=250), nullable=True),
sa.ForeignKeyConstraint(['client_id'], ['a76.gclient_provider.client_id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(['client_id'], ['a76.client_provider.client_id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('client_id'), sa.PrimaryKeyConstraint('client_id'),
schema='a76' schema='a76'
) )
@@ -178,7 +178,7 @@ def upgrade() -> None:
sa.Column('subassembly_service', sa.SmallInteger(), nullable=True), sa.Column('subassembly_service', sa.SmallInteger(), nullable=True),
sa.Column('autse_dates', sa.Integer(), nullable=True), sa.Column('autse_dates', sa.Integer(), nullable=True),
sa.Column('autse_number', sa.String(length=300), nullable=True), sa.Column('autse_number', sa.String(length=300), nullable=True),
sa.ForeignKeyConstraint(['client_id'], ['a76.gclient_provider.client_id'], ondelete='CASCADE'), sa.ForeignKeyConstraint(['client_id'], ['a76.client_provider.client_id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('client_id'), sa.PrimaryKeyConstraint('client_id'),
schema='a76' schema='a76'
) )
@@ -189,7 +189,7 @@ def downgrade() -> None:
# Drop tables in reverse dependency order # Drop tables in reverse dependency order
op.drop_table('gclient_provider_programs', schema='a76') op.drop_table('gclient_provider_programs', schema='a76')
op.drop_table('gclient_provider_address', schema='a76') op.drop_table('gclient_provider_address', schema='a76')
op.drop_table('gparts', schema='a76') op.drop_table('parts', schema='a76')
op.drop_table('gclasses', schema='a76') op.drop_table('classes', schema='a76')
op.drop_table('gcompany', schema='a76') op.drop_table('gcompany', schema='a76')
op.drop_table('gclient_provider', schema='a76') op.drop_table('client_provider', schema='a76')

View File

@@ -1,355 +0,0 @@
"""create_a76_tables_company_clients_parts_classes
Revision ID: eb8a17e5fbde
Revises: 7937209f9718
Create Date: 2025-11-06 03:15:17.248159
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'eb8a17e5fbde'
down_revision: Union[str, Sequence[str], None] = '7937209f9718'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema - Create new A76 tables only."""
# Crear tabla gcompany
op.create_table('gcompany',
sa.Column('id', sa.String(length=3), nullable=False),
sa.Column('consecutive', sa.Boolean(), nullable=False),
sa.Column('name', sa.String(length=255), nullable=True),
sa.Column('rfc', sa.String(length=30), nullable=True),
sa.Column('main_activity', sa.String(length=255), nullable=True),
sa.Column('program', sa.String(length=10), nullable=True),
sa.Column('program_number', sa.String(length=40), nullable=True),
sa.Column('prosec', sa.SmallInteger(), nullable=True),
sa.Column('prosec_authorization', sa.String(length=20), nullable=True),
sa.Column('manufacturer_id', sa.String(length=25), nullable=True),
sa.Column('broker_company', sa.String(length=10), nullable=True),
sa.Column('responsible', sa.String(length=80), nullable=True),
sa.Column('responsible_name', sa.String(length=20), nullable=True),
sa.Column('responsible_last_name', sa.String(length=20), nullable=True),
sa.Column('responsible_mother_last_name', sa.String(length=20), nullable=True),
sa.Column('responsible_rfc', sa.String(length=30), nullable=True),
sa.Column('position', sa.String(length=30), nullable=True),
sa.Column('logo', sa.String(length=255), nullable=True),
sa.Column('has_express_line', sa.Boolean(), nullable=True),
sa.Column('order_format_type', sa.String(length=19), nullable=True),
sa.Column('previous_code', sa.SmallInteger(), nullable=True),
sa.Column('is_service_company', sa.Boolean(), nullable=True),
sa.Column('client_name', sa.String(length=300), nullable=True),
sa.Column('subassembly_mode', sa.String(length=7), nullable=True),
sa.Column('curp', sa.String(length=19), nullable=True),
sa.Column('inter_db_name', sa.String(length=100), nullable=True),
sa.Column('ctpat_svi', sa.String(length=100), nullable=True),
sa.Column('trusted_exporter_number', sa.String(length=50), nullable=True),
sa.Column('prevalidator_key', sa.String(length=20), nullable=True),
sa.Column('seventh_amendment', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('consecutive'),
schema='a76'
)
# Crear tabla gclient_provider
op.create_table('gclient_provider',
sa.Column('client_id', sa.String(length=8), nullable=False),
sa.Column('type_nat_foreign', sa.String(length=1), nullable=True),
sa.Column('name', sa.String(length=256), nullable=True),
sa.Column('short_name', sa.String(length=10), nullable=True),
sa.Column('rfc', sa.String(length=30), nullable=True),
sa.Column('curp', sa.String(length=19), nullable=True),
sa.Column('client_or_provider', sa.String(length=1), nullable=True),
sa.Column('linking', sa.String(length=1), nullable=True),
sa.Column('transform_subassembly', sa.String(length=1), nullable=True),
sa.Column('extra_information', sa.String(length=399), nullable=True),
sa.Column('web_key', sa.String(length=40), nullable=True),
sa.Column('responsible', sa.String(length=80), nullable=True),
sa.Column('position', sa.String(length=30), nullable=True),
sa.Column('incoterm', sa.String(length=19), nullable=True),
sa.Column('is_national_provider', sa.String(length=2), nullable=True),
sa.Column('enabled_disabled', sa.SmallInteger(), nullable=True),
sa.PrimaryKeyConstraint('client_id'),
schema='a76'
)
# Crear tabla gclasses
op.create_table('gclasses',
sa.Column('client_key', sa.Integer(), nullable=False),
sa.Column('class_code', sa.String(length=8), nullable=False),
sa.Column('description_spanish', sa.String(length=500), nullable=True),
sa.Column('description_english', sa.String(length=500), nullable=True),
sa.Column('material_key', sa.String(length=10), nullable=True),
sa.Column('unit_of_measure', sa.String(length=5), nullable=True),
sa.Column('fraction', sa.String(length=10), nullable=True),
sa.Column('us_fraction', sa.String(length=16), nullable=True),
sa.Column('sub_key', sa.String(length=5), nullable=True),
sa.Column('physical_review', sa.SmallInteger(), nullable=True),
sa.Column('iva_exempt_fraction', sa.String(length=4), nullable=True),
sa.ForeignKeyConstraint(['material_key'], ['public.material_types.key'], ),
sa.PrimaryKeyConstraint('client_key', 'class_code'),
schema='a76'
)
# Crear tabla gparts
op.create_table('gparts',
sa.Column('client_key', sa.Integer(), nullable=False),
sa.Column('part_number', sa.String(length=49), nullable=False),
sa.Column('fraction', sa.String(length=10), nullable=True),
sa.Column('description_spanish', sa.String(length=500), nullable=True),
sa.Column('description_english', sa.String(length=500), nullable=True),
sa.Column('part_class', sa.String(length=8), nullable=True),
sa.Column('unit_of_measure', sa.String(length=5), nullable=True),
sa.Column('commercial_part_number', sa.String(length=70), nullable=True),
sa.Column('country_of_origin', sa.String(length=3), nullable=True),
sa.Column('unit_cost', sa.Numeric(precision=23, scale=8), nullable=True),
sa.Column('currency_type', sa.String(length=2), nullable=True),
sa.Column('currency_key', sa.String(length=3), nullable=True),
sa.Column('unit_weight', sa.Numeric(precision=19, scale=8), nullable=True),
sa.Column('weight_type', sa.String(length=6), nullable=True),
sa.Column('us_fraction', sa.String(length=16), nullable=True),
sa.Column('fda_key', sa.String(length=20), nullable=True),
sa.Column('fcc_key', sa.String(length=30), nullable=True),
sa.Column('license_code', sa.String(length=3), nullable=True),
sa.Column('eccn', sa.String(length=20), nullable=True),
sa.Column('export_code', sa.String(length=2), nullable=True),
sa.Column('exclusion_symbol', sa.String(length=19), nullable=True),
sa.Column('supplier', sa.String(length=14), nullable=True),
sa.Column('alternate_unit_measure', sa.String(length=14), nullable=True),
sa.Column('added_value', sa.Numeric(precision=23, scale=8), nullable=True),
sa.Column('enabled_disabled', sa.SmallInteger(), nullable=True),
sa.Column('creation_date', sa.Integer(), nullable=True),
sa.Column('modification_date', sa.Integer(), nullable=True),
sa.Column('modification_date_iso', sa.DateTime(timezone=True), nullable=True),
sa.Column('part_photo', sa.String(length=255), nullable=True),
sa.ForeignKeyConstraint(['country_of_origin'], ['public.countries.m3_key'], ),
sa.ForeignKeyConstraint(['currency_key'], ['public.currency_types.code'], ),
sa.PrimaryKeyConstraint('client_key', 'part_number'),
schema='a76'
)
# Crear tabla gclient_provider_address
op.create_table('gclient_provider_address',
sa.Column('client_id', sa.String(length=8), nullable=False),
sa.Column('municipality', sa.String(length=150), nullable=True),
sa.Column('streets', sa.String(length=100), nullable=True),
sa.Column('neighborhood', sa.String(length=40), nullable=True),
sa.Column('interior_number', sa.String(length=20), nullable=True),
sa.Column('exterior_number', sa.String(length=20), nullable=True),
sa.Column('postal_code', sa.String(length=15), nullable=True),
sa.Column('city', sa.String(length=30), nullable=True),
sa.Column('state', sa.String(length=30), nullable=True),
sa.Column('country', sa.String(length=3), nullable=True),
sa.Column('phone', sa.String(length=30), nullable=True),
sa.Column('fax_number', sa.String(length=30), nullable=True),
sa.Column('email', sa.String(length=100), nullable=True),
sa.Column('contact', sa.String(length=50), nullable=True),
sa.Column('reference', sa.String(length=250), nullable=True),
sa.ForeignKeyConstraint(['client_id'], ['a76.gclient_provider.client_id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('client_id'),
schema='a76'
)
# Crear tabla gclient_provider_programs
op.create_table('gclient_provider_programs',
sa.Column('client_id', sa.String(length=8), nullable=False),
sa.Column('program', sa.String(length=7), nullable=True),
sa.Column('program_number', sa.String(length=40), nullable=True),
sa.Column('prosec', sa.SmallInteger(), nullable=True),
sa.Column('prosec_authorization', sa.String(length=20), nullable=True),
sa.Column('secon_auth_date', sa.Integer(), nullable=True),
sa.Column('manufacturer_id', sa.String(length=25), nullable=True),
sa.Column('tax_id', sa.String(length=30), nullable=True),
sa.Column('broker', sa.String(length=6), nullable=True),
sa.Column('import_broker', sa.String(length=6), nullable=True),
sa.Column('transfer_key', sa.String(length=8), nullable=True),
sa.Column('secon_authorization', sa.String(length=20), nullable=True),
sa.Column('applied_proportion', sa.Numeric(precision=7, scale=2), nullable=True),
sa.Column('is_certified_company', sa.String(length=1), nullable=True),
sa.Column('certified_company_registry', sa.String(length=40), nullable=True),
sa.Column('donation_auth_number', sa.String(length=50), nullable=True),
sa.Column('ctpat_svi', sa.String(length=100), nullable=True),
sa.Column('tax_registry_number', sa.String(length=40), nullable=True),
sa.Column('subassembly_service', sa.SmallInteger(), nullable=True),
sa.Column('autse_dates', sa.Integer(), nullable=True),
sa.Column('autse_number', sa.String(length=300), nullable=True),
sa.ForeignKeyConstraint(['client_id'], ['a76.gclient_provider.client_id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('client_id'),
schema='a76'
)
op.create_table('gclasses',
sa.Column('client_key', sa.Integer(), nullable=False),
sa.Column('class_code', sa.String(length=8), nullable=False),
sa.Column('description_spanish', sa.String(length=500), nullable=True),
sa.Column('description_english', sa.String(length=500), nullable=True),
sa.Column('material_key', sa.String(length=10), nullable=True),
sa.Column('unit_of_measure', sa.String(length=5), nullable=True),
sa.Column('fraction', sa.String(length=10), nullable=True),
sa.Column('us_fraction', sa.String(length=16), nullable=True),
sa.Column('sub_key', sa.String(length=5), nullable=True),
sa.Column('physical_review', sa.SmallInteger(), nullable=True),
sa.Column('iva_exempt_fraction', sa.String(length=4), nullable=True),
sa.ForeignKeyConstraint(['material_key'], ['public.material_types.key'], ),
sa.PrimaryKeyConstraint('client_key', 'class_code'),
schema='a76'
)
op.create_table('gclient_provider_address',
sa.Column('client_id', sa.String(length=8), nullable=False),
sa.Column('municipality', sa.String(length=150), nullable=True),
sa.Column('streets', sa.String(length=100), nullable=True),
sa.Column('neighborhood', sa.String(length=40), nullable=True),
sa.Column('interior_number', sa.String(length=20), nullable=True),
sa.Column('exterior_number', sa.String(length=20), nullable=True),
sa.Column('postal_code', sa.String(length=15), nullable=True),
sa.Column('city', sa.String(length=30), nullable=True),
sa.Column('state', sa.String(length=30), nullable=True),
sa.Column('country', sa.String(length=3), nullable=True),
sa.Column('phone', sa.String(length=30), nullable=True),
sa.Column('fax_number', sa.String(length=30), nullable=True),
sa.Column('email', sa.String(length=100), nullable=True),
sa.Column('contact', sa.String(length=50), nullable=True),
sa.Column('reference', sa.String(length=250), nullable=True),
sa.ForeignKeyConstraint(['client_id'], ['a76.gclient_provider.client_id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('client_id'),
schema='a76'
)
op.create_table('gclient_provider_programs',
sa.Column('client_id', sa.String(length=8), nullable=False),
sa.Column('program', sa.String(length=7), nullable=True),
sa.Column('program_number', sa.String(length=40), nullable=True),
sa.Column('prosec', sa.SmallInteger(), nullable=True),
sa.Column('prosec_authorization', sa.String(length=20), nullable=True),
sa.Column('secon_auth_date', sa.Integer(), nullable=True),
sa.Column('manufacturer_id', sa.String(length=25), nullable=True),
sa.Column('tax_id', sa.String(length=30), nullable=True),
sa.Column('broker', sa.String(length=6), nullable=True),
sa.Column('import_broker', sa.String(length=6), nullable=True),
sa.Column('transfer_key', sa.String(length=8), nullable=True),
sa.Column('secon_authorization', sa.String(length=20), nullable=True),
sa.Column('applied_proportion', sa.Numeric(precision=7, scale=2), nullable=True),
sa.Column('is_certified_company', sa.String(length=1), nullable=True),
sa.Column('certified_company_registry', sa.String(length=40), nullable=True),
sa.Column('donation_auth_number', sa.String(length=50), nullable=True),
sa.Column('ctpat_svi', sa.String(length=100), nullable=True),
sa.Column('tax_registry_number', sa.String(length=40), nullable=True),
sa.Column('subassembly_service', sa.SmallInteger(), nullable=True),
sa.Column('autse_dates', sa.Integer(), nullable=True),
sa.Column('autse_number', sa.String(length=300), nullable=True),
sa.ForeignKeyConstraint(['client_id'], ['a76.gclient_provider.client_id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('client_id'),
schema='a76'
)
op.create_table('gparts',
sa.Column('client_key', sa.Integer(), nullable=False),
sa.Column('part_number', sa.String(length=49), nullable=False),
sa.Column('fraction', sa.String(length=10), nullable=True),
sa.Column('description_spanish', sa.String(length=500), nullable=True),
sa.Column('description_english', sa.String(length=500), nullable=True),
sa.Column('part_class', sa.String(length=8), nullable=True),
sa.Column('unit_of_measure', sa.String(length=5), nullable=True),
sa.Column('commercial_part_number', sa.String(length=70), nullable=True),
sa.Column('country_of_origin', sa.String(length=3), nullable=True),
sa.Column('unit_cost', sa.Numeric(precision=23, scale=8), nullable=True),
sa.Column('currency_type', sa.String(length=2), nullable=True),
sa.Column('currency_key', sa.String(length=3), nullable=True),
sa.Column('unit_weight', sa.Numeric(precision=19, scale=8), nullable=True),
sa.Column('weight_type', sa.String(length=6), nullable=True),
sa.Column('us_fraction', sa.String(length=16), nullable=True),
sa.Column('fda_key', sa.String(length=20), nullable=True),
sa.Column('fcc_key', sa.String(length=30), nullable=True),
sa.Column('license_code', sa.String(length=3), nullable=True),
sa.Column('eccn', sa.String(length=20), nullable=True),
sa.Column('export_code', sa.String(length=2), nullable=True),
sa.Column('exclusion_symbol', sa.String(length=19), nullable=True),
sa.Column('supplier', sa.String(length=14), nullable=True),
sa.Column('alternate_unit_measure', sa.String(length=14), nullable=True),
sa.Column('added_value', sa.Numeric(precision=23, scale=8), nullable=True),
sa.Column('enabled_disabled', sa.SmallInteger(), nullable=True),
sa.Column('creation_date', sa.Integer(), nullable=True),
sa.Column('modification_date', sa.Integer(), nullable=True),
sa.Column('modification_date_iso', sa.DateTime(timezone=True), nullable=True),
sa.Column('part_photo', sa.String(length=255), nullable=True),
sa.ForeignKeyConstraint(['country_of_origin'], ['public.countries.m3_key'], ),
sa.ForeignKeyConstraint(['currency_key'], ['public.currency_types.code'], ),
sa.PrimaryKeyConstraint('client_key', 'part_number'),
schema='a76'
)
op.create_table('license_usage',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('tenant_id', sa.Integer(), nullable=False),
sa.Column('period_start', sa.DateTime(timezone=True), nullable=False),
sa.Column('period_end', sa.DateTime(timezone=True), nullable=False),
sa.Column('active_users', sa.Integer(), nullable=True),
sa.Column('storage_used_gb', sa.Integer(), nullable=True),
sa.Column('operations_count', sa.Integer(), nullable=True),
sa.Column('api_calls_count', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ),
sa.PrimaryKeyConstraint('id'),
schema='a76'
)
op.create_index(op.f('ix_a76_license_usage_id'), 'license_usage', ['id'], unique=False, schema='a76')
op.create_index(op.f('ix_a76_license_usage_tenant_id'), 'license_usage', ['tenant_id'], unique=False, schema='a76')
op.create_table('licenses',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('tenant_id', sa.Integer(), nullable=False),
sa.Column('plan', sa.Enum('FREE', 'BASIC', 'PROFESSIONAL', 'ENTERPRISE', name='licenseplan'), nullable=False),
sa.Column('status', sa.Enum('ACTIVE', 'EXPIRED', 'SUSPENDED', 'PENDING', 'CANCELLED', name='licensestatus'), nullable=False),
sa.Column('max_users', sa.Integer(), nullable=False),
sa.Column('max_storage_gb', sa.Integer(), nullable=False),
sa.Column('max_monthly_operations', sa.Integer(), nullable=False),
sa.Column('feature_api_access', sa.Boolean(), nullable=True),
sa.Column('feature_advanced_reports', sa.Boolean(), nullable=True),
sa.Column('feature_integrations', sa.Boolean(), nullable=True),
sa.Column('feature_dedicated_support', sa.Boolean(), nullable=True),
sa.Column('starts_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False),
sa.ForeignKeyConstraint(['tenant_id'], ['a76.tenants.id'], ),
sa.PrimaryKeyConstraint('id'),
schema='a76'
)
op.create_index(op.f('ix_a76_licenses_id'), 'licenses', ['id'], unique=False, schema='a76')
op.create_index(op.f('ix_a76_licenses_tenant_id'), 'licenses', ['tenant_id'], unique=True, schema='a76')
op.drop_constraint(op.f('fk_regimenped'), 'code_pedimento_regimens', type_='foreignkey')
op.drop_constraint(op.f('fk_codeped'), 'code_pedimento_regimens', type_='foreignkey')
op.create_foreign_key('fk_regimenped', 'code_pedimento_regimens', 'pedimento_regimens', ['regimen_code'], ['code'], source_schema='public', referent_schema='public')
op.create_foreign_key('fk_codeped', 'code_pedimento_regimens', 'pedimento_codes', ['pedimento_code'], ['code'], source_schema='public', referent_schema='public')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint('fk_codeped', 'code_pedimento_regimens', schema='public', type_='foreignkey')
op.drop_constraint('fk_regimenped', 'code_pedimento_regimens', schema='public', type_='foreignkey')
op.create_foreign_key(op.f('fk_codeped'), 'code_pedimento_regimens', 'pedimento_codes', ['pedimento_code'], ['code'])
op.create_foreign_key(op.f('fk_regimenped'), 'code_pedimento_regimens', 'pedimento_regimens', ['regimen_code'], ['code'])
op.drop_index(op.f('ix_a76_licenses_tenant_id'), table_name='licenses', schema='a76')
op.drop_index(op.f('ix_a76_licenses_id'), table_name='licenses', schema='a76')
op.drop_table('licenses', schema='a76')
op.drop_index(op.f('ix_a76_license_usage_tenant_id'), table_name='license_usage', schema='a76')
op.drop_index(op.f('ix_a76_license_usage_id'), table_name='license_usage', schema='a76')
op.drop_table('license_usage', schema='a76')
op.drop_table('gparts', schema='a76')
op.drop_table('gclient_provider_programs', schema='a76')
op.drop_table('gclient_provider_address', schema='a76')
op.drop_table('gclasses', schema='a76')
op.drop_index(op.f('ix_a76_tenants_slug'), table_name='tenants', schema='a76')
op.drop_index(op.f('ix_a76_tenants_name'), table_name='tenants', schema='a76')
op.drop_index(op.f('ix_a76_tenants_id'), table_name='tenants', schema='a76')
op.drop_table('tenants', schema='a76')
op.drop_table('gcompany', schema='a76')
op.drop_table('gclient_provider', schema='a76')
# ### end Alembic commands ###

View File

@@ -1,5 +1,5 @@
""" """
Módulo de GClass Módulo de Class
""" """
from .routes import router from .routes import router

View File

@@ -11,15 +11,15 @@ import enum
from typing import TYPE_CHECKING, List, Optional from typing import TYPE_CHECKING, List, Optional
if TYPE_CHECKING: if TYPE_CHECKING:
from api.v1.modules.a76.GParts.models import GPart from api.v1.modules.a76.parts.models import Part
from api.v1.modules.public.reference_data.material_types.models import MaterialType from api.v1.modules.public.reference_data.material_types.models import MaterialType
class GClass(Base): class Class(Base):
""" """
Modelo para la tabla GClases - Información de clases en sistemas SCAII y SCAF Modelo para la tabla GClases - Información de clases en sistemas SCAII y SCAF
""" """
__tablename__ = "gclasses" __tablename__ = "classes"
__table_args__ = {"schema": "a76"} __table_args__ = {"schema": "a76"}
# Primary key compuesta # Primary key compuesta
@@ -48,14 +48,14 @@ class GClass(Base):
# Inverse relationship with GParts that have this class # Inverse relationship with GParts that have this class
parts = relationship( parts = relationship(
"GPart", "Part",
primaryjoin="and_(GClass.client_key == GPart.client_key, GClass.class_code == GPart.part_class)", primaryjoin="and_(Class.client_key == Part.client_key, Class.class_code == Part.part_class)",
foreign_keys="[GPart.client_key, GPart.part_class]", foreign_keys="[Part.client_key, Part.part_class]",
viewonly=True, viewonly=True,
back_populates="part_class_info" back_populates="part_class_info"
) )
def __repr__(self): def __repr__(self):
return f"<GClass(client_key={self.client_key}, class_code='{self.class_code}', description='{self.description_spanish}')>" return f"<Class(client_key={self.client_key}, class_code='{self.class_code}', description='{self.description_spanish}')>"

View File

@@ -8,7 +8,7 @@ from fastapi import HTTPException
from typing import List, Optional from typing import List, Optional
import logging import logging
from .models import GClass from .models import Class
from .dto import ( from .dto import (
ClassCreateDTO, ClassCreateDTO,
ClassUpdateDTO, ClassUpdateDTO,
@@ -42,10 +42,10 @@ class ClassService:
""" """
try: try:
# Verificar que no exista la clase # Verificar que no exista la clase
existing = self.db.query(GClass).filter( existing = self.db.query(Class).filter(
and_( and_(
GClass.client_key == class_data.client_key, Class.client_key == class_data.client_key,
GClass.class_code == class_data.class_code Class.class_code == class_data.class_code
) )
).first() ).first()
@@ -56,7 +56,7 @@ class ClassService:
) )
# Crear clase # Crear clase
db_class = GClass( db_class = Class(
client_key=class_data.client_key, client_key=class_data.client_key,
class_code=class_data.class_code, class_code=class_data.class_code,
description_spanish=class_data.description_spanish, description_spanish=class_data.description_spanish,
@@ -100,10 +100,10 @@ class ClassService:
Returns: Returns:
ClassResponseDTO o None si no existe ClassResponseDTO o None si no existe
""" """
class_obj = self.db.query(GClass).filter( class_obj = self.db.query(Class).filter(
and_( and_(
GClass.client_key == client_key, Class.client_key == client_key,
GClass.class_code == class_code Class.class_code == class_code
) )
).first() ).first()
@@ -128,33 +128,33 @@ class ClassService:
Returns: Returns:
ClassListDTO con la lista paginada ClassListDTO con la lista paginada
""" """
query = self.db.query(GClass) query = self.db.query(Class)
# Aplicar filtros si se proporcionan # Aplicar filtros si se proporcionan
if search_params: if search_params:
if search_params.client_key: if search_params.client_key:
query = query.filter(GClass.client_key == search_params.client_key) query = query.filter(Class.client_key == search_params.client_key)
if search_params.class_code: if search_params.class_code:
query = query.filter(GClass.class_code.ilike(f"%{search_params.class_code}%")) query = query.filter(Class.class_code.ilike(f"%{search_params.class_code}%"))
if search_params.description: if search_params.description:
description_pattern = f"%{search_params.description}%" description_pattern = f"%{search_params.description}%"
query = query.filter( query = query.filter(
or_( or_(
GClass.description_spanish.ilike(description_pattern), Class.description_spanish.ilike(description_pattern),
GClass.description_english.ilike(description_pattern) Class.description_english.ilike(description_pattern)
) )
) )
if search_params.material_key: if search_params.material_key:
query = query.filter(GClass.material_key.ilike(f"%{search_params.material_key}%")) query = query.filter(Class.material_key.ilike(f"%{search_params.material_key}%"))
if search_params.fraction: if search_params.fraction:
query = query.filter(GClass.fraction.ilike(f"%{search_params.fraction}%")) query = query.filter(Class.fraction.ilike(f"%{search_params.fraction}%"))
if search_params.physical_review is not None: if search_params.physical_review is not None:
query = query.filter(GClass.physical_review == search_params.physical_review) query = query.filter(Class.physical_review == search_params.physical_review)
# Contar total # Contar total
total = query.count() total = query.count()
@@ -184,10 +184,10 @@ class ClassService:
Returns: Returns:
ClassResponseDTO actualizado o None si no existe ClassResponseDTO actualizado o None si no existe
""" """
class_obj = self.db.query(GClass).filter( class_obj = self.db.query(Class).filter(
and_( and_(
GClass.client_key == client_key, Class.client_key == client_key,
GClass.class_code == class_code Class.class_code == class_code
) )
).first() ).first()
@@ -222,10 +222,10 @@ class ClassService:
Returns: Returns:
True si se eliminó, False si no existe True si se eliminó, False si no existe
""" """
class_obj = self.db.query(GClass).filter( class_obj = self.db.query(Class).filter(
and_( and_(
GClass.client_key == client_key, Class.client_key == client_key,
GClass.class_code == class_code Class.class_code == class_code
) )
).first() ).first()
@@ -244,40 +244,40 @@ class ClassService:
def search_by_fraction(self, fraction: str) -> List[ClassBasicDTO]: def search_by_fraction(self, fraction: str) -> List[ClassBasicDTO]:
"""Busca clases por fracción arancelaria""" """Busca clases por fracción arancelaria"""
classes = self.db.query(GClass).filter(GClass.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] return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes]
def search_by_client(self, client_key: int, skip: int = 0, limit: int = 100) -> List[ClassBasicDTO]: def search_by_client(self, client_key: int, skip: int = 0, limit: int = 100) -> List[ClassBasicDTO]:
"""Obtiene todas las clases de un cliente específico""" """Obtiene todas las clases de un cliente específico"""
classes = self.db.query(GClass).filter(GClass.client_key == client_key).offset(skip).limit(limit).all() classes = self.db.query(Class).filter(Class.client_key == client_key).offset(skip).limit(limit).all()
return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes] return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes]
def search_by_material(self, material_key: str) -> List[ClassBasicDTO]: def search_by_material(self, material_key: str) -> List[ClassBasicDTO]:
"""Busca clases por clave de material""" """Busca clases por clave de material"""
classes = self.db.query(GClass).filter(GClass.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] 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""" """Obtiene clases por indicador de revisión física"""
classes = self.db.query(GClass).filter(GClass.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] return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes]
def get_classes_statistics(self) -> dict: def get_classes_statistics(self) -> dict:
"""Obtiene estadísticas básicas de clases""" """Obtiene estadísticas básicas de clases"""
total_classes = self.db.query(GClass).count() total_classes = self.db.query(Class).count()
# Contar por clientes # Contar por clientes
clients_count = self.db.query(GClass.client_key).distinct().count() clients_count = self.db.query(Class.client_key).distinct().count()
# Contar por revisión física # Contar por revisión física
physical_review_stats = {} physical_review_stats = {}
for i in range(3): # Asumiendo valores 0, 1, 2 for i in range(3): # Asumiendo valores 0, 1, 2
count = self.db.query(GClass).filter(GClass.physical_review == i).count() count = self.db.query(Class).filter(Class.physical_review == i).count()
physical_review_stats[f"physical_review_{i}"] = count physical_review_stats[f"physical_review_{i}"] = count
# Contar clases con fracciones # Contar clases con fracciones
with_fraction = self.db.query(GClass).filter(GClass.fraction.isnot(None)).count() with_fraction = self.db.query(Class).filter(Class.fraction.isnot(None)).count()
with_us_fraction = self.db.query(GClass).filter(GClass.us_fraction.isnot(None)).count() with_us_fraction = self.db.query(Class).filter(Class.us_fraction.isnot(None)).count()
return { return {
"total_classes": total_classes, "total_classes": total_classes,
@@ -289,6 +289,6 @@ class ClassService:
def get_classes_by_unit_measure(self, unit_of_measure: str) -> List[ClassBasicDTO]: def get_classes_by_unit_measure(self, unit_of_measure: str) -> List[ClassBasicDTO]:
"""Obtiene clases por unidad de medida""" """Obtiene clases por unidad de medida"""
classes = self.db.query(GClass).filter(GClass.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] return [ClassBasicDTO.model_validate(class_obj) for class_obj in classes]

View File

@@ -8,11 +8,11 @@ from core.database import Base
import enum import enum
class GClientProvider(Base): class ClientProvider(Base):
""" """
Modelo para la tabla GClientesPro - Información de clientes y proveedores Modelo para la tabla GClientesPro - Información de clientes y proveedores
""" """
__tablename__ = "gclient_provider" __tablename__ = "client_provider"
__table_args__ = {"schema": "a76"} __table_args__ = {"schema": "a76"}
# Primary key # Primary key
@@ -48,7 +48,7 @@ class GClientProviderAddress(Base):
__table_args__ = {"schema": "a76"} __table_args__ = {"schema": "a76"}
# Primary key (foreign key) # Primary key (foreign key)
client_id = Column(String(8), ForeignKey('a76.gclient_provider.client_id', ondelete='CASCADE'), primary_key=True, nullable=False) client_id = Column(String(8), ForeignKey('a76.client_provider.client_id', ondelete='CASCADE'), primary_key=True, nullable=False)
# Address information # Address information
municipality = Column(String(150), nullable=True) municipality = Column(String(150), nullable=True)
@@ -67,7 +67,7 @@ class GClientProviderAddress(Base):
reference = Column(String(250), nullable=True) reference = Column(String(250), nullable=True)
# Relationship # Relationship
client_provider = relationship("GClientProvider", back_populates="address") client_provider = relationship("ClientProvider", back_populates="address")
class GClientProviderPrograms(Base): class GClientProviderPrograms(Base):
@@ -78,7 +78,7 @@ class GClientProviderPrograms(Base):
__table_args__ = {"schema": "a76"} __table_args__ = {"schema": "a76"}
# Primary key (foreign key) # Primary key (foreign key)
client_id = Column(String(8), ForeignKey('a76.gclient_provider.client_id', ondelete='CASCADE'), primary_key=True, nullable=False) client_id = Column(String(8), ForeignKey('a76.client_provider.client_id', ondelete='CASCADE'), primary_key=True, nullable=False)
# Program information # Program information
program = Column(String(7), nullable=True) program = Column(String(7), nullable=True)
@@ -103,6 +103,6 @@ class GClientProviderPrograms(Base):
autse_number = Column(String(300), nullable=True) autse_number = Column(String(300), nullable=True)
# Relationship # Relationship
client_provider = relationship("GClientProvider", back_populates="programs") client_provider = relationship("ClientProvider", back_populates="programs")

View File

@@ -16,7 +16,7 @@ from .dto import (
ClientProviderListDTO ClientProviderListDTO
) )
router = APIRouter(prefix="/clients-providers", tags=["Clients & Providers"]) 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)

View File

@@ -8,7 +8,7 @@ from fastapi import HTTPException
from typing import List, Optional from typing import List, Optional
import logging import logging
from .models import GClientProvider, GClientProviderAddress, GClientProviderPrograms from .models import ClientProvider, GClientProviderAddress, GClientProviderPrograms
from .dto import ( from .dto import (
ClientProviderCreateDTO, ClientProviderCreateDTO,
ClientProviderUpdateDTO, ClientProviderUpdateDTO,
@@ -43,12 +43,12 @@ class ClientProviderService:
""" """
try: try:
# Verificar que no exista el cliente # Verificar que no exista el cliente
existing = self.db.query(GClientProvider).filter(GClientProvider.client_id == client_data.client_id).first() existing = self.db.query(ClientProvider).filter(ClientProvider.client_id == client_data.client_id).first()
if existing: 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 # Crear cliente/proveedor principal
db_client = GClientProvider( db_client = ClientProvider(
client_id=client_data.client_id, client_id=client_data.client_id,
type_nat_foreign=client_data.type_nat_foreign, type_nat_foreign=client_data.type_nat_foreign,
name=client_data.name, name=client_data.name,
@@ -118,10 +118,10 @@ class ClientProviderService:
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""" """Método privado para obtener cliente con relaciones"""
client = self.db.query(GClientProvider).options( client = self.db.query(ClientProvider).options(
joinedload(GClientProvider.address), joinedload(ClientProvider.address),
joinedload(GClientProvider.programs) joinedload(ClientProvider.programs)
).filter(GClientProvider.client_id == client_id).first() ).filter(ClientProvider.client_id == client_id).first()
if not client: if not client:
return None return None
@@ -148,25 +148,25 @@ class ClientProviderService:
Returns: Returns:
ClientProviderListDTO con la lista paginada ClientProviderListDTO con la lista paginada
""" """
query = self.db.query(GClientProvider) query = self.db.query(ClientProvider)
# Aplicar filtros # Aplicar filtros
if search: if search:
search_pattern = f"%{search}%" search_pattern = f"%{search}%"
query = query.filter( query = query.filter(
or_( or_(
GClientProvider.name.ilike(search_pattern), ClientProvider.name.ilike(search_pattern),
GClientProvider.short_name.ilike(search_pattern), ClientProvider.short_name.ilike(search_pattern),
GClientProvider.rfc.ilike(search_pattern), ClientProvider.rfc.ilike(search_pattern),
GClientProvider.client_id.ilike(search_pattern) ClientProvider.client_id.ilike(search_pattern)
) )
) )
if client_or_provider: if client_or_provider:
query = query.filter(GClientProvider.client_or_provider == client_or_provider) query = query.filter(ClientProvider.client_or_provider == client_or_provider)
if enabled_only: if enabled_only:
query = query.filter(GClientProvider.enabled_disabled == 1) query = query.filter(ClientProvider.enabled_disabled == 1)
# Contar total # Contar total
total = query.count() total = query.count()
@@ -195,7 +195,7 @@ class ClientProviderService:
Returns: Returns:
ClientProviderResponseDTO actualizado o None si no existe ClientProviderResponseDTO actualizado o None si no existe
""" """
client = self.db.query(GClientProvider).filter(GClientProvider.client_id == client_id).first() client = self.db.query(ClientProvider).filter(ClientProvider.client_id == client_id).first()
if not client: if not client:
return None return None
@@ -257,7 +257,7 @@ class ClientProviderService:
Returns: Returns:
True si se eliminó, False si no existe True si se eliminó, False si no existe
""" """
client = self.db.query(GClientProvider).filter(GClientProvider.client_id == client_id).first() client = self.db.query(ClientProvider).filter(ClientProvider.client_id == client_id).first()
if not client: if not client:
return False return False
@@ -273,24 +273,24 @@ class ClientProviderService:
def get_clients_only(self, skip: int = 0, limit: int = 100) -> List[ClientProviderBasicDTO]: def get_clients_only(self, skip: int = 0, limit: int = 100) -> List[ClientProviderBasicDTO]:
"""Obtiene solo clientes (C)""" """Obtiene solo clientes (C)"""
query = self.db.query(GClientProvider).filter(GClientProvider.client_or_provider == 'C') query = self.db.query(ClientProvider).filter(ClientProvider.client_or_provider == 'C')
clients = query.offset(skip).limit(limit).all() clients = query.offset(skip).limit(limit).all()
return [ClientProviderBasicDTO.model_validate(client) for client in clients] 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)""" """Obtiene solo proveedores (P)"""
query = self.db.query(GClientProvider).filter(GClientProvider.client_or_provider == 'P') query = self.db.query(ClientProvider).filter(ClientProvider.client_or_provider == 'P')
providers = query.offset(skip).limit(limit).all() 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]: def search_by_rfc(self, rfc: str) -> List[ClientProviderBasicDTO]:
"""Busca clientes/proveedores por RFC""" """Busca clientes/proveedores por RFC"""
clients = self.db.query(GClientProvider).filter(GClientProvider.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] return [ClientProviderBasicDTO.model_validate(client) for client in clients]
def toggle_status(self, client_id: str) -> Optional[ClientProviderResponseDTO]: def toggle_status(self, client_id: str) -> Optional[ClientProviderResponseDTO]:
"""Cambia el estado habilitado/deshabilitado""" """Cambia el estado habilitado/deshabilitado"""
client = self.db.query(GClientProvider).filter(GClientProvider.client_id == client_id).first() client = self.db.query(ClientProvider).filter(ClientProvider.client_id == client_id).first()
if not client: if not client:
return None return None

View File

@@ -10,7 +10,7 @@ from core.security import get_current_user, has_role
from .service import CompanyService from .service import CompanyService
from .dto import CompanyCreateDTO, CompanyUpdateDTO, CompanyResponseDTO from .dto import CompanyCreateDTO, CompanyUpdateDTO, CompanyResponseDTO
router = APIRouter(prefix="/company", tags=["Company"]) 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)

View File

@@ -13,14 +13,14 @@ from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING: if TYPE_CHECKING:
from api.v1.modules.public.reference_data.countries.models import Country from api.v1.modules.public.reference_data.countries.models import Country
from api.v1.modules.public.reference_data.currency_types.models import CurrencyType from api.v1.modules.public.reference_data.currency_types.models import CurrencyType
from api.v1.modules.a76.GClass.models import GClass from api.v1.modules.a76.classes.models import Class
class GPart(Base): class Part(Base):
""" """
Modelo para la tabla GPartes - Información de partes en los sistemas SCAII (N), SCAF (S) Y WINSAAI (W) Modelo para la tabla GPartes - Información de partes en los sistemas SCAII (N), SCAF (S) Y WINSAAI (W)
""" """
__tablename__ = "gparts" __tablename__ = "parts"
__table_args__ = {"schema": "a76"} __table_args__ = {"schema": "a76"}
# Primary key compuesta # Primary key compuesta
@@ -72,17 +72,17 @@ class GPart(Base):
country = relationship("Country", foreign_keys=[country_of_origin]) country = relationship("Country", foreign_keys=[country_of_origin])
currency = relationship("CurrencyType", foreign_keys=[currency_key]) currency = relationship("CurrencyType", foreign_keys=[currency_key])
# Relationship with GClass through composite foreign key # Relationship with Class through composite foreign key
# Note: This requires both client_key and part_class to match client_key and class_code in GClass # Note: This requires both client_key and part_class to match client_key and class_code in Class
part_class_info = relationship( part_class_info = relationship(
"GClass", "Class",
primaryjoin="and_(GPart.client_key == GClass.client_key, GPart.part_class == GClass.class_code)", primaryjoin="and_(Part.client_key == Class.client_key, Part.part_class == Class.class_code)",
foreign_keys="[GPart.client_key, GPart.part_class]", foreign_keys="[Part.client_key, Part.part_class]",
viewonly=True, viewonly=True,
back_populates="parts" back_populates="parts"
) )
def __repr__(self): def __repr__(self):
return f"<GPart(client_key={self.client_key}, part_number='{self.part_number}', description='{self.description_spanish}')>" return f"<Part(client_key={self.client_key}, part_number='{self.part_number}', description='{self.description_spanish}')>"

View File

@@ -17,7 +17,7 @@ from .dto import (
PartSearchDTO PartSearchDTO
) )
router = APIRouter(prefix="/parts", tags=["Parts"]) router = APIRouter(prefix="/parts")
@router.post("/", response_model=PartResponseDTO, status_code=status.HTTP_201_CREATED) @router.post("/", response_model=PartResponseDTO, status_code=status.HTTP_201_CREATED)

View File

@@ -9,7 +9,7 @@ from typing import List, Optional
import logging import logging
from datetime import datetime from datetime import datetime
from .models import GPart from .models import Part
from .dto import PartCreateDTO, PartUpdateDTO from .dto import PartCreateDTO, PartUpdateDTO
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -21,12 +21,12 @@ class PartService:
""" """
@staticmethod @staticmethod
def create_part(db: Session, part_data: PartCreateDTO) -> GPart: def create_part(db: Session, part_data: PartCreateDTO) -> Part:
""" """
Crear una nueva parte Crear una nueva parte
""" """
try: try:
db_part = GPart(**part_data.model_dump()) db_part = Part(**part_data.model_dump())
db.add(db_part) db.add(db_part)
db.commit() db.commit()
db.refresh(db_part) db.refresh(db_part)
@@ -41,15 +41,15 @@ class PartService:
raise HTTPException(status_code=500, detail="Error creating part") raise HTTPException(status_code=500, detail="Error creating part")
@staticmethod @staticmethod
def get_part(db: Session, client_key: int, part_number: str) -> Optional[GPart]: def get_part(db: Session, client_key: int, part_number: str) -> Optional[Part]:
""" """
Obtener una parte por clave de cliente y número de parte Obtener una parte por clave de cliente y número de parte
""" """
try: try:
return db.query(GPart).filter( return db.query(Part).filter(
and_( and_(
GPart.client_key == client_key, Part.client_key == client_key,
GPart.part_number == part_number Part.part_number == part_number
) )
).first() ).first()
except Exception as e: except Exception as e:
@@ -65,29 +65,29 @@ class PartService:
client_key: Optional[int] = None, client_key: Optional[int] = None,
fraction: Optional[str] = None, fraction: Optional[str] = None,
country_of_origin: Optional[str] = None country_of_origin: Optional[str] = None
) -> tuple[List[GPart], int]: ) -> tuple[List[Part], int]:
""" """
Obtener partes con paginación y filtros Obtener partes con paginación y filtros
""" """
try: try:
query = db.query(GPart) query = db.query(Part)
# Aplicar filtros # Aplicar filtros
if search: if search:
query = query.filter(or_( query = query.filter(or_(
GPart.description_spanish.ilike(f"%{search}%"), Part.description_spanish.ilike(f"%{search}%"),
GPart.description_english.ilike(f"%{search}%"), Part.description_english.ilike(f"%{search}%"),
GPart.part_number.ilike(f"%{search}%") Part.part_number.ilike(f"%{search}%")
)) ))
if client_key is not None: if client_key is not None:
query = query.filter(GPart.client_key == client_key) query = query.filter(Part.client_key == client_key)
if fraction: if fraction:
query = query.filter(GPart.fraction == fraction) query = query.filter(Part.fraction == fraction)
if country_of_origin: if country_of_origin:
query = query.filter(GPart.country_of_origin == country_of_origin) query = query.filter(Part.country_of_origin == country_of_origin)
# Contar total # Contar total
total = query.count() total = query.count()
@@ -101,26 +101,26 @@ class PartService:
raise HTTPException(status_code=500, detail="Error retrieving parts") raise HTTPException(status_code=500, detail="Error retrieving parts")
@staticmethod @staticmethod
def get_parts_by_client(db: Session, client_key: int) -> List[GPart]: def get_parts_by_client(db: Session, client_key: int) -> List[Part]:
""" """
Obtener todas las partes de un cliente específico Obtener todas las partes de un cliente específico
""" """
try: try:
return db.query(GPart).filter(GPart.client_key == client_key).all() return db.query(Part).filter(Part.client_key == client_key).all()
except Exception as e: except Exception as e:
logger.error(f"Error getting parts by client: {e}") logger.error(f"Error getting parts by client: {e}")
raise HTTPException(status_code=500, detail="Error retrieving client parts") raise HTTPException(status_code=500, detail="Error retrieving client parts")
@staticmethod @staticmethod
def search_parts_by_fraction(db: Session, fraction: str) -> List[GPart]: def search_parts_by_fraction(db: Session, fraction: str) -> List[Part]:
""" """
Buscar partes por fracción arancelaria Buscar partes por fracción arancelaria
""" """
try: try:
return db.query(GPart).filter( return db.query(Part).filter(
or_( or_(
GPart.fraction.ilike(f"%{fraction}%"), Part.fraction.ilike(f"%{fraction}%"),
GPart.us_fraction.ilike(f"%{fraction}%") Part.us_fraction.ilike(f"%{fraction}%")
) )
).all() ).all()
except Exception as e: except Exception as e:
@@ -128,29 +128,29 @@ class PartService:
raise HTTPException(status_code=500, detail="Error searching parts by fraction") raise HTTPException(status_code=500, detail="Error searching parts by fraction")
@staticmethod @staticmethod
def search_parts_by_supplier(db: Session, supplier: str) -> List[GPart]: def search_parts_by_supplier(db: Session, supplier: str) -> List[Part]:
""" """
Buscar partes por proveedor Buscar partes por proveedor
""" """
try: try:
return db.query(GPart).filter(GPart.supplier.ilike(f"%{supplier}%")).all() return db.query(Part).filter(Part.supplier.ilike(f"%{supplier}%")).all()
except Exception as e: except Exception as e:
logger.error(f"Error searching parts by supplier: {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 @staticmethod
def search_parts_by_country(db: Session, country_code: str) -> List[GPart]: def search_parts_by_country(db: Session, country_code: str) -> List[Part]:
""" """
Buscar partes por país de origen Buscar partes por país de origen
""" """
try: try:
return db.query(GPart).filter(GPart.country_of_origin == country_code).all() return db.query(Part).filter(Part.country_of_origin == country_code).all()
except Exception as e: except Exception as e:
logger.error(f"Error searching parts by country: {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 @staticmethod
def update_part(db: Session, client_key: int, part_number: str, part_data: PartUpdateDTO) -> Optional[GPart]: def update_part(db: Session, client_key: int, part_number: str, part_data: PartUpdateDTO) -> Optional[Part]:
""" """
Actualizar una parte existente Actualizar una parte existente
""" """
@@ -190,7 +190,7 @@ class PartService:
raise HTTPException(status_code=500, detail="Error deleting part") raise HTTPException(status_code=500, detail="Error deleting part")
@staticmethod @staticmethod
def toggle_part_status(db: Session, client_key: int, part_number: str) -> Optional[GPart]: def toggle_part_status(db: Session, client_key: int, part_number: str) -> Optional[Part]:
""" """
Cambiar el estado habilitado/deshabilitado de una parte Cambiar el estado habilitado/deshabilitado de una parte
""" """
@@ -216,24 +216,24 @@ class PartService:
Obtener estadísticas de partes Obtener estadísticas de partes
""" """
try: try:
total_parts = db.query(GPart).count() total_parts = db.query(Part).count()
# Partes por cliente # Partes por cliente
parts_by_client = db.query( parts_by_client = db.query(
GPart.client_key, Part.client_key,
func.count(GPart.part_number).label('count') func.count(Part.part_number).label('count')
).group_by(GPart.client_key).all() ).group_by(Part.client_key).all()
# Partes por país de origen # Partes por país de origen
parts_by_country = db.query( parts_by_country = db.query(
GPart.country_of_origin, Part.country_of_origin,
func.count(GPart.part_number).label('count') func.count(Part.part_number).label('count')
).filter(GPart.country_of_origin.isnot(None))\ ).filter(Part.country_of_origin.isnot(None))\
.group_by(GPart.country_of_origin).all() .group_by(Part.country_of_origin).all()
# Partes habilitadas vs deshabilitadas # Partes habilitadas vs deshabilitadas
enabled_parts = db.query(GPart).filter(GPart.enabled_disabled == 1).count() enabled_parts = db.query(Part).filter(Part.enabled_disabled == 1).count()
disabled_parts = db.query(GPart).filter(GPart.enabled_disabled == 0).count() disabled_parts = db.query(Part).filter(Part.enabled_disabled == 0).count()
return { return {
"total_parts": total_parts, "total_parts": total_parts,

View File

@@ -9,6 +9,10 @@ from .auth import router as auth_router
from .tenants import router as tenants_router from .tenants import router as tenants_router
from .licenses import router as licenses_router from .licenses import router as licenses_router
from .pedmientos.router import router as pedimentos_router from .pedmientos.router import router as pedimentos_router
from .client_and_provider import router as client_and_provider_router
from .company import router as company_router
from .classes import router as classes_router
from .parts import router as parts_router
# Router principal # Router principal
router = APIRouter() router = APIRouter()
@@ -18,3 +22,8 @@ router.include_router(auth_router)
router.include_router(tenants_router, prefix="/a76", tags=["a76 / tenants"]) router.include_router(tenants_router, prefix="/a76", tags=["a76 / tenants"])
router.include_router(licenses_router, prefix="/a76", tags=["a76 / licenses"]) router.include_router(licenses_router, prefix="/a76", tags=["a76 / licenses"])
router.include_router(pedimentos_router, prefix="/a76") router.include_router(pedimentos_router, prefix="/a76")
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"])

View File

@@ -20,7 +20,7 @@
- Control de inventario y clasificación arancelaria - Control de inventario y clasificación arancelaria
- Información regulatoria y de cumplimiento - Información regulatoria y de cumplimiento
### Módulo de Clases (GClass) ### Módulo de Clases (Class)
- Clasificaciones para sistemas SCAII y SCAF - Clasificaciones para sistemas SCAII y SCAF
- Información arancelaria detallada - Información arancelaria detallada
- Gestión de fracciones arancelarias y materiales - Gestión de fracciones arancelarias y materiales
@@ -30,17 +30,17 @@
## 🔗 Relaciones de Base de Datos ## 🔗 Relaciones de Base de Datos
### Relaciones Principales ### Relaciones Principales
- **GPart ↔ GClass**: Relación de clave compuesta (client_key, part_class ↔ class_code) - **Part ↔ Class**: Relación de clave compuesta (client_key, part_class ↔ class_code)
- **GPart → Country**: Clave foránea a public.countries (country_of_origin) - **Part → Country**: Clave foránea a public.countries (country_of_origin)
- **GPart → CurrencyType**: Clave foránea a public.currency_types (currency_key) - **Part → CurrencyType**: Clave foránea a public.currency_types (currency_key)
- **GClass → MaterialType**: Clave foránea a public.material_types (material_key) - **Class → MaterialType**: Clave foránea a public.material_types (material_key)
### Esquema de Relaciones ### Esquema de Relaciones
``` ```
GPart (Partes) Part (Partes)
├── País de origen → Country ├── País de origen → Country
├── Tipo de moneda → CurrencyType ├── Tipo de moneda → CurrencyType
└── Información de clase → GClass └── Información de clase → Class
└── Tipo de material → MaterialType └── Tipo de material → MaterialType
``` ```

View File

@@ -2,8 +2,8 @@
## Resumen de Relaciones Establecidas ## Resumen de Relaciones Establecidas
### GPart (Tabla: gparts) ### Part (Tabla: parts)
El modelo `GPart` representa las partes/componentes en los sistemas SCAII, SCAF y WINSAAI. El modelo `Part` representa las partes/componentes en los sistemas SCAII, SCAF y WINSAAI.
#### Relaciones: #### Relaciones:
@@ -17,14 +17,14 @@ El modelo `GPart` representa las partes/componentes en los sistemas SCAII, SCAF
- Relación: Many-to-One - Relación: Many-to-One
- Propósito: Tipo de moneda para el costo unitario - Propósito: Tipo de moneda para el costo unitario
3. **Con GClass (gclasses)** 3. **Con Class (classes)**
- Campos: `(client_key, part_class)``(client_key, class_code)` - Campos: `(client_key, part_class)``(client_key, class_code)`
- Relación: Many-to-One (usando primaryjoin complejo) - Relación: Many-to-One (usando primaryjoin complejo)
- Propósito: Clasificación de la parte - Propósito: Clasificación de la parte
- Atributo: `part_class_info` - Atributo: `part_class_info`
### GClass (Tabla: gclasses) ### Class (Tabla: classes)
El modelo `GClass` representa las clases de clasificación en sistemas SCAII y SCAF. El modelo `Class` representa las clases de clasificación en sistemas SCAII y SCAF.
#### Relaciones: #### Relaciones:
@@ -33,24 +33,24 @@ El modelo `GClass` representa las clases de clasificación en sistemas SCAII y S
- Relación: Many-to-One - Relación: Many-to-One
- Propósito: Tipo de material de la clase - Propósito: Tipo de material de la clase
2. **Con GPart (gparts)** 2. **Con Part (parts)**
- Campos: `(client_key, class_code)``(client_key, part_class)` - Campos: `(client_key, class_code)``(client_key, part_class)`
- Relación: One-to-Many (inversa de la relación en GPart) - Relación: One-to-Many (inversa de la relación en Part)
- Propósito: Partes que pertenecen a esta clase - Propósito: Partes que pertenecen a esta clase
- Atributo: `parts` - Atributo: `parts`
## Esquema de Relaciones ## Esquema de Relaciones
``` ```
GPart Part
├── country (Country) # País de origen ├── country (Country) # País de origen
├── currency (CurrencyType) # Tipo de moneda ├── currency (CurrencyType) # Tipo de moneda
└── part_class_info (GClass) # Información de clasificación └── part_class_info (Class) # Información de clasificación
└── material_type (MaterialType) # Tipo de material └── material_type (MaterialType) # Tipo de material
GClass Class
├── material_type (MaterialType) # Tipo de material ├── material_type (MaterialType) # Tipo de material
└── parts (List[GPart]) # Partes que usan esta clase └── parts (List[Part]) # Partes que usan esta clase
``` ```
## Uso de las Relaciones ## Uso de las Relaciones
@@ -58,13 +58,13 @@ GClass
### En consultas: ### En consultas:
```python ```python
# Obtener una parte con su información completa # Obtener una parte con su información completa
part = session.query(GPart).options( part = session.query(Part).options(
joinedload(GPart.country), joinedload(Part.country),
joinedload(GPart.currency), joinedload(Part.currency),
joinedload(GPart.part_class_info).joinedload(GClass.material_type) joinedload(Part.part_class_info).joinedload(Class.material_type)
).filter( ).filter(
GPart.client_key == 1, Part.client_key == 1,
GPart.part_number == "PART001" Part.part_number == "PART001"
).first() ).first()
# Acceder a los datos relacionados # Acceder a los datos relacionados
@@ -89,7 +89,7 @@ class PartDetailResponseDTO(BaseModel):
## Consideraciones Técnicas ## Consideraciones Técnicas
1. **Composite Foreign Keys**: La relación entre `GPart` y `GClass` usa claves foráneas compuestas que requieren `primaryjoin` personalizado. 1. **Composite Foreign Keys**: La relación entre `Part` y `Class` usa claves foráneas compuestas que requieren `primaryjoin` personalizado.
2. **Viewonly Relationships**: Algunas relaciones están marcadas como `viewonly=True` para evitar problemas de escritura accidental. 2. **Viewonly Relationships**: Algunas relaciones están marcadas como `viewonly=True` para evitar problemas de escritura accidental.

View File

@@ -13,11 +13,11 @@ Se han actualizado todos los modelos en `api/v1/modules/a76/` para usar el schem
| Módulo | Tabla | Schema | Estado | | Módulo | Tabla | Schema | Estado |
|--------|-------|---------|---------| |--------|-------|---------|---------|
| **Company** | `gcompany` | `a76` | ✅ Actualizada | | **Company** | `gcompany` | `a76` | ✅ Actualizada |
| **Client & Provider** | `gclient_provider` | `a76` | ✅ Actualizada | | **Client & Provider** | `client_provider` | `a76` | ✅ Actualizada |
| **Client & Provider** | `gclient_provider_address` | `a76` | ✅ Actualizada | | **Client & Provider** | `gclient_provider_address` | `a76` | ✅ Actualizada |
| **Client & Provider** | `gclient_provider_programs` | `a76` | ✅ Actualizada | | **Client & Provider** | `gclient_provider_programs` | `a76` | ✅ Actualizada |
| **GParts** | `gparts` | `a76` | ✅ Actualizada | | **GParts** | `parts` | `a76` | ✅ Actualizada |
| **GClass** | `gclasses` | `a76` | ✅ Actualizada | | **Class** | `classes` | `a76` | ✅ Actualizada |
| **Licenses** | `licenses` | `a76` | ✅ Ya estaba | | **Licenses** | `licenses` | `a76` | ✅ Ya estaba |
| **Licenses** | `license_usage` | `a76` | ✅ Ya estaba | | **Licenses** | `license_usage` | `a76` | ✅ Ya estaba |
| **Tenants** | `tenants` | `a76` | ✅ Ya estaba | | **Tenants** | `tenants` | `a76` | ✅ Ya estaba |
@@ -39,10 +39,10 @@ class GCompany(Base):
#### 2. Foreign Keys Actualizadas #### 2. Foreign Keys Actualizadas
```python ```python
# ANTES # ANTES
client_id = Column(String(8), ForeignKey('gclient_provider.client_id'), ...) client_id = Column(String(8), ForeignKey('client_provider.client_id'), ...)
# DESPUÉS # DESPUÉS
client_id = Column(String(8), ForeignKey('a76.gclient_provider.client_id'), ...) client_id = Column(String(8), ForeignKey('a76.client_provider.client_id'), ...)
``` ```
### 🏗️ Estructura de Schemas ### 🏗️ Estructura de Schemas
@@ -60,11 +60,11 @@ PostgreSQL Database
├── licenses ├── licenses
├── license_usage ├── license_usage
├── gcompany ├── gcompany
├── gclient_provider ├── client_provider
├── gclient_provider_address ├── gclient_provider_address
├── gclient_provider_programs ├── gclient_provider_programs
├── gparts ├── parts
└── gclasses └── classes
``` ```
### 🔗 Relaciones Mantenidas ### 🔗 Relaciones Mantenidas
@@ -77,13 +77,13 @@ Las relaciones entre schemas funcionan correctamente:
#### Ejemplos de Relaciones Cross-Schema: #### Ejemplos de Relaciones Cross-Schema:
```python ```python
# GPart (a76) → Country (public) # Part (a76) → Country (public)
country_of_origin = Column(String(3), ForeignKey('public.countries.m3_key')) country_of_origin = Column(String(3), ForeignKey('public.countries.m3_key'))
# GPart (a76) → CurrencyType (public) # Part (a76) → CurrencyType (public)
currency_key = Column(String(3), ForeignKey('public.currency_types.code')) currency_key = Column(String(3), ForeignKey('public.currency_types.code'))
# GClass (a76) → MaterialType (public) # Class (a76) → MaterialType (public)
material_key = Column(String(10), ForeignKey('public.material_types.key')) material_key = Column(String(10), ForeignKey('public.material_types.key'))
``` ```