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/
lib64/
parts/
sdist/
var/
wheels/

View File

@@ -13,7 +13,7 @@ import sqlalchemy as sa
# revision identifiers, used by Alembic.
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
depends_on: Union[str, Sequence[str], None] = None

View File

@@ -22,7 +22,7 @@ def upgrade() -> None:
"""Upgrade schema - Create only new A76 tables."""
# 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('type_nat_foreign', sa.String(length=1), nullable=True),
sa.Column('name', sa.String(length=256), nullable=True),
@@ -81,7 +81,7 @@ def upgrade() -> None:
schema='a76'
)
op.create_table('gclasses',
op.create_table('classes',
sa.Column('client_key', sa.Integer(), nullable=False),
sa.Column('class_code', sa.String(length=8), nullable=False),
sa.Column('description_spanish', sa.String(length=500), nullable=True),
@@ -98,7 +98,7 @@ def upgrade() -> None:
schema='a76'
)
op.create_table('gparts',
op.create_table('parts',
sa.Column('client_key', sa.Integer(), nullable=False),
sa.Column('part_number', sa.String(length=49), nullable=False),
sa.Column('fraction', sa.String(length=10), nullable=True),
@@ -151,7 +151,7 @@ def upgrade() -> None:
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.ForeignKeyConstraint(['client_id'], ['a76.client_provider.client_id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('client_id'),
schema='a76'
)
@@ -178,7 +178,7 @@ def upgrade() -> None:
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.ForeignKeyConstraint(['client_id'], ['a76.client_provider.client_id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('client_id'),
schema='a76'
)
@@ -189,7 +189,7 @@ def downgrade() -> None:
# Drop tables in reverse dependency order
op.drop_table('gclient_provider_programs', schema='a76')
op.drop_table('gclient_provider_address', schema='a76')
op.drop_table('gparts', schema='a76')
op.drop_table('gclasses', schema='a76')
op.drop_table('parts', schema='a76')
op.drop_table('classes', 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

View File

@@ -11,15 +11,15 @@ import enum
from typing import TYPE_CHECKING, List, Optional
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
class GClass(Base):
class Class(Base):
"""
Modelo para la tabla GClases - Información de clases en sistemas SCAII y SCAF
"""
__tablename__ = "gclasses"
__tablename__ = "classes"
__table_args__ = {"schema": "a76"}
# Primary key compuesta
@@ -48,14 +48,14 @@ class GClass(Base):
# Inverse relationship with GParts that have this class
parts = relationship(
"GPart",
primaryjoin="and_(GClass.client_key == GPart.client_key, GClass.class_code == GPart.part_class)",
foreign_keys="[GPart.client_key, GPart.part_class]",
"Part",
primaryjoin="and_(Class.client_key == Part.client_key, Class.class_code == Part.part_class)",
foreign_keys="[Part.client_key, Part.part_class]",
viewonly=True,
back_populates="part_class_info"
)
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
import logging
from .models import GClass
from .models import Class
from .dto import (
ClassCreateDTO,
ClassUpdateDTO,
@@ -42,10 +42,10 @@ class ClassService:
"""
try:
# Verificar que no exista la clase
existing = self.db.query(GClass).filter(
existing = self.db.query(Class).filter(
and_(
GClass.client_key == class_data.client_key,
GClass.class_code == class_data.class_code
Class.client_key == class_data.client_key,
Class.class_code == class_data.class_code
)
).first()
@@ -56,7 +56,7 @@ class ClassService:
)
# Crear clase
db_class = GClass(
db_class = Class(
client_key=class_data.client_key,
class_code=class_data.class_code,
description_spanish=class_data.description_spanish,
@@ -100,10 +100,10 @@ class ClassService:
Returns:
ClassResponseDTO o None si no existe
"""
class_obj = self.db.query(GClass).filter(
class_obj = self.db.query(Class).filter(
and_(
GClass.client_key == client_key,
GClass.class_code == class_code
Class.client_key == client_key,
Class.class_code == class_code
)
).first()
@@ -128,33 +128,33 @@ class ClassService:
Returns:
ClassListDTO con la lista paginada
"""
query = self.db.query(GClass)
query = self.db.query(Class)
# Aplicar filtros si se proporcionan
if search_params:
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:
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:
description_pattern = f"%{search_params.description}%"
query = query.filter(
or_(
GClass.description_spanish.ilike(description_pattern),
GClass.description_english.ilike(description_pattern)
Class.description_spanish.ilike(description_pattern),
Class.description_english.ilike(description_pattern)
)
)
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:
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:
query = query.filter(GClass.physical_review == search_params.physical_review)
query = query.filter(Class.physical_review == search_params.physical_review)
# Contar total
total = query.count()
@@ -184,10 +184,10 @@ class ClassService:
Returns:
ClassResponseDTO actualizado o None si no existe
"""
class_obj = self.db.query(GClass).filter(
class_obj = self.db.query(Class).filter(
and_(
GClass.client_key == client_key,
GClass.class_code == class_code
Class.client_key == client_key,
Class.class_code == class_code
)
).first()
@@ -222,10 +222,10 @@ class ClassService:
Returns:
True si se eliminó, False si no existe
"""
class_obj = self.db.query(GClass).filter(
class_obj = self.db.query(Class).filter(
and_(
GClass.client_key == client_key,
GClass.class_code == class_code
Class.client_key == client_key,
Class.class_code == class_code
)
).first()
@@ -244,40 +244,40 @@ class ClassService:
def search_by_fraction(self, fraction: str) -> List[ClassBasicDTO]:
"""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]
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"""
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]
def search_by_material(self, material_key: str) -> List[ClassBasicDTO]:
"""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]
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(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]
def get_classes_statistics(self) -> dict:
"""Obtiene estadísticas básicas de clases"""
total_classes = self.db.query(GClass).count()
total_classes = self.db.query(Class).count()
# 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
physical_review_stats = {}
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
# Contar clases con fracciones
with_fraction = self.db.query(GClass).filter(GClass.fraction.isnot(None)).count()
with_us_fraction = self.db.query(GClass).filter(GClass.us_fraction.isnot(None)).count()
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()
return {
"total_classes": total_classes,
@@ -289,6 +289,6 @@ class ClassService:
def get_classes_by_unit_measure(self, unit_of_measure: str) -> List[ClassBasicDTO]:
"""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]

View File

@@ -8,11 +8,11 @@ from core.database import Base
import enum
class GClientProvider(Base):
class ClientProvider(Base):
"""
Modelo para la tabla GClientesPro - Información de clientes y proveedores
"""
__tablename__ = "gclient_provider"
__tablename__ = "client_provider"
__table_args__ = {"schema": "a76"}
# Primary key
@@ -48,7 +48,7 @@ class GClientProviderAddress(Base):
__table_args__ = {"schema": "a76"}
# 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
municipality = Column(String(150), nullable=True)
@@ -67,7 +67,7 @@ class GClientProviderAddress(Base):
reference = Column(String(250), nullable=True)
# Relationship
client_provider = relationship("GClientProvider", back_populates="address")
client_provider = relationship("ClientProvider", back_populates="address")
class GClientProviderPrograms(Base):
@@ -78,7 +78,7 @@ class GClientProviderPrograms(Base):
__table_args__ = {"schema": "a76"}
# 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 = Column(String(7), nullable=True)
@@ -103,6 +103,6 @@ class GClientProviderPrograms(Base):
autse_number = Column(String(300), nullable=True)
# 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
)
router = APIRouter(prefix="/clients-providers", tags=["Clients & Providers"])
router = APIRouter(prefix="/clients-providers")
@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
import logging
from .models import GClientProvider, GClientProviderAddress, GClientProviderPrograms
from .models import ClientProvider, GClientProviderAddress, GClientProviderPrograms
from .dto import (
ClientProviderCreateDTO,
ClientProviderUpdateDTO,
@@ -43,12 +43,12 @@ class ClientProviderService:
"""
try:
# 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:
raise HTTPException(status_code=400, detail=f"Client with ID '{client_data.client_id}' already exists")
# Crear cliente/proveedor principal
db_client = GClientProvider(
db_client = ClientProvider(
client_id=client_data.client_id,
type_nat_foreign=client_data.type_nat_foreign,
name=client_data.name,
@@ -118,10 +118,10 @@ class ClientProviderService:
def _get_client_with_relations(self, client_id: str) -> Optional[ClientProviderResponseDTO]:
"""Método privado para obtener cliente con relaciones"""
client = self.db.query(GClientProvider).options(
joinedload(GClientProvider.address),
joinedload(GClientProvider.programs)
).filter(GClientProvider.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
@@ -148,25 +148,25 @@ class ClientProviderService:
Returns:
ClientProviderListDTO con la lista paginada
"""
query = self.db.query(GClientProvider)
query = self.db.query(ClientProvider)
# Aplicar filtros
if search:
search_pattern = f"%{search}%"
query = query.filter(
or_(
GClientProvider.name.ilike(search_pattern),
GClientProvider.short_name.ilike(search_pattern),
GClientProvider.rfc.ilike(search_pattern),
GClientProvider.client_id.ilike(search_pattern)
ClientProvider.name.ilike(search_pattern),
ClientProvider.short_name.ilike(search_pattern),
ClientProvider.rfc.ilike(search_pattern),
ClientProvider.client_id.ilike(search_pattern)
)
)
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:
query = query.filter(GClientProvider.enabled_disabled == 1)
query = query.filter(ClientProvider.enabled_disabled == 1)
# Contar total
total = query.count()
@@ -195,7 +195,7 @@ class ClientProviderService:
Returns:
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:
return None
@@ -257,7 +257,7 @@ class ClientProviderService:
Returns:
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:
return False
@@ -273,24 +273,24 @@ class ClientProviderService:
def get_clients_only(self, skip: int = 0, limit: int = 100) -> List[ClientProviderBasicDTO]:
"""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()
return [ClientProviderBasicDTO.model_validate(client) for client in clients]
def get_providers_only(self, skip: int = 0, limit: int = 100) -> List[ClientProviderBasicDTO]:
"""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()
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(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]
def toggle_status(self, client_id: str) -> Optional[ClientProviderResponseDTO]:
"""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:
return None

View File

@@ -10,7 +10,7 @@ from core.security import get_current_user, has_role
from .service import CompanyService
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)

View File

@@ -13,14 +13,14 @@ from typing import TYPE_CHECKING, Optional
if TYPE_CHECKING:
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.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)
"""
__tablename__ = "gparts"
__tablename__ = "parts"
__table_args__ = {"schema": "a76"}
# Primary key compuesta
@@ -72,17 +72,17 @@ class GPart(Base):
country = relationship("Country", foreign_keys=[country_of_origin])
currency = relationship("CurrencyType", foreign_keys=[currency_key])
# Relationship with GClass through composite foreign key
# Note: This requires both client_key and part_class to match client_key and class_code in GClass
# Relationship with Class through composite foreign key
# Note: This requires both client_key and part_class to match client_key and class_code in Class
part_class_info = relationship(
"GClass",
primaryjoin="and_(GPart.client_key == GClass.client_key, GPart.part_class == GClass.class_code)",
foreign_keys="[GPart.client_key, GPart.part_class]",
"Class",
primaryjoin="and_(Part.client_key == Class.client_key, Part.part_class == Class.class_code)",
foreign_keys="[Part.client_key, Part.part_class]",
viewonly=True,
back_populates="parts"
)
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
)
router = APIRouter(prefix="/parts", tags=["Parts"])
router = APIRouter(prefix="/parts")
@router.post("/", response_model=PartResponseDTO, status_code=status.HTTP_201_CREATED)

View File

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

View File

@@ -9,6 +9,10 @@ from .auth import router as auth_router
from .tenants import router as tenants_router
from .licenses import router as licenses_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 = APIRouter()
@@ -18,3 +22,8 @@ router.include_router(auth_router)
router.include_router(tenants_router, prefix="/a76", tags=["a76 / 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(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
- Información regulatoria y de cumplimiento
### Módulo de Clases (GClass)
### Módulo de Clases (Class)
- Clasificaciones para sistemas SCAII y SCAF
- Información arancelaria detallada
- Gestión de fracciones arancelarias y materiales
@@ -30,17 +30,17 @@
## 🔗 Relaciones de Base de Datos
### Relaciones Principales
- **GPart ↔ GClass**: Relación de clave compuesta (client_key, part_class ↔ class_code)
- **GPart → Country**: Clave foránea a public.countries (country_of_origin)
- **GPart → CurrencyType**: Clave foránea a public.currency_types (currency_key)
- **GClass → MaterialType**: Clave foránea a public.material_types (material_key)
- **Part ↔ Class**: Relación de clave compuesta (client_key, part_class ↔ class_code)
- **Part → Country**: Clave foránea a public.countries (country_of_origin)
- **Part → CurrencyType**: Clave foránea a public.currency_types (currency_key)
- **Class → MaterialType**: Clave foránea a public.material_types (material_key)
### Esquema de Relaciones
```
GPart (Partes)
Part (Partes)
├── País de origen → Country
├── Tipo de moneda → CurrencyType
└── Información de clase → GClass
└── Información de clase → Class
└── Tipo de material → MaterialType
```

View File

@@ -2,8 +2,8 @@
## Resumen de Relaciones Establecidas
### GPart (Tabla: gparts)
El modelo `GPart` representa las partes/componentes en los sistemas SCAII, SCAF y WINSAAI.
### Part (Tabla: parts)
El modelo `Part` representa las partes/componentes en los sistemas SCAII, SCAF y WINSAAI.
#### Relaciones:
@@ -17,14 +17,14 @@ El modelo `GPart` representa las partes/componentes en los sistemas SCAII, SCAF
- Relación: Many-to-One
- 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)`
- Relación: Many-to-One (usando primaryjoin complejo)
- Propósito: Clasificación de la parte
- Atributo: `part_class_info`
### GClass (Tabla: gclasses)
El modelo `GClass` representa las clases de clasificación en sistemas SCAII y SCAF.
### Class (Tabla: classes)
El modelo `Class` representa las clases de clasificación en sistemas SCAII y SCAF.
#### Relaciones:
@@ -33,24 +33,24 @@ El modelo `GClass` representa las clases de clasificación en sistemas SCAII y S
- Relación: Many-to-One
- 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)`
- 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
- Atributo: `parts`
## Esquema de Relaciones
```
GPart
Part
├── country (Country) # País de origen
├── 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
GClass
Class
├── 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
@@ -58,13 +58,13 @@ GClass
### En consultas:
```python
# Obtener una parte con su información completa
part = session.query(GPart).options(
joinedload(GPart.country),
joinedload(GPart.currency),
joinedload(GPart.part_class_info).joinedload(GClass.material_type)
part = session.query(Part).options(
joinedload(Part.country),
joinedload(Part.currency),
joinedload(Part.part_class_info).joinedload(Class.material_type)
).filter(
GPart.client_key == 1,
GPart.part_number == "PART001"
Part.client_key == 1,
Part.part_number == "PART001"
).first()
# Acceder a los datos relacionados
@@ -89,7 +89,7 @@ class PartDetailResponseDTO(BaseModel):
## 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.

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 |
|--------|-------|---------|---------|
| **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_programs` | `a76` | ✅ Actualizada |
| **GParts** | `gparts` | `a76` | ✅ Actualizada |
| **GClass** | `gclasses` | `a76` | ✅ Actualizada |
| **GParts** | `parts` | `a76` | ✅ Actualizada |
| **Class** | `classes` | `a76` | ✅ Actualizada |
| **Licenses** | `licenses` | `a76` | ✅ Ya estaba |
| **Licenses** | `license_usage` | `a76` | ✅ Ya estaba |
| **Tenants** | `tenants` | `a76` | ✅ Ya estaba |
@@ -39,10 +39,10 @@ class GCompany(Base):
#### 2. Foreign Keys Actualizadas
```python
# ANTES
client_id = Column(String(8), ForeignKey('gclient_provider.client_id'), ...)
client_id = Column(String(8), ForeignKey('client_provider.client_id'), ...)
# 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
@@ -60,11 +60,11 @@ PostgreSQL Database
├── licenses
├── license_usage
├── gcompany
├── gclient_provider
├── client_provider
├── gclient_provider_address
├── gclient_provider_programs
├── gparts
└── gclasses
├── parts
└── classes
```
### 🔗 Relaciones Mantenidas
@@ -77,13 +77,13 @@ Las relaciones entre schemas funcionan correctamente:
#### Ejemplos de Relaciones Cross-Schema:
```python
# GPart (a76) → Country (public)
# Part (a76) → Country (public)
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'))
# GClass (a76) → MaterialType (public)
# Class (a76) → MaterialType (public)
material_key = Column(String(10), ForeignKey('public.material_types.key'))
```