From 61d386a7c73e324234bc7926fa108d66b71739c1 Mon Sep 17 00:00:00 2001 From: gerardoe Date: Wed, 1 Apr 2026 13:48:40 -0700 Subject: [PATCH] first commit --- .env | 4 + .envexample | 0 .gitingnore | 0 Dockerfile | 0 Dockerfile.dev | 16 + alembic.ini | 116 +++++ app/core/__init__.py | 0 app/core/__pycache__/__init__.cpython-311.pyc | Bin 0 -> 134 bytes app/core/__pycache__/auth.cpython-311.pyc | Bin 0 -> 9239 bytes app/core/__pycache__/base.cpython-311.pyc | Bin 0 -> 604 bytes .../baseRepository.cpython-311.pyc | Bin 0 -> 11252 bytes app/core/__pycache__/security.cpython-311.pyc | Bin 0 -> 3579 bytes app/core/__pycache__/servo.cpython-311.pyc | Bin 0 -> 4050 bytes app/core/affidavit.py | 184 ++++++++ app/core/auth.py | 160 +++++++ app/core/base.py | 14 + app/core/baseRepository.py | 224 ++++++++++ app/core/dependencies.py | 0 app/core/intefaces.py | 0 app/core/security.py | 64 +++ app/core/servo.py | 91 ++++ app/modules/__init__.py | 52 +++ .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 1768 bytes .../__pycache__/models.cpython-311.pyc | Bin 0 -> 3746 bytes app/modules/affidavit_logs/models.py | 58 +++ app/modules/affidavit_logs/repository.py | 0 app/modules/affidavit_logs/route.py | 0 app/modules/affidavit_logs/schema.py | 0 .../__pycache__/models.cpython-311.pyc | Bin 0 -> 2733 bytes app/modules/branches/models.py | 47 ++ app/modules/branches/repository.py | 0 app/modules/branches/route.py | 0 app/modules/branches/schema.py | 0 .../__pycache__/models.cpython-311.pyc | Bin 0 -> 4674 bytes app/modules/clients/models.py | 76 ++++ app/modules/clients/repository.py | 0 app/modules/clients/route.py | 0 app/modules/clients/schema.py | 0 .../__pycache__/models.cpython-311.pyc | Bin 0 -> 2345 bytes app/modules/coments/models.py | 37 ++ app/modules/coments/repository.py | 0 app/modules/coments/route.py | 0 app/modules/coments/schema.py | 0 app/modules/coments/services.py | 0 .../__pycache__/models.cpython-311.pyc | Bin 0 -> 2235 bytes app/modules/configuration/models.py | 34 ++ app/modules/configuration/repository.py | 0 app/modules/configuration/route.py | 0 app/modules/configuration/schema.py | 0 .../__pycache__/models.cpython-311.pyc | Bin 0 -> 3087 bytes app/modules/credits/models.py | 50 +++ app/modules/credits/repository.py | 0 app/modules/credits/route.py | 0 app/modules/credits/schema.py | 0 .../diot/__pycache__/models.cpython-311.pyc | Bin 0 -> 140 bytes app/modules/diot/models.py | 0 app/modules/diot/repository.py | 0 app/modules/diot/route.py | 0 app/modules/diot/schema.py | 0 .../edos/__pycache__/models.cpython-311.pyc | Bin 0 -> 2098 bytes app/modules/edos/models.py | 30 ++ app/modules/edos/repository.py | 0 app/modules/edos/route.py | 0 app/modules/edos/schema.py | 0 .../efos/__pycache__/models.cpython-311.pyc | Bin 0 -> 2419 bytes app/modules/efos/models.py | 33 ++ app/modules/efos/repository.py | 0 app/modules/efos/route.py | 0 app/modules/efos/schema.py | 0 app/modules/entreprise/models.py | 0 app/modules/entreprise/repository.py | 0 app/modules/entreprise/route.py | 0 app/modules/entreprise/schema.py | 0 .../feed/__pycache__/models.cpython-311.pyc | Bin 0 -> 2107 bytes app/modules/feed/models.py | 26 ++ app/modules/feed/repository.py | 0 app/modules/feed/route.py | 0 app/modules/feed/schema.py | 0 .../files/__pycache__/models.cpython-311.pyc | Bin 0 -> 1847 bytes app/modules/files/models.py | 24 ++ app/modules/files/repository.py | 0 app/modules/files/route.py | 0 app/modules/files/schema.py | 0 .../__pycache__/models.cpython-311.pyc | Bin 0 -> 2196 bytes app/modules/interactions/models.py | 34 ++ app/modules/interactions/repository.py | 0 app/modules/interactions/route.py | 0 app/modules/interactions/schema.py | 0 .../__pycache__/models.cpython-311.pyc | Bin 0 -> 1685 bytes app/modules/invoices/models.py | 23 + app/modules/invoices/repository.py | 0 app/modules/invoices/route.py | 0 app/modules/invoices/schema.py | 0 .../__pycache__/models.cpython-311.pyc | Bin 0 -> 2813 bytes app/modules/license/models.py | 45 ++ app/modules/license/repository.py | 0 app/modules/license/route.py | 0 app/modules/license/schema.py | 0 .../__pycache__/models.cpython-311.pyc | Bin 0 -> 3013 bytes app/modules/location/models.py | 50 +++ app/modules/location/repository.py | 0 app/modules/location/route.py | 0 app/modules/location/schema.py | 0 app/modules/location/service.py | 0 .../moves/__pycache__/models.cpython-311.pyc | Bin 0 -> 3941 bytes app/modules/moves/models.py | 71 +++ app/modules/moves/repository.py | 0 app/modules/moves/route.py | 0 app/modules/moves/schema.py | 0 .../__pycache__/models.cpython-311.pyc | Bin 0 -> 143 bytes app/modules/reports/models.py | 0 app/modules/reports/repository.py | 0 app/modules/reports/route.py | 0 app/modules/reports/schema.py | 0 .../__pycache__/models.cpython-311.pyc | Bin 0 -> 3418 bytes app/modules/suppliers/models.py | 55 +++ app/modules/suppliers/repository.py | 0 app/modules/suppliers/route.py | 0 app/modules/suppliers/schema.py | 0 app/modules/suppliers/service.py | 0 app/modules/taxes/models.py | 0 app/modules/taxes/repository.py | 0 app/modules/taxes/route.py | 0 app/modules/taxes/schema.py | 0 app/modules/taxpayers/models.py | 0 app/modules/taxpayers/repository.py | 0 app/modules/taxpayers/route.py | 0 app/modules/taxpayers/schema.py | 0 .../users/__pycache__/models.cpython-311.pyc | Bin 0 -> 4406 bytes .../__pycache__/repository.cpython-311.pyc | Bin 0 -> 14603 bytes .../users/__pycache__/route.cpython-311.pyc | Bin 0 -> 9438 bytes .../users/__pycache__/schema.cpython-311.pyc | Bin 0 -> 6032 bytes .../__pycache__/services.cpython-311.pyc | Bin 0 -> 7855 bytes app/modules/users/models.py | 79 ++++ app/modules/users/repository.py | 212 +++++++++ app/modules/users/route.py | 188 ++++++++ app/modules/users/schema.py | 96 +++++ app/modules/users/services.py | 158 +++++++ .../__pycache__/base.cpython-311.pyc | Bin 0 -> 4027 bytes .../__pycache__/crud.cpython-311.pyc | Bin 0 -> 17867 bytes app/pipelines/base.py | 79 ++++ app/pipelines/crud.py | 289 +++++++++++++ app/serviceInput/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 142 bytes .../__pycache__/base.cpython-311.pyc | Bin 0 -> 2014 bytes app/serviceInput/base.py | 29 ++ app/serviceOutput/__init__.py | 0 .../__pycache__/__init__.cpython-311.pyc | Bin 0 -> 143 bytes .../__pycache__/base.cpython-311.pyc | Bin 0 -> 2513 bytes app/serviceOutput/base.py | 37 ++ database.py | 104 +++++ docker-compose.dev.yml | 50 +++ docker-compose.prod.yml | 0 main.py | 58 +++ migrations/README | 1 + migrations/__pycache__/env.cpython-311.pyc | Bin 0 -> 2777 bytes migrations/env.py | 55 +++ migrations/script.py.mako | 26 ++ .../03baecbcb5d9_adjustmen_modules.py | 407 ++++++++++++++++++ ...754e_xma_architecture_all_modules_with_.py | 36 ++ .../84e064c18a0f_adjustmen_modules.py | 42 ++ .../8e2919155783_base_adjustment_1_5.py | 30 ++ ...ecbcb5d9_adjustmen_modules.cpython-311.pyc | Bin 0 -> 40584 bytes ...itecture_all_modules_with_.cpython-311.pyc | Bin 0 -> 2230 bytes ...64c18a0f_adjustmen_modules.cpython-311.pyc | Bin 0 -> 2216 bytes ...155783_base_adjustment_1_5.cpython-311.pyc | Bin 0 -> 1098 bytes ...441b91f586_base_adjustment.cpython-311.pyc | Bin 0 -> 1852 bytes ...fc0d7e_fix_moves_relations.cpython-311.pyc | Bin 0 -> 1098 bytes ...relationships_foreign_keys.cpython-311.pyc | Bin 0 -> 1120 bytes ...41d0457f936_initial_schema.cpython-311.pyc | Bin 0 -> 1062 bytes ...a356_fixed_relations_ships.cpython-311.pyc | Bin 0 -> 3369 bytes .../versions/b7441b91f586_base_adjustment.py | 34 ++ .../b7bce6fc0d7e_fix_moves_relations.py | 30 ++ ...58ebc7d4_fix_relationships_foreign_keys.py | 30 ++ .../versions/d41d0457f936_initial_schema.py | 30 ++ .../fbbb979fa356_fixed_relations_ships.py | 56 +++ requerimiento_minimos.txt | 38 ++ requirements.txt | 15 + 178 files changed, 3827 insertions(+) create mode 100644 .env create mode 100644 .envexample create mode 100644 .gitingnore create mode 100644 Dockerfile create mode 100644 Dockerfile.dev create mode 100644 alembic.ini create mode 100644 app/core/__init__.py create mode 100644 app/core/__pycache__/__init__.cpython-311.pyc create mode 100644 app/core/__pycache__/auth.cpython-311.pyc create mode 100644 app/core/__pycache__/base.cpython-311.pyc create mode 100644 app/core/__pycache__/baseRepository.cpython-311.pyc create mode 100644 app/core/__pycache__/security.cpython-311.pyc create mode 100644 app/core/__pycache__/servo.cpython-311.pyc create mode 100644 app/core/affidavit.py create mode 100644 app/core/auth.py create mode 100644 app/core/base.py create mode 100644 app/core/baseRepository.py create mode 100644 app/core/dependencies.py create mode 100644 app/core/intefaces.py create mode 100644 app/core/security.py create mode 100644 app/core/servo.py create mode 100644 app/modules/__init__.py create mode 100644 app/modules/__pycache__/__init__.cpython-311.pyc create mode 100644 app/modules/affidavit_logs/__pycache__/models.cpython-311.pyc create mode 100644 app/modules/affidavit_logs/models.py create mode 100644 app/modules/affidavit_logs/repository.py create mode 100644 app/modules/affidavit_logs/route.py create mode 100644 app/modules/affidavit_logs/schema.py create mode 100644 app/modules/branches/__pycache__/models.cpython-311.pyc create mode 100644 app/modules/branches/models.py create mode 100644 app/modules/branches/repository.py create mode 100644 app/modules/branches/route.py create mode 100644 app/modules/branches/schema.py create mode 100644 app/modules/clients/__pycache__/models.cpython-311.pyc create mode 100644 app/modules/clients/models.py create mode 100644 app/modules/clients/repository.py create mode 100644 app/modules/clients/route.py create mode 100644 app/modules/clients/schema.py create mode 100644 app/modules/coments/__pycache__/models.cpython-311.pyc create mode 100644 app/modules/coments/models.py create mode 100644 app/modules/coments/repository.py create mode 100644 app/modules/coments/route.py create mode 100644 app/modules/coments/schema.py create mode 100644 app/modules/coments/services.py create mode 100644 app/modules/configuration/__pycache__/models.cpython-311.pyc create mode 100644 app/modules/configuration/models.py create mode 100644 app/modules/configuration/repository.py create mode 100644 app/modules/configuration/route.py create mode 100644 app/modules/configuration/schema.py create mode 100644 app/modules/credits/__pycache__/models.cpython-311.pyc create mode 100644 app/modules/credits/models.py create mode 100644 app/modules/credits/repository.py create mode 100644 app/modules/credits/route.py create mode 100644 app/modules/credits/schema.py create mode 100644 app/modules/diot/__pycache__/models.cpython-311.pyc create mode 100644 app/modules/diot/models.py create mode 100644 app/modules/diot/repository.py create mode 100644 app/modules/diot/route.py create mode 100644 app/modules/diot/schema.py create mode 100644 app/modules/edos/__pycache__/models.cpython-311.pyc create mode 100644 app/modules/edos/models.py create mode 100644 app/modules/edos/repository.py create mode 100644 app/modules/edos/route.py create mode 100644 app/modules/edos/schema.py create mode 100644 app/modules/efos/__pycache__/models.cpython-311.pyc create mode 100644 app/modules/efos/models.py create mode 100644 app/modules/efos/repository.py create mode 100644 app/modules/efos/route.py create mode 100644 app/modules/efos/schema.py create mode 100644 app/modules/entreprise/models.py create mode 100644 app/modules/entreprise/repository.py create mode 100644 app/modules/entreprise/route.py create mode 100644 app/modules/entreprise/schema.py create mode 100644 app/modules/feed/__pycache__/models.cpython-311.pyc create mode 100644 app/modules/feed/models.py create mode 100644 app/modules/feed/repository.py create mode 100644 app/modules/feed/route.py create mode 100644 app/modules/feed/schema.py create mode 100644 app/modules/files/__pycache__/models.cpython-311.pyc create mode 100644 app/modules/files/models.py create mode 100644 app/modules/files/repository.py create mode 100644 app/modules/files/route.py create mode 100644 app/modules/files/schema.py create mode 100644 app/modules/interactions/__pycache__/models.cpython-311.pyc create mode 100644 app/modules/interactions/models.py create mode 100644 app/modules/interactions/repository.py create mode 100644 app/modules/interactions/route.py create mode 100644 app/modules/interactions/schema.py create mode 100644 app/modules/invoices/__pycache__/models.cpython-311.pyc create mode 100644 app/modules/invoices/models.py create mode 100644 app/modules/invoices/repository.py create mode 100644 app/modules/invoices/route.py create mode 100644 app/modules/invoices/schema.py create mode 100644 app/modules/license/__pycache__/models.cpython-311.pyc create mode 100644 app/modules/license/models.py create mode 100644 app/modules/license/repository.py create mode 100644 app/modules/license/route.py create mode 100644 app/modules/license/schema.py create mode 100644 app/modules/location/__pycache__/models.cpython-311.pyc create mode 100644 app/modules/location/models.py create mode 100644 app/modules/location/repository.py create mode 100644 app/modules/location/route.py create mode 100644 app/modules/location/schema.py create mode 100644 app/modules/location/service.py create mode 100644 app/modules/moves/__pycache__/models.cpython-311.pyc create mode 100644 app/modules/moves/models.py create mode 100644 app/modules/moves/repository.py create mode 100644 app/modules/moves/route.py create mode 100644 app/modules/moves/schema.py create mode 100644 app/modules/reports/__pycache__/models.cpython-311.pyc create mode 100644 app/modules/reports/models.py create mode 100644 app/modules/reports/repository.py create mode 100644 app/modules/reports/route.py create mode 100644 app/modules/reports/schema.py create mode 100644 app/modules/suppliers/__pycache__/models.cpython-311.pyc create mode 100644 app/modules/suppliers/models.py create mode 100644 app/modules/suppliers/repository.py create mode 100644 app/modules/suppliers/route.py create mode 100644 app/modules/suppliers/schema.py create mode 100644 app/modules/suppliers/service.py create mode 100644 app/modules/taxes/models.py create mode 100644 app/modules/taxes/repository.py create mode 100644 app/modules/taxes/route.py create mode 100644 app/modules/taxes/schema.py create mode 100644 app/modules/taxpayers/models.py create mode 100644 app/modules/taxpayers/repository.py create mode 100644 app/modules/taxpayers/route.py create mode 100644 app/modules/taxpayers/schema.py create mode 100644 app/modules/users/__pycache__/models.cpython-311.pyc create mode 100644 app/modules/users/__pycache__/repository.cpython-311.pyc create mode 100644 app/modules/users/__pycache__/route.cpython-311.pyc create mode 100644 app/modules/users/__pycache__/schema.cpython-311.pyc create mode 100644 app/modules/users/__pycache__/services.cpython-311.pyc create mode 100644 app/modules/users/models.py create mode 100644 app/modules/users/repository.py create mode 100644 app/modules/users/route.py create mode 100644 app/modules/users/schema.py create mode 100644 app/modules/users/services.py create mode 100644 app/pipelines/__pycache__/base.cpython-311.pyc create mode 100644 app/pipelines/__pycache__/crud.cpython-311.pyc create mode 100644 app/pipelines/base.py create mode 100644 app/pipelines/crud.py create mode 100644 app/serviceInput/__init__.py create mode 100644 app/serviceInput/__pycache__/__init__.cpython-311.pyc create mode 100644 app/serviceInput/__pycache__/base.cpython-311.pyc create mode 100644 app/serviceInput/base.py create mode 100644 app/serviceOutput/__init__.py create mode 100644 app/serviceOutput/__pycache__/__init__.cpython-311.pyc create mode 100644 app/serviceOutput/__pycache__/base.cpython-311.pyc create mode 100644 app/serviceOutput/base.py create mode 100644 database.py create mode 100644 docker-compose.dev.yml create mode 100644 docker-compose.prod.yml create mode 100644 main.py create mode 100644 migrations/README create mode 100644 migrations/__pycache__/env.cpython-311.pyc create mode 100644 migrations/env.py create mode 100644 migrations/script.py.mako create mode 100644 migrations/versions/03baecbcb5d9_adjustmen_modules.py create mode 100644 migrations/versions/37e3df20754e_xma_architecture_all_modules_with_.py create mode 100644 migrations/versions/84e064c18a0f_adjustmen_modules.py create mode 100644 migrations/versions/8e2919155783_base_adjustment_1_5.py create mode 100644 migrations/versions/__pycache__/03baecbcb5d9_adjustmen_modules.cpython-311.pyc create mode 100644 migrations/versions/__pycache__/37e3df20754e_xma_architecture_all_modules_with_.cpython-311.pyc create mode 100644 migrations/versions/__pycache__/84e064c18a0f_adjustmen_modules.cpython-311.pyc create mode 100644 migrations/versions/__pycache__/8e2919155783_base_adjustment_1_5.cpython-311.pyc create mode 100644 migrations/versions/__pycache__/b7441b91f586_base_adjustment.cpython-311.pyc create mode 100644 migrations/versions/__pycache__/b7bce6fc0d7e_fix_moves_relations.cpython-311.pyc create mode 100644 migrations/versions/__pycache__/c01f58ebc7d4_fix_relationships_foreign_keys.cpython-311.pyc create mode 100644 migrations/versions/__pycache__/d41d0457f936_initial_schema.cpython-311.pyc create mode 100644 migrations/versions/__pycache__/fbbb979fa356_fixed_relations_ships.cpython-311.pyc create mode 100644 migrations/versions/b7441b91f586_base_adjustment.py create mode 100644 migrations/versions/b7bce6fc0d7e_fix_moves_relations.py create mode 100644 migrations/versions/c01f58ebc7d4_fix_relationships_foreign_keys.py create mode 100644 migrations/versions/d41d0457f936_initial_schema.py create mode 100644 migrations/versions/fbbb979fa356_fixed_relations_ships.py create mode 100644 requerimiento_minimos.txt create mode 100644 requirements.txt diff --git a/.env b/.env new file mode 100644 index 0000000..383bdef --- /dev/null +++ b/.env @@ -0,0 +1,4 @@ +# .env + + +DATABASE_URL=postgresql://postgres:postgres@localhost:5432/postgres \ No newline at end of file diff --git a/.envexample b/.envexample new file mode 100644 index 0000000..e69de29 diff --git a/.gitingnore b/.gitingnore new file mode 100644 index 0000000..e69de29 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..e69de29 diff --git a/Dockerfile.dev b/Dockerfile.dev new file mode 100644 index 0000000..8683d36 --- /dev/null +++ b/Dockerfile.dev @@ -0,0 +1,16 @@ +# Dockerfile.dev +FROM python:3.11-alpine + +WORKDIR /app + +RUN apk add --no-cache \ + gcc \ + musl-dev \ + postgresql-dev + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +EXPOSE 8000 \ No newline at end of file diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..6d200d8 --- /dev/null +++ b/alembic.ini @@ -0,0 +1,116 @@ +# A generic, single database configuration. + +[alembic] +# path to migration scripts +script_location = migrations + +# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s +# Uncomment the line below if you want the files to be prepended with date and time +# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file +# for all available tokens +# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s + +# sys.path path, will be prepended to sys.path if present. +# defaults to the current working directory. +prepend_sys_path = . + +# timezone to use when rendering the date within the migration file +# as well as the filename. +# If specified, requires the python-dateutil library that can be +# installed by adding `alembic[tz]` to the pip requirements +# string value is passed to dateutil.tz.gettz() +# leave blank for localtime +# timezone = + +# max length of characters to apply to the +# "slug" field +# truncate_slug_length = 40 + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + +# set to 'true' to allow .pyc and .pyo files without +# a source .py file to be detected as revisions in the +# versions/ directory +# sourceless = false + +# version location specification; This defaults +# to migrations/versions. When using multiple version +# directories, initial revisions must be specified with --version-path. +# The path separator used here should be the separator specified by "version_path_separator" below. +# version_locations = %(here)s/bar:%(here)s/bat:migrations/versions + +# version path separator; As mentioned above, this is the character used to split +# version_locations. The default within new alembic.ini files is "os", which uses os.pathsep. +# If this key is omitted entirely, it falls back to the legacy behavior of splitting on spaces and/or commas. +# Valid values for version_path_separator are: +# +# version_path_separator = : +# version_path_separator = ; +# version_path_separator = space +version_path_separator = os # Use os.pathsep. Default configuration used for new projects. + +# set to 'true' to search source files recursively +# in each "version_locations" directory +# new in Alembic version 1.10 +# recursive_version_locations = false + +# the output encoding used when revision files +# are written from script.py.mako +# output_encoding = utf-8 + +sqlalchemy.url = postgresql+psycopg2://postgres:postgres@postgres:5432/postgres + + +[post_write_hooks] +# post_write_hooks defines scripts or Python functions that are run +# on newly generated revision scripts. See the documentation for further +# detail and examples + +# format using "black" - use the console_scripts runner, against the "black" entrypoint +# hooks = black +# black.type = console_scripts +# black.entrypoint = black +# black.options = -l 79 REVISION_SCRIPT_FILENAME + +# lint with attempts to fix using "ruff" - use the exec runner, execute a binary +# hooks = ruff +# ruff.type = exec +# ruff.executable = %(here)s/.venv/bin/ruff +# ruff.options = --fix REVISION_SCRIPT_FILENAME + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/app/core/__init__.py b/app/core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/__pycache__/__init__.cpython-311.pyc b/app/core/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d5737c57f01b75ab8c3f2d6674121200284eeb15 GIT binary patch literal 134 zcmZ3^%ge<81d}3;W`gL)AOZ#$p^VRLK*n^26oz01O-8?!3`I;p{%4TnFG>Bxf&ws3 z&M!*UkB`sH%PfhH*DI*}#bJ}1pHiBWYFESxR0%Sxm>)=dU}j`w{J;PsikN|70JY;A AB>(^b literal 0 HcmV?d00001 diff --git a/app/core/__pycache__/auth.cpython-311.pyc b/app/core/__pycache__/auth.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d971fa1ddb2aad5264b3ec7fc02d6fe5ea5cf92d GIT binary patch literal 9239 zcmcIpeQXm+mhZN^+u!cQ&Ibu!O$Z-mjxfUjJ8(=!nP9-=i%lH%@Ql{e$7$Hz*(IyHm2wipf?#!=G$YL)cM{UBPWxv&@(QaZB&55Mj{nSsmC)Wl z_g=N_xQ*GHJ!#7BS5>cGbyrvY>b+O5f93PJ8Avz&`QMZO)4?$Rft6ZvR12$%EW_Mn z6h>iF%p4PA=GYj^V%?Uq&Dmr2IWET0x;@3mc;MSpLQH_1OF8D8G3T5s=9+WI+*F27 zdFH$^@0>5@V;LKxdT+yH?$w^j#QebH&xtjLA}EgAHjseJOtCh_8EXfrK$E0Paa$#C z!?^FkRqYqFF~=CibCprNR=HRQZSg^izp15@wusQu*3{BPTiT%|(A3fm<8rEg>(h~^ z1g|ny?ulOPH6WZ_;UngWU zoQj-NbICc?a3NLHR8B_rL^hYqq~(;sjV86+c*t%zj;L8Rt!Retcr?SCxrMj15>Gn$MlhkYrxQdkqt~dpehtQ<~wP3J*_)qw$gO>HOA9bMkIY zO%OG=dse-?J8@P{pHX)w)4Q`Iqs%9;ufZRm+PnXN(K9qW9G;qrM<+(YOA0Z!>QVfvR}tZAQ++VS?N_;G$SDDs*r4H@)>IO`oK^F^ z5*DQ#l%6OS3b&m}A9LQca{P4iIxj48w`EFb{T*3is!#2FvJ|G3AA~0>i>sKC$ z(uyc8_uLsOiUTEa;K9}}c0bxXys~%r;g~)ZE$*Ez?VVl`r*-G_D)eK9>@2rwm9N(C6fR)H#sp8HTa<;W07&geR48#Dl!^A7CvH#locP1 z42Q>uBSTY$WBTNgp=fy0;HIa-lcW!xW_S`QS<~iJ*oKN>*Kz=&4>Oq**$$0&8L|U1 zf-gndYXl40FM$;5zcSaabNx@;ZG|IcPoNMkw{0k#D0glzOqBiIg=1xGs5lvyZ&5Bg zeT(PI&hDixWoO{Vcgs%k29&&u=PN!(yHJQ!0*ugJvDx@j?6Ov2F#qaJGk>_x%p2)5 zJBVL>&+O3HV2&Kent-dGYM5Q!1*~tj=@lp7&}E1du>4d_$*IXqO3gbY%yz)`OJwG< z1ookD0kjw7u2kj>sKjJKHaxO2FQ?_WmYK*C{{KFIuw@S`&fnjDpLQZaaSPEMw%egZX}NyY1kArjmn zLG>dPFroiM-~2zJaFfZ^-<4s`)yq~>lxygpLwiw8_gSmokO%%1KIf{nqkgwaEZ7$8 z3)}+#O`fXQ{!zFh>${O~hoC~q!1TaD_f;TXP`CKBMxkwNN#E;}nWZTbO%ozfh z(~P2$7ca?)ZkAv$!d#4!eF9rKSfd<_b2FRl04{d0B@=H6G_uYEuE9GC)x;R!8$4cUu z?i@3r#?x^naNE=pf`S5R1qwGA*DIj3z{2v>`$4(BG-p@<5Y*Nd>xyl`HpOgZ8n@#) zoP3)484_TbMnEVw#eSc&Is+fGmfm&9wCv5l-1{PP$$px-$c6-jjk~^uwWmLZ?UDCK zae*Ydp0bj;Yco8izZyq07=ohaU@QIsPYR}FGJWAU|CCB9nUI5g54b2|1&+}frsz?Y3Mhh`E3Qu9WtK)Rrk0G4pd{|BFs$nY z+ejY=Hs2Z6-#V@bBE>+Y6o}~F2$k(esSzq$qgC069yn19oG1lO=-v|*J4(HH@j^or z@$uH}%#m-syfL4 zkfR|R`92KRaAfGoMk8^kkpz+tfP@4J2%;dPu13fVR&nj2J5?yS&>HTbx~?!0)M|+v zTPCre3Ivzfj!PWu`s~n>x*WX~f7t)q0e$GZ`uk_~a~b`;tR6gH44y9q&+Fdv<~sk` zYlmw8igbdG9L@*Y+>C7H;PGf*V6NSYjyF!GR~?$!Pc zNTL2KbKN@EUEyr}R&?j8#O>k7+2!F1gZTrynLn)Ndh~lHLw`SR7Fy6=CqKe2!|gD? z15<%{3);a~Ohsog;i71VTOGf@4}m?bYlY!Aa6|jd*a4_!*}nfXjpKkOL0n z=pMm&la^i7H8RWSCb{9%$8cit|LE->Xl2RL62u%?i?!J?uo23B>ED>-}1y_=vaF2c`TUJ3fN_Ba0mTC6AI zT>aI-1FM&3X{q+QcGFqG0}8=AkP#q_&ZHx$#W4bq4>rC>m5D&purIuZ7`*<2&)41B zxFQbd&H*~wZ)>2tTMg6+W1x|d9sQj9Sx@kq+qdWI}t3%z>YN&zQpw@yX`AtpJ z+Z0?u&rAydsn2w2%fS`#pzb_q0t2H57PE%f1|Nu7eHRG0<3MX^x$chFIG9_!sir^( z>|!>5@Dw@#ut_B?DK%`~9GoUqWaN_B42^q$oRWb+h&B%g%w&duXF(CR9UemgSF=Dg z=7E5$Z5fov_cqRjHL#I_cC8l(xT#|IBXRqRxP3Wy>jw|66vc2!4C~G?osV(Dheuoj z@hVNllT3gfX)HWTgb4>hrwgBe`)C>Rm(y)v-K(<+M2x1G1shoplUJYA#l;RB_rMkOK3>BYt}Llf%~ruMeW@wXPiOzOk?D z*@%_zEcf3U`11G{<12md>V5B)gOc8VU?q4!_Z}#V9gFiOK&f{1wY1$h<)+z2D^B(y zp|5!XOP7#PX63pqV+&-g>lUou+$rBn&Ce>52xh04;6Wk z{59N%!VA6W0`sNkUwmKq3TBK4B6tm9b1%k4;$ZDDTw;a_a5S}q9^K!-wR#jb%?QIb z87?@jv9HikHtVtfiro@IKv%w+7VOJht=yPHh!>u@_L;<5du6Yt=e|D(NNzol)zakAmfKf z>gP(*4b>)R$!LRkcsybHiE&ic1QBu>CI1e{f58uq8*aw8uH@Ycv0Z=fBma&S|BgF+ z(LYe~4-}4l?df{#@h@Kd^rKHcTH0R}x0J*!MNePJ(^vL)emeWf?9z^+zpv!)D+hXR zzV?&XmS%tb*8R7OfrF*M!E)Eeo3lTeUEcHMJEb>Ai(O--uCa=X_X^J#AkUFN?AXtE z+X^EM2Dh&BVT1@bJ6x+Lfc&Uw!!`P1R>;l3mx@MhvyTUGjbqU$$-+~*y zw6^$t0gNjF17R9oSkIa}t!h~{@ta?Rt1T+C1z-68Qk` z2fhK-_SKW>-vZ10Hi8;NDwx3tw?e=10oE_)w^cv2P}IJX3@;M43s z{QI}Ziu=b)`^Qn!Z=mG!kJid9OMuHRo$I9IF}(FJPY|%REcmyF&;v01KZ#Hnid2ss zOAYuWxW1NA)?rDYWet|+pk}6P>@0;I*$G}+;|{C|Fjn&hP{n&z(>H)B`4PzG*Ed4d zGX5G+?c^i~nyyMCTCW4wjH{SvrBKLkt}Q>@s}(?hfxS=>*8uA2E_t@y*|6f-uY2}? zm3lNdxiUCe9E_F*qrj*kb@;*idiOB=|4*cnYjATwCl2prbb1Avmo>0AkoOni+(2%*YV7gqSql6gVpct-aA!u8qW~G2&m}`^y3GKpa3;5G(t|z3$-j^Y@o&mO@!$=;!dK&e%XC7zg!NIpMwa( zMRUNegwP<%mYJP;tAClZBY(uPr; z>8S|a>?v>~ThnKh|GcqvuY+9=f%%unGvqyAYY6nS7;9=xpHcqv#@11GBMW}(%j6mI zp072$%QA1g^}z9!v-tW@>Gh$qv}4(!?|d8nMQN}k4OV!OZL2Uq%I&)=cF3OuHkwQU hKgT+1%~ZxD{Vuj^d8)!d)=bs`Zz<8twMtTK|1WNF literal 0 HcmV?d00001 diff --git a/app/core/__pycache__/base.cpython-311.pyc b/app/core/__pycache__/base.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dca068522d32f35064ba7bbfe55281cce79542f7 GIT binary patch literal 604 zcmYjNL5mYH6i%iy?QGZ5!z!X6&b5NO7mu>gowhL0cEIj}^fIKA*BY6bj7eHhJnWAU zyn7M0r zmX5{_-_C|nH$uCVTux9(%y#bSzU>IkH+2Bfp(zYEJ0xDyVrZQ(n$V%J(aJ?3s?Sy#) zxm4VS7m8K+JJ7Zh%M#hz_F`GB#b&GaJy=)31=juXC|zKjxHtjeHkgP4J0B#K0mbs% z-C@I|+DVbSiBp71gAHbKhxyTBTi5s`rRRf=?PM93d6wDGy?KSA?$jK!x<17(+Pcx< zL6ZucQtV+xk!3%~jr)z;M`uXhJBoobH{`VbcpJXH>pxe|kwwvUilJ#yN*m9kenVUc l34QvTJh{J{_Wc@WCy&3pyzV#chsRz+4&l1vmU@OW{{akNw~zn; literal 0 HcmV?d00001 diff --git a/app/core/__pycache__/baseRepository.cpython-311.pyc b/app/core/__pycache__/baseRepository.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1ab5fd98c7304462e732ae60ae76b9ff65574089 GIT binary patch literal 11252 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 literal 0 HcmV?d00001 diff --git a/app/core/__pycache__/security.cpython-311.pyc b/app/core/__pycache__/security.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66a8c8cc8ad2220443a34f54c7f673f7c585377a GIT binary patch literal 3579 zcma)8UuYXg8lU~Ef3{@HmL1zkqxj!dPMlnG#3X5x;P@2Van3Gw(w+r}*k~rU9Q|W< z6~{(CM6L&-6z*CA5!@AY(581Jh1{E83O)MPr(Ls0#DXDE`p`E+UP_=sUKkH(2xb@~nW+BZLtaQYbCN1cA-Pl$aJ{ zVp@txd@ZHqv=USJTu!Mm6?Ej3E9T;(JLVP>X zSOgjLeOVjG)V22d8oROQkcJ<7V*$|WIHc*rZLOMzp4y4qNf7%<@Q#cFAIPyT+yU!O z&g;IT;vn!s_z3PIJ-C}FMCz+=0&y!9hdxkZy|{;4^njOMusDL{B^2rVf$2sBr)`RE z}nHkT~EY2sGV@H`; zI(u%y>6@IIiY_h1Z!XM5=i|}ezdO4ajn6FHT3m93w;Xp))AjW%#f}Qj@-&lZ#0cI- z+V3yhcuo{h1sJb3EVQ1p9(t{&*s4hb27AVS!&0mCfdGCQc~RG5BbHk=w*;e}zaT9f z+7DLl%FK#-3ka)N`LlQs*DYkYnXJ|-N`2Nm7EtTcAlk0{2xv@`453l?Z z!yvlO&N3YSsqc)I%kiGbQgQ|oDNmEe#_8OK)44*7c*BbMq61*m16}_UkRsYsk-Ou$ ztAEGUU!JgBgSKmM4++ZKmDT5?7j{N3Y>!%_llJH&XYTrsn0*&4|3%w>(R5#Y5jyrL z{z<&@z7-m`L*vEyU6;3X*>d&U;B0Znb=-6vuXYWTr^<`vsfU+J?(cm)&wT?szJcqw^%jqHLItBuv7G)*08|Z%?II$BrQ91qWj1{DLOplgY$pU7qrXyZQPGoZ%l;yso=8Z%qyH42$N7Ap&5;FbWWfg7xlmGH$nDSN?j5(3I&0~6?m+-H5MS=0Ju5?WUx5wH+(3e9th0 zPBPJ&Qr<@P=$rnVJlA-zHJr_}x>aK(GxxsvM=FW45f}Y6a2*vhcZ4SpeT#8IQ4SAy zDY=r-;P0hY9Vr79r*k%QPXlxAj6*3@#R(>H<+P1dR>S+{u!e==b(EdgzYx!KeyTsY-$}#&Xo_5>*FnT`Sx*Cn zw-baH=uNZruC^{4(kdD-TkjXhQ{-0_d5iq2B7c!z@PFbRDD~Q&{_=UtGi-Z?iz@H} z1Eo}DrgCw6)bvkU{z=30jK%K9FHp+q`lSvY2+jswAFYAQ-2M7J8_L$v&0`r(qQXo{KF-Iy$qK_3~ zqK_Bi(8nFQtP~Up*T{}qP81S?R~)UZ6*R%e9KE1}Ty>J=R3TMP7t#`m5vG?i{g<-r z6N!*d;n!`2T#Q^MG;x)7prdIMx z+;}xFh4PYHEkmzes|2>^S`PBm3Rqy5Sw0r#0@%qcdalcgxFF0@+pl;&Gnu<)!(>lI z(e*!c_b?0}6Gp(e1Pu#DjTK}XFDO(7m6b3lw7nj{i$eL9=LKP$SKUwstt+c}HPlSg zwaUyi!=!1JJz8~;Pn+huRmANs~P%wpSJ05bB8Ng8MJ z?HHB8ISN(ol7dQApb45_8q8~VWq^zh7D$B}_)Hj~`Ulu3X+RtBV9#;yR@p@%dfE6X zNoLg?u;yvk7i507XWds&ymTxfE@Y!0`rJhl7EY|}d7|b`oUCg(* z?e(5ov0PTNVCmogYB?YWbXXN@&7$1}VVtt{PzLj}VuVFd7I`$o?(cv+CU>Me#00L} zf`OH#h=&z@!;t_T(Q758&piR=)Bb?K`u>R}YdA{uMHJtrMI!>njIF+v*ZB<0b$vVr zT&Va6E{^+$YBeaj-g}{rNHY=Hp=t(}|0^GW2Do!=cMY&E`^>p5_VH11!{8J6G+@zo z?Z7l^<9(ma^`}n20{W=?!`yg75aHFFw<+4dxgQ|;-a z&}BB^5&$dq&kXEAP}$$ZZ}*olyG$f7Zh-T>wmrDEKM5h~4%svG9|?{9DLx!_BoajC zBV^dn{@l8z-mrX( ztp`oM#$ z_pUZ(UTvlqTIq#GdSNG>yMN{3k3U$xzr48|?Kjud=3Cl)L!18s4#+R?fV8KN;8ED= zzw&%jd!?nl(&*p&&j*GOaXy{_^X<>asP}^bl$WBg1el8EX$(eDXoB+q#qV!bFrNsd$4?O`Gb}FE1Pfu z2KA|?Hr>*u8`^Y)u+vTLOiMe{=-&c9p~_iMR)}r#?Ra*`?;x zYyY0S@~_D&&B=FKlke=DywE(k&^o!$n7+D|+Dd)yc64Pd*&$t`Cxr{4O6AkAbw0_l zu;!Q^I2tZYm?rg#P#4Difb$bLe+CKSFqElX4EU=!#(Ga|CGt6+!XXkgng1S;y*jWH zQxpvp0FqUdW5^xbx-Pm=lFcX=IwT^q8RaD$_Q)|+Np3B62z1XSU7dtFqIj7HRfd3y zP!m912WGeysJ{e%J%}SgEFzT&h(}SW1xurb02=-)@%~UPaEE+0lC!O$dQ!^)+Tv2z8Ir(KUWP(S#F;SW`JoDkVD7pwE`$m3QJ3K1p>j$Wu)(67{}5z1QT_uU zkbDxE`fu9W_@<6mJ9n^=JNJ-3-uQ51`|!s%n^WgnQ|C72o$;BipfNt%9G`8C&u*$a zP`a-OQ|6`B`~=6kJWN>rhFi3~Q1$IK_*cU*&q2C`KRX;P8mhrY1*$%dChiuA3Cy=R zbsne0F~D#aqP|x}D(>P>gv-}(jR=>*eW<_qnh~r$)4t#R^Kyw_h9wwW{OakWXZF0MA2Xbj(N@ Operation: + """Convierte string de operación a Enum Operation""" + operation_map = { + "create": Operation.CREATED, + "CREATE": Operation.CREATED, + "update": Operation.UPDATED, + "UPDATE": Operation.UPDATED, + "delete": Operation.DELETED, + "DELETE": Operation.DELETED, + "cancel": Operation.CANCELED, + "CANCEL": Operation.CANCELED, + "tried": Operation.TRIED, + "TRIED": Operation.TRIED, + } + return operation_map.get(self.operation.lower(), Operation.TRIED) + + def _get_status_enum(self, success: bool) -> StatusOP: + """Convierte resultado a StatusOP""" + if success: + return StatusOP.COMPLETED + return StatusOP.INVALID + + def _hash_data(self, data: Any) -> str: + """Genera hash para integridad de datos""" + data_str = json.dumps(data, sort_keys=True, default=str) + return hashlib.sha256(data_str.encode()).hexdigest() + + def certify_entity(self, entity_type: str, entity_id: int, entity_data: Dict) -> 'LegalAffidavit': + """Certifica que una entidad fue afectada""" + self.entities_affected.append({ + "type": entity_type, + "id": entity_id, + "data_hash": self._hash_data(entity_data), + "timestamp": self.timestamp.isoformat() + }) + return self + + def validate_rule(self, rule_name: str, rule_result: bool, rule_details: str = "") -> 'LegalAffidavit': + """Valida una regla de negocio""" + self.rules_validated.append({ + "rule": rule_name, + "passed": rule_result, + "details": rule_details, + "validated_at": self.timestamp.isoformat() + }) + + if not rule_result: + raise AffidavitValidationError(f"Regla '{rule_name}' no cumplida: {rule_details}") + + return self + + def add_evidence(self, evidence_type: str, evidence_data: Any) -> 'LegalAffidavit': + """Agrega evidencia digital""" + self.evidence.append({ + "type": evidence_type, + "data": evidence_data, + "hash": self._hash_data(evidence_data), + "timestamp": self.timestamp.isoformat() + }) + return self + + def _generate_signature(self) -> str: + """Genera firma digital del acta""" + data_to_sign = f"{self.user.id}{self.timestamp.isoformat()}{self.operation}{json.dumps(self.entities_affected, sort_keys=True)}" + return hashlib.sha512(data_to_sign.encode()).hexdigest() + + def _build_description(self) -> str: + """Construye la descripción del acta""" + entities = ", ".join([f"{e['type']}:{e['id']}" for e in self.entities_affected]) + rules = ", ".join([r['rule'] for r in self.rules_validated if r['passed']]) + + desc = f"Operación {self.operation} por usuario {self.user.email}" + if entities: + desc += f" | Entidades: {entities}" + if rules: + desc += f" | Reglas: {rules}" + return desc + + def sign(self, success: bool = True) -> Dict: + """ + Firma y guarda el acta notarial en la base de datos + """ + description = self._build_description() + signature = self._generate_signature() + status = self._get_status_enum(success) + operation_enum = self._get_operation_enum() + + # Crear registro en BD + self._record = AffidavitRecord( + operation=operation_enum, + user_id=self.user.id, + client_id=self.client_id, + description=description, + adjustment=json.dumps(self.evidence) if self.evidence else None, + status=status, + sign=signature, + created_at=self.timestamp, + created_by=self.user.id + ) + + self.db.add(self._record) + self.db.commit() + self.db.refresh(self._record) + + return { + "affidavit_id": self._record.id, + "operation": self.operation, + "user": { + "id": self.user.id, + "email": self.user.email, + "user_type": self.user.user_type, + "rol_operativo": self.user.rol_operativo + }, + "timestamp": self.timestamp.isoformat(), + "description": description, + "status": status.value, + "signature": signature, + "entities_affected": self.entities_affected, + "rules_validated": self.rules_validated + } + + def get_record_id(self) -> Optional[int]: + """Retorna el ID del registro creado""" + return self._record.id if self._record else None + + +class AffidavitValidationError(Exception): + """Error de validación notarial""" + pass + + +def certify_operation(operation_name: str): + """Decorador para certificar operaciones automáticamente""" + def decorator(func): + async def wrapper(*args, **kwargs): + db = kwargs.get('db') + user = kwargs.get('current_user') + client_id = kwargs.get('client_id') + + if not db and args and hasattr(args[0], 'db'): + db = args[0].db + + if db and user: + affidavit = LegalAffidavit(db, user, operation_name, client_id) + kwargs['affidavit'] = affidavit + + try: + result = await func(*args, **kwargs) + affidavit.sign(success=True) + return result + except Exception as e: + affidavit.sign(success=False) + raise + + return await func(*args, **kwargs) + return wrapper + return decorator \ No newline at end of file diff --git a/app/core/auth.py b/app/core/auth.py new file mode 100644 index 0000000..0e2d790 --- /dev/null +++ b/app/core/auth.py @@ -0,0 +1,160 @@ +from datetime import datetime, timedelta +from typing import Optional, List +import os +#======================== +import jwt +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials + + +security = HTTPBearer() + +SECRET_KEY = os.getenv("SECRET_KEY", "xma-secret-key-change-in-production") +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_HOURS = int(os.getenv("ACCESS_TOKEN_EXPIRE_HOURS", "1")) + + +class UserType: + """ User type with permisions""" + + ROOT ="root" + ADMIN ="admin" + PRIVILEGED ="privileged" + ADMIN_LICENCIAS ="admin_licencias" + UPDATER = "updater" + USER ="user" + + + HIERARCHY = { + ROOT :6, + ADMIN :5, + ADMIN_LICENCIAS : 4, + PRIVILEGED : 3, + UPDATER : 2, + USER : 1, + } + + @classmethod + def has_permission(cls, user_type: str, required_type: str) -> bool: + """ VERIFY if user_type has permition equal o plus required_type""" + return cls.HIERARCHY.get(user_type, 0) >= cls.HIERARCHY.get(required_type, 0) + +class OperationalRole: + """ Role's""" + COMPRAS ="compras" + VENTAS ="ventas" + LOGISTICA ="logistica" + ADUANA_SOFT ="aduana_soft" + CLIENTE ="cliente" + OPERATIVO ="operativo" + +class AuthService: + """ Service auth""" + + def __init__(self): + self.secret_key = SECRET_KEY + self.algorithm = ALGORITHM + + + def create_token(self, user_id: int, email: str, user_type: str, rol_operativo: str) -> str: + + expire = datetime.utcnow() + timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS) + payload = { + "sub": str(user_id), + "email": email, + "user_type": user_type, + "rol_operativo": rol_operativo, + "exp": expire, + "iat": datetime.utcnow() + } + return jwt.encode(payload, self.secret_key, algorithm=self.algorithm) + + def verify_token(self, token: str) -> dict: + + try: + payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm]) + return payload + except jwt.ExpiredSignatureError: + raise HTTPException(status_code=401, detail="Token expirado") + except jwt.InvalidTokenError: + raise HTTPException(status_code=401, detail="Token inválido") + + def decode_token(self, token: str) -> dict: + + try: + return jwt.decode(token, self.secret_key, algorithms=[self.algorithm], options={"verify_exp": False}) + except jwt.InvalidTokenError: + raise HTTPException(status_code=401, detail="Token inválido") + +class CurrentUser: + """Modelo del usuario autenticado""" + + def __init__(self, id: int, email: str, user_type: str, rol_operativo: str): + self.id = id + self.email = email + self.user_type = user_type + self.rol_operativo = rol_operativo + self.ip_address = None + + def is_root(self) -> bool: + return self.user_type == UserType.ROOT + + def is_admin(self) -> bool: + return self.user_type == UserType.ADMIN + + def is_admin_licencias(self) -> bool: + return self.user_type == UserType.ADMIN_LICENCIAS + + def has_permission(self, required_type: str) -> bool: + """Verifica si el usuario tiene el tipo requerido o superior""" + return UserType.has_permission(self.user_type, required_type) + + def can_access_enterprise(self, enterprise_id: int, user_enterprise_id: int = None) -> bool: + """Verifica si puede acceder a una empresa""" + if self.is_root(): + return True + if user_enterprise_id and self.user_type == UserType.ADMIN: + return enterprise_id == user_enterprise_id + return False + + +async def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(security), + auth_service: AuthService = Depends(lambda: AuthService()) +) -> CurrentUser: + """Dependencia para obtener el usuario actual desde el token""" + token = credentials.credentials + payload = auth_service.verify_token(token) + + user = CurrentUser( + id=int(payload.get("sub")), + email=payload.get("email"), + user_type=payload.get("user_type", UserType.USER), + rol_operativo=payload.get("rol_operativo", OperationalRole.OPERATIVO) + ) + + return user + + +def require_user_type(required_type: str): + """Dependencia para requerir un tipo de usuario específico""" + async def dependency(current_user: CurrentUser = Depends(get_current_user)): + if not current_user.has_permission(required_type): + raise HTTPException( + status_code=403, + detail=f"Se requiere tipo de usuario: {required_type}" + ) + return current_user + return dependency + + +def require_rol(required_rol: str): + """Dependencia para requerir un rol operativo específico""" + async def dependency(current_user: CurrentUser = Depends(get_current_user)): + if current_user.rol_operativo != required_rol and not current_user.is_root(): + raise HTTPException( + status_code=403, + detail=f"Se requiere rol operativo: {required_rol}" + ) + return current_user + return dependency \ No newline at end of file diff --git a/app/core/base.py b/app/core/base.py new file mode 100644 index 0000000..831956f --- /dev/null +++ b/app/core/base.py @@ -0,0 +1,14 @@ +"""UNIFICACION DE LOS MODELOS XMA + Todos los modelos deben importarse para que alembic los detecte +""" + +from database import Base + +# Export target_metadata for Alembic +target_metadata = Base.metadata + +# Lista explicita de modelos +__all__ = [ + "Base", + "target_metadata", +] \ No newline at end of file diff --git a/app/core/baseRepository.py b/app/core/baseRepository.py new file mode 100644 index 0000000..f107a89 --- /dev/null +++ b/app/core/baseRepository.py @@ -0,0 +1,224 @@ +# core/base_repository.py +from typing import Generic, TypeVar, Type, Optional, List, Dict, Any +from datetime import datetime +from sqlalchemy.orm import Session, Query +from sqlalchemy import and_, desc + +from database import Base + +ModelType = TypeVar("ModelType", bound= Base) + + +class BaseRepository(Generic[ModelType]): + """ + Repositorio base con: + - CRUD completo + - Soft delete integrado + - Trazabilidad (created_by, updated_by, deleted_by) + - Métodos para root, admin, admin_licencias + """ + + def __init__(self, db: Session, model: Type[ModelType]): + self.db = db + self.model = model + + # ============================================ + # QUERY BASE + # ============================================ + + def _base_query(self, include_deleted: bool = False) -> Query: + """ + Query base con filtro de soft delete + include_deleted=True: incluye registros eliminados + """ + query = self.db.query(self.model) + if not include_deleted: + query = query.filter( + self.model.deleted_at.is_(None), + self.model.is_active == True + ) + return query + + # ============================================ + # CRUD BÁSICO + # ============================================ + + def get_by_id(self, id: int, include_deleted: bool = False) -> Optional[ModelType]: + """Obtiene por ID""" + return self._base_query(include_deleted).filter(self.model.id == id).first() + + def get_all( + self, + skip: int = 0, + limit: int = 100, + filters: Dict[str, Any] = None, + order_by: str = None, + descending: bool = False, + include_deleted: bool = False + ) -> List[ModelType]: + """Lista con paginación y filtros""" + query = self._base_query(include_deleted) + + # Aplicar filtros + if filters: + for key, value in filters.items(): + if hasattr(self.model, key) and value is not None: + query = query.filter(getattr(self.model, key) == value) + + # Ordenamiento + if order_by and hasattr(self.model, order_by): + order_col = getattr(self.model, order_by) + query = query.order_by(desc(order_col) if descending else order_col) + + return query.offset(skip).limit(limit).all() + + def count(self, filters: Dict[str, Any] = None, include_deleted: bool = False) -> int: + """Cuenta registros""" + query = self._base_query(include_deleted) + if filters: + for key, value in filters.items(): + if hasattr(self.model, key) and value is not None: + query = query.filter(getattr(self.model, key) == value) + return query.count() + + def create(self, data: Dict[str, Any], created_by: int = None) -> ModelType: + """Crea un nuevo registro""" + import logging + logger = logging.getLogger(__name__) + + try: + # Eliminar campos que deben ser automáticos + data.pop("created_at", None) + data.pop("updated_at", None) + data.pop("deleted_at", None) + data.pop("last_login", None) + data.pop("id", None) + + if created_by: + data["created_by"] = created_by + + logger.info(f"CREANDO ENTIDAD {self.model.__name__} CON DATOS: {data}") + + entity = self.model(**data) + self.db.add(entity) + self.db.commit() + self.db.refresh(entity) + return entity + + except Exception as e: + logger.error(f"ERROR EN CREATE: {str(e)}") + self.db.rollback() + raise e + + def update( + self, + id: int, + data: Dict[str, Any], + updated_by: int = None, + include_deleted: bool = False + ) -> Optional[ModelType]: + """Actualiza un registro""" + entity = self.get_by_id(id, include_deleted) + if not entity: + return None + + # No permitir actualizar si está eliminado + if entity.deleted_at is not None and not include_deleted: + return None + + # Actualizar campos + for key, value in data.items(): + if hasattr(entity, key) and value is not None: + setattr(entity, key, value) + + if updated_by: + entity.updated_by = updated_by + + self.db.commit() + self.db.refresh(entity) + return entity + + def soft_delete(self, id: int, deleted_by: int = None) -> bool: + """Soft delete: marca is_active=False y deleted_at""" + entity = self.get_by_id(id) + if not entity: + return False + + if entity.deleted_at is not None: + return False + + entity.is_active = False + entity.deleted_at = datetime.utcnow() + if deleted_by: + entity.deleted_by = deleted_by + + self.db.commit() + return True + + def hard_delete(self, id: int) -> bool: + """Eliminación física""" + entity = self.get_by_id(id, include_deleted=True) + if not entity: + return False + + self.db.delete(entity) + self.db.commit() + return True + + def restore(self, id: int, updated_by: int = None) -> Optional[ModelType]: + """Restaura un registro soft-deleted""" + entity = self.get_by_id(id, include_deleted=True) + if not entity: + return None + + if entity.deleted_at is None: + return entity + + entity.is_active = True + entity.deleted_at = None + if updated_by: + entity.updated_by = updated_by + + self.db.commit() + self.db.refresh(entity) + return entity + + # ============================================ + # MÉTODOS PARA DIFERENCIACIÓN DE USUARIOS + # ============================================ + # Estos métodos serán sobrescritos en repositorios específicos + # si el modelo tiene los campos correspondientes + + def get_by_user_type(self, user_type: str) -> List[ModelType]: + """Obtiene por tipo de usuario (root, admin, admin_licencias, user)""" + if hasattr(self.model, "tipo_usuario"): + return self._base_query().filter(self.model.tipo_usuario == user_type).all() + return [] + + def get_root(self) -> List[ModelType]: + """Obtiene usuarios tipo root""" + return self.get_by_user_type("root") + + def get_admin(self) -> List[ModelType]: + """Obtiene usuarios tipo admin""" + return self.get_by_user_type("admin") + + def get_admin_licencias(self) -> List[ModelType]: + """Obtiene usuarios tipo admin_licencias""" + return self.get_by_user_type("admin_licencias") + + #=============================================== + # el tipo de usuario "PRIVILEGED" un esta por definir que clase de permisos tendras + #================================= + + def get_by_role(self, role: str) -> List[ModelType]: + """Obtiene por rol operativo""" + if hasattr(self.model, "rol_operativo"): + return self._base_query().filter(self.model.rol_operativo == role).all() + return [] + + def get_by_enterprise(self, enterprise_id: int) -> List[ModelType]: + """Obtiene por empresa""" + if hasattr(self.model, "enterprise_id"): + return self._base_query().filter(self.model.enterprise_id == enterprise_id).all() + return [] \ No newline at end of file diff --git a/app/core/dependencies.py b/app/core/dependencies.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/intefaces.py b/app/core/intefaces.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/security.py b/app/core/security.py new file mode 100644 index 0000000..dd464e5 --- /dev/null +++ b/app/core/security.py @@ -0,0 +1,64 @@ +# core/security.py +from passlib.context import CryptContext +from datetime import datetime, timedelta +from typing import Optional +# +import os +import jwt +import logging + + +logger = logging.getLogger(__name__) + +# Configuración de hashing de contraseñas +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +# Configuración de JWT +SECRET_KEY = os.getenv("SECRET_KEY", "xma-secret-key-change-in-production") +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_HOURS = int(os.getenv("ACCESS_TOKEN_EXPIRE_HOURS", "1")) + + +def get_password_hash(password: str) -> str: + """Genera hash de contraseña con bcrypt""" + + if password.startswith("$2b$") or password.startswith("$2a$"): + raise ValueError("La contrasena ya esta hasheada") + + password_bytes = password.encode('utf-8') + if len(password_bytes) > 72: + password = password[:72] + + return pwd_context.hash(password) + + +def verify_password(plain_password: str, hashed_password: str) -> bool: + try: + return pwd_context.verify(plain_password, hashed_password) + except Exception as e: + logger.error(f"Error verifying password: {e}") + return False + + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: + """Crea token JWT""" + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(hours=ACCESS_TOKEN_EXPIRE_HOURS) + + to_encode.update({"exp": expire, "iat": datetime.utcnow()}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + + +def decode_access_token(token: str) -> dict: + """Decodifica token JWT""" + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + return payload + except jwt.ExpiredSignatureError: + raise ValueError("Token expirado") + except jwt.InvalidTokenError: + raise ValueError("Token inválido") \ No newline at end of file diff --git a/app/core/servo.py b/app/core/servo.py new file mode 100644 index 0000000..0e6555c --- /dev/null +++ b/app/core/servo.py @@ -0,0 +1,91 @@ +# core/servo.py +""" +XMA Servomotor - Gestor simple del ciclo de vida +""" + +import asyncio +import signal +import sys +from enum import Enum +from typing import Optional +from datetime import datetime +import logging + +from database import test_connection, dispose_engine + +logger = logging.getLogger(__name__) + + +class ServoState(str, Enum): + BOOT = "boot" + RUN = "run" + STOP = "stop" + + +class XMAServo: + """ + Servomotor simple que: + - Verifica BD al arrancar + - Ejecuta tareas en segundo plano si es necesario + - Maneja señales de apagado + """ + + def __init__(self, mode: str = "dev"): + self.mode = mode + self.state = ServoState.BOOT + self.boot_time = datetime.utcnow() + self._tasks = [] + self._running = False + + async def start(self): + """Arranca el servomotor""" + logger.info(f" XMA Servomotor iniciando (modo: {self.mode})") + + # Verificar BD + if not test_connection(): + logger.error(" Base de datos no disponible") + sys.exit(1) + + logger.info(" Base de datos conectada") + + self.state = ServoState.RUN + self._running = True + logger.info(f" XMA Servomotor en estado RUN") + + # Si necesitas tareas en segundo plano, las agregas aquí + # await self._start_background_tasks() + + async def stop(self): + """Detiene el servomotor""" + logger.info(" Deteniendo servomotor...") + self.state = ServoState.STOP + self._running = False + + # Cancelar tareas + for task in self._tasks: + task.cancel() + + # Cerrar conexiones + dispose_engine() + + logger.info(" Servomotor detenido") + + def get_status(self) -> dict: + return { + "state": self.state.value, + "mode": self.mode, + "uptime": (datetime.utcnow() - self.boot_time).total_seconds(), + "database": "connected" if test_connection() else "disconnected" + } + + +# Instancia global +_servo = None + +def get_servo(mode: str = None) -> XMAServo: + global _servo + if _servo is None: + import os + mode = mode or os.getenv("XMA_MODE", "dev") + _servo = XMAServo(mode=mode) + return _servo \ No newline at end of file diff --git a/app/modules/__init__.py b/app/modules/__init__.py new file mode 100644 index 0000000..e33b26b --- /dev/null +++ b/app/modules/__init__.py @@ -0,0 +1,52 @@ +""" +UNIFICACION DE LOS MODELOS XMA +Todos los modelos deben importarse para que alembic los detecte +""" + +# Importar Base directamente desde database para evitar circular imports +from database import Base + +# Importar todos los modelos +from app.modules.clients.models import Client +from app.modules.users.models import Users +from app.modules.affidavit_logs.models import AffidavitRecord +from app.modules.branches.models import Branches +from app.modules.coments.models import Coments +from app.modules.configuration.models import Configuration +from app.modules.credits.models import Credits +from app.modules.edos.models import EDOS +from app.modules.efos.models import EFOS +from app.modules.feed.models import Feed +from app.modules.files.models import Files +from app.modules.interactions.models import Interacction +from app.modules.invoices.models import Invoices +from app.modules.license.models import License +from app.modules.location.models import Locations +from app.modules.moves.models import Moves +from app.modules.suppliers.models import Suppliers + +# Metadata para Alembic +target_metadata = Base.metadata + +# Lista explícita de modelos XMA +__all__ = [ + "Base", + "target_metadata", + "Users", + "Client", + "Locations", + "AffidavitRecord", + "Branches", + "Coments", + "Configuration", + "Credits", + "EDOS", + "EFOS", + "Feed", + "Interacction", + "Files", + "Invoices", + "License", + "Moves", + "Suppliers", +] diff --git a/app/modules/__pycache__/__init__.cpython-311.pyc b/app/modules/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..945413e28d4ec2d3a4ba01ebd045ebf97c09beeb GIT binary patch literal 1768 zcma)4OK;Oa5MJl$q)F2@>66l$QlLdNJWdD!Qb}7WQXdip3GKyl?M*P+wS(k3sS=@`XcB+#+#7oY>i9i?{SZGXDJS&Ukiq=KE5umJ$Db?|yU7@(7*5LjE#i z^79ubpOAwvax^DHHI~5{4S1GjSq|p}&(S<9;DX?JT4Xxb1uxJNE90`@MLNMMxFWbt zt85Za3SOc$Hif4IFVkr@gJ%Svpmo;34Z$mPmd)Wg!K-whE#L*gC+Q+v!b^hJ=oPk% zmj$1qSJ?_)5qz4iG6Nfe&(LdZ4X+7ar`Op!Ue^%GytyH-jc<05)A+36Th8n;+MIiD zm0upV_u8%PR{QAC*x5DqkB*Ilqn%xNzdG19$}fD!4-Lv6<~s!54mly7;j*3|L{<T&}z%~I&QZgSdr^{!WR(76~5f=&e1X7%I)rr-g|^V zs{EcyMV?CAi%4ME_V`=9?Vb9rJvw8b6HgRa+V^dd9uCYO_@^SEblmUtc;&p{dXVK; zN^1P_T@sm$M3!Um1=kn~asV>`1%M)e4p0IBK?V~56@V&04PX*r3Sb(b4$$CmxVe!O zP1Esh(@boxgM`uuuLTXZ#qHKCy>BUwlX^r6|(4W%a6(r!zRz@&cn zJPz)VTuH~BjOQXV!37hwlF_te09-yX(kblp3}qO!=R%&*WMu%fE}2i~AzYZ!3q8Hy z?fmsZvsG`0YeRkuQ&4-=0mVo+j3!x4AMB2F1Ts30f+?zweDd0@Kg4e3a8G5{v8 zrl4ab$_UVvS}6m0N;jw`Sx#3PO3}&?&{U$IOLi8PTwfNYruh~9`7Tc(p0T*@ znx^Zyk!d!2?|$o#N4rmhCJ)y56ZSa{vzn$2Gg&P+L;w|}EyZXtZNmc6nlai++W_5; zRU4ouv1$W!FIH`Ucpk+D=uxcNWicBuQf#0ux%aZ2e=B?#BgOu&!uv5&?6SK*h>>Ev aXZ87yTOVq}LSrH`L=rv};UC=L?f(OCC=xjU literal 0 HcmV?d00001 diff --git a/app/modules/affidavit_logs/__pycache__/models.cpython-311.pyc b/app/modules/affidavit_logs/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1a40a8e771ca07885d691b6de926e68d35dd12b5 GIT binary patch literal 3746 zcmb_eO>EoP5vG19Qlun{isQdz6DR9#q6K0Dn`AdAk_L|D$jUmh6D!+{|4gO55aurAPpPpOOekYft$!3Fk^OA8X!w(HSEmM~tW(HDYqi zh|6(U%oP z3nVd9(;J3KxRUAM6>O2jl4EJ+3gHW?gDaYW342|ysSe?;)oMCcO%ku*rw$RW*DS29 zn4jZy5}!3224PDk!cX%N5?^kZRrpU?SXUjbX4((6I+b5jZCtK)JPH0HoynWiz3assh3hZ5j7NBAX2bsF}_jV$@JWr+i9L9ye6Iqe;#z++3WWt;`k( zw>W*fQktHJ0F_GtBbFDG#oG%rvolm(Dt|FOUn=~Kc|+gihy0XUuLmPEZBMDo%NkOj zXpW-SR_rMdW8I#tuUit}0|NVwvCjZ$zPsDmW6e@KGu$kOBzLm;dB8P`&7w8lU3qvY zCqeyFp_F;V$w|b^0!l(y1(IB-V=MSrgaZ%5s8%fun*ie1sH1>kVQ;`ZVbp9YkL%dB z32SI(!zO%HHLI`^qAXg&)Ws1>Mu8BnFxv^|%HPNcuhK_2&1n{y;_<9LFq*BG{cf@xJU#P%R7&^N2;UokVZuVSYca zVKvvQm4(nYP!aLZATOSGa|7zgPO=Z3I)D~_5}gjv8Sp9jEDDxkgqJ)(H88SBJj6;b zR_b6moI|NY-Y>U$q-*z(_k-F6vCAIf15~TPhap&%?)3sR!>e=~1=lm6&pSNBfTc`d zR7QWHrRdH;pQW)Mw-kJh-pCxIXv_lrU%uV(pN0P5XQ593`XtKY)Jg zPb-Q;qaU7@7F0>bic{C4-~hLDYo4|PVU?bux?fX5`z&jYk{guJ7Kexk+f_@e(;mTM zsYFoGYQuKu#|#h1;}m?As^T_u8y0QKtTdg+%-Wh&rkv0@p1@lobPlh8@4m%@^?eNG zyO;;-RwghV1UV5#(MnNKrt^-9YOUCc(tiY91dK(=7!2Eha&X`X8k}g}{4P1LG1TI= zDlcYV4Z6v)mn{3qa`Rd{JJd?F)>>)U^L#k#{~Z@BCam)*One|N>Tp19dHFT3Vv z*JuMUxUt;&?dIy{>h_p-;(~wT!b``^6ueBq&lKR83vF!)Tf+8bZ~T%!erflDo1OEr zbAEOX*5^iAA8ih9HQd}eFL%z*or5ky8g313Cg8UJEdE8@My_<;lg|6n`R0vw`pCu? ztzxUVIk$aj_ozEq^ahLmV9~vG&%dR*dfiPw_R^32^yB9BwkU0U+WKhgqATV-G4G4{ zW??VEz!Qfy`AvR%!X26NMyC9csa<|I{w@Cx;akDIBfHXlPrC0*_u*-QC%xF0XJ1Zy zbN25afAjGx$CWCcRPm+Ck3c6}>8dAP^`)!L8{H>!TVq>e+c&&ZpZTXg+XY0?lZw7n z?3(!HdU!%=WaIKCx4pKlxpQ}2@t!B%^Tm76RHR2Yez$q{MgGMxH+{uRU-8pdn%7}L zGPA+DnbTV(kDv7U$u^&+^NFoT9{-8Yf6|$s**@m+7k&QXJN-kCpYr*s&iq$ffAshZ zK7V0PObjKOWqOC53*CSDVItU%bycrE#K!u5E(YloW(!C6Ds5_lqze;Kq|;lJgu8v; z#3Zm`f@KZ}YnBnF3J;B-`T+WPi#A^*j%nu~vmGl;SK3SkdC`|JKk%2shWIxYEvvM( zv5y1Yi$x-lHeGuBG%o(@$*JeiD-rskO*bMHyI2#$+W8NnlcB}AJ={427 r-B)M$$mkyPK54~Tm9Or-rkc0=>Y?#SVvl*Bw9soReS3&1_{jeOmTTd$ literal 0 HcmV?d00001 diff --git a/app/modules/affidavit_logs/models.py b/app/modules/affidavit_logs/models.py new file mode 100644 index 0000000..a3da56f --- /dev/null +++ b/app/modules/affidavit_logs/models.py @@ -0,0 +1,58 @@ +from sqlalchemy import Column, Integer, String, DateTime, Float, Boolean, Text, ForeignKey, Enum as SQLEnum, Index +from sqlalchemy.sql import func +from sqlalchemy.orm import relationship +from database import Base + +import enum + +class StatusOP(str, enum.Enum): + COMPLETED = "completed" + PARTIAL = "partial" + INITIAL = "initial" + NO_PROCECED = "no_proceced" + INVALID = "invalid" + +class Operation(str, enum.Enum): + DELETED ="deleted" + CREATED ="created" + UPDATED ="updated" + TRIED ="tried" + MOST ="mosted" + LESS ="less" + MINUS ="minus" + CANCELED ="canceled" + + +class AffidavitRecord(Base): + __tablename__ = "affidavit_logs" + + id = Column(Integer, primary_key=True, nullable=False, autoincrement=True) + operation = Column(SQLEnum(Operation, name="operation", create_type=False), default=Operation.TRIED, nullable=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=True) + client_id = Column(Integer, ForeignKey("clients.id"), nullable=True) + description = Column(Text,nullable=False) + adjustment = Column(String(100), nullable=True) + status = Column(SQLEnum(StatusOP, name="status", create_type=False), default=StatusOP.INITIAL, nullable=True) + sign = Column(String, nullable=False) + + #timestamsp + created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) + updated_at = Column(DateTime(timezone=True), onupdate=func.now(), nullable=True) + deleted_at = Column(DateTime(timezone=True), nullable=True) + + #trace + created_by = Column(Integer, ForeignKey("users.id"), nullable=True) + updated_by = Column(Integer, ForeignKey("users.id"), nullable=True) + deleted_by = Column(Integer, ForeignKey("users.id"), nullable=True) + + #relationships + user = relationship("Users", foreign_keys=[user_id], back_populates="affidavit_logs") + client = relationship("Client", foreign_keys=[client_id]) + + __table_args__ = ( + Index('idx_affidavit_user', 'user_id'), + Index('idx_affidavit_client', 'client_id'), + Index('idx_affidavit_operation', 'operation'), + Index('idx_affidavit_status', 'status'), + Index('idx_affidavit_created', 'created_at'), + ) \ No newline at end of file diff --git a/app/modules/affidavit_logs/repository.py b/app/modules/affidavit_logs/repository.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/affidavit_logs/route.py b/app/modules/affidavit_logs/route.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/affidavit_logs/schema.py b/app/modules/affidavit_logs/schema.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/branches/__pycache__/models.cpython-311.pyc b/app/modules/branches/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..81925fdc7481f52d4963f79bb0e05cff819841f6 GIT binary patch literal 2733 zcmb_eO-$TI6!!jk?OpcA%WohdGzl$>l&nyK6dFh*LJ0(jRE4OKX=|CCfjF_f8+(@o z(pEn7z>!Lox<^iwDjYacDF-U$kV_?7@?kY6q@Hr4N)J^o?HhZSWcg8~s$3UG`=HvSCB%fsht#B&5>p-@R@0gw3LYO(Gh!yl++q;>J%gfH z*a$JqbLf5(n^EL0ljUC1VKzqCn+3J3=_ER-m+&Gs2z$3=DEcDd#^e&7RWwW@<7z=J zk?3flpki4kkx3olrzAX!pO#2$ykKBuQJ=!gB>b6P*0KQ-UMTB%_%<0>l}k!NHyI_ju?tX6qc)sc44}YP z`=MQ*@w6Q{ajdr6)1E?|TV0K~;#qVWvhfVzMh#idKfos8=4;&Scs4}hMMKeKV_A9x z`NipFRh8#eOcHXrR8aK1fi*^Dyo3#@HpfE%F;~#bMFcg$hiq2RQ>pH%?ID~b>9U3;iNqvHE1pt&qKK*!FXI@^zc}WrL@P$(rAiY%H3R zWF)DOt(_Nla4umQ_g|Pdpk>~k&&kE2*Zp9ax%xrpK!8>AQgPW3z|tdh8S^5PTkGJd zwsc!FdwgoOZ7sS!wmtYNZ1Xb?KjZQ<)@U`|w#Ke4tu0mZKRo;K+2%bvec4H0cGH)w z$!cv*ug&*4e4oqrS)+TIjrU8Ck2vWOH$7ra)}-sP_1NaH({t7Bxw?DBPER=L2{%0ft0!Al z7uLS0Jghw2>~y-XxZPKFN_KM0NshV6G3(Yo%fPyA6|TZ{aL zm%CUSAK35nkv7)4v!7v-ovU9~`nI#%U9Uttao~Q<61Njkm83SEAdT9UpSyLvDP?8l#UfQvZLlEhJ_>R%I2+pSJuiv%IYGbA#Rz zyoB=pX@g#|{A_9hjzsT%K22{PP`M6ZE&=PO)ceAMq4^2GK_Qdp;WlEVC_)&+bpMYY zrr4fu*MWP>4f`o@-S`ZK^p;`n0@)7*0)Z;?*#1A>Ds$Fue5=eScH>)RPT7s`9uv2` YXP@m2e7esZC99=Bsr0X7RNfB%1sj#?3;+NC literal 0 HcmV?d00001 diff --git a/app/modules/branches/models.py b/app/modules/branches/models.py new file mode 100644 index 0000000..4413795 --- /dev/null +++ b/app/modules/branches/models.py @@ -0,0 +1,47 @@ +from sqlalchemy import Column, Integer, String, DateTime, Float, Boolean,Index, Text, ForeignKey, Enum as SQLEnum +from sqlalchemy.sql import func +from sqlalchemy.orm import relationship +from database import Base + +import enum + + +class Branches(Base): + __tablename__ = "branches" + + id = Column(Integer,primary_key=True, nullable=False, autoincrement=True) + direccion = Column(String(255), nullable=False) + cp = Column(Integer, nullable=False) + #physical + is_physical = Column(Boolean, nullable=False) + location_id = Column(Integer, ForeignKey("location.id"), nullable=False) + #validation + is_active = Column(Boolean, nullable=False, default=True) + #cliente_id + client_id = Column(Integer, ForeignKey("clients.id"), nullable=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=True) + + #timestamsp + created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) + updated_at = Column(DateTime(timezone=True), onupdate=func.now(), nullable=True) + deleted_at = Column(DateTime(timezone=True), nullable=True) + + #trace + created_by = Column(Integer, nullable=True) + updated_by = Column(Integer, nullable=True) + deleted_by = Column(Integer, nullable=True) + + #indices + __table_args__ = ( + Index('idx_branches_client', 'client_id'), + Index('idx_branches_location', 'location_id'), + Index('idx_branches_user', 'user_id'), + Index('idx_branches_active', 'is_active') + ) + + #relaciones + + client = relationship("Client", foreign_keys=[client_id], back_populates="branches") + location = relationship("Locations", foreign_keys=[location_id], back_populates="branches") + user = relationship("Users", back_populates="branch") + diff --git a/app/modules/branches/repository.py b/app/modules/branches/repository.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/branches/route.py b/app/modules/branches/route.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/branches/schema.py b/app/modules/branches/schema.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/clients/__pycache__/models.cpython-311.pyc b/app/modules/clients/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3087ae783eb4121cfa18a3e41f3a859f51666c5d GIT binary patch literal 4674 zcmb_fO>Emn7N&kl{hO9-%Rl*Z-6Tr0@owXFH;wDWKeFT4krP=-vkVG?7RR<+l2V3J z8hg6{4?Xy(070u_vw%-oFY1GjS)hk3&_gZ@kN^(?PX+drn}Y;<=wqmbLe5&3QiRC0Jl80{?BS|P1ii)~YC=vO%prTB^ zVTt{5H43kf1M>}qD9$BPoLh8p9?{KtMGxl_y`0~+*>Cx7AhA%i{IZ}_1j+J30qjB# z^GLGjB_Us~U?G#DEr#b6K}I}p1$kaBiWQ0QVV=Ja)fyWAjj_z_u{p~-IWwQLeCfLr z={bv@8k@-7&W`<+dQA-bsee!?mF(APMIFpb1*DYKK`0|hy--@iEC6l70HgK;(f03p zq(hskhgfa09*%42dbCTMCO0O-wJRO&jUG@RKwmWqpllu7w@n@SJxFazn{sNBO22Saf%y``_IRvd2_v=a@KFI{q=LG(Wc{7_{$k_v2NN4ZNz(^NvX6L8U&XCg!R&p$xO=l*@Zp@@Jx%q{e z%#GXW%=jE=eDk^N{6eb-Qp5v<#NkzmQx16G%thThP(LxKd;9F^)zVtp@d2sP&FecRFIo>6SIZO>-V z#+tPpyI;FqY&)cO`zo1a`#PjH?9ujIL{W#x9#Xqwm8`U*YIl*^C<(Ld+iq{MtJ~K3 zfcA*-Lww+A+P`}bpc5@@SSD|~3ovC!E!K+)X*>#;2MF^A`yk^f zDwUE5MmU|e6~u>p!xcPf)}jgJ3!;@csM_nt+V7D-e7A`q&}w+eNmz&nRV%tEM{4n0@MBSi(LI(>tDmdKfLIBfGPNX_r>${;5DJgh+K_3wZ{ zZt41xS_RIz{sfsLl=tya_CR@mpM|pb&onvTGwY!dXZFt)#81}!=TG9L{3q)%_zK`O zUM=Peg7nF{J2jf}SZ)}f2LO<89ng}5OGVO>O8PsA$x0`Q5k@7<^XoCE`3rl38BnX| zfM`@b#?^99KGkC!JX@37*BtJ7&P?BNSV9@jKI$^53#}2fJHlQ?SmT+5~&lTBo}ow0UKIS`9?J05D4%TA=d~Y{|Gn&#PTS` zFRY-m8bluQ;Gh$f4i8BnDkF#A!z`}7#SnfEv$%#yifF}o$M+34@d92}G0CtjuN~IR zegxA}BleiZMzG=A#ITk$?Keb52uT~&UU2wg+{>y=bx*At#J?L%q|r)0^>cKX~%Nj_+AckB=Mi zaWg)y&D9e<)v4NWZFsZ)+mD}o3|di7Oc;p?GclpvsJC@i{nZE62erjV>yOvB(t6v6 zM%#yG+lSiBE<;7)VAV&}k7~c&{PKB+9-lVi(`I~HzjepF^_jjR>tV$RD`r@Me4Fh8 z3+WoYNpC4H-q({ijpR);c~j>ElUvc%hdR4vuxlo}1{QbpR`1lNYtx%E`p~4F{Z#L} zYxLbU`|j$CpPP&KcPZ+Ru5mYfIw#z(Z2HEgTBgy>*1$8L&W;-FsL76M*ME#AHkPWN z)>dn)TRq0{A@lgqvx*)~8_~2GO>5J2aNp@#*XFs+b6cMo{g=)D%g;r9e0uccX3AjB zo6Pw-6Cve+P1#_EOlD};=Sldrx!ougPHv3U=&did3NHeBC~JhWW+)3kOR?UKFKe?q zf7p@r*rXAgG-H$Svk~ptkZPB9?&{G|BRXnEN5P3U^7_t0J$BWIT{UA@wQIY6m+ziy zmpYs}2|8IVsU++@_a#AsU0hx}uus`rl)VFx?c83pm~0rB{J%RpMVN5+fWYCk-^2xA zolWDQu!v=6bu)0535)OxjLFe!dC7TedDJp?wzE^DN%G@hFXfgs{u49e{DF>Q3=p!V zt2v;%9+%5ir?CG2c" + + diff --git a/app/modules/clients/repository.py b/app/modules/clients/repository.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/clients/route.py b/app/modules/clients/route.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/clients/schema.py b/app/modules/clients/schema.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/coments/__pycache__/models.cpython-311.pyc b/app/modules/coments/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..82cac0d441dc8d0548d70a1005f8326e26e4bfd6 GIT binary patch literal 2345 zcmb_dJ#5=n5GEx`r2c5hl5ANH+$4q3s0b3nap538E)Y9*61M^BxOU1KAZWd3g$aKY zN=o7sC^U4?sI3|`xhmqpqg!Nffec*=kN^kaO$Iu3bCFJ&dXJP7%Q0G@=p*0p?!J4+ zyW_pb9}OIrzGo1FSdGzHiPZyAKo3emos&2{B!%>_ z6xMl(*9A$?BT__t&2ei1JkP;pr)I@zi8t`~dOX?{p#~>c3 zwt@`v0;WHtGz#5g3c?>$EJR6o*3@dcLHOH76_>C@!uP6{YLtjDr&RH>s$&wmZ6N%N zgyuCHG0!@Vg5XIw<|#ojm?B`2eovqsc$hqlVhflThXMl` zM2~q&{YOuM?btC-&;_)DK;N$gtz3UC(*I!ne&sS?okek!=&!%0716*6+9XPypdCc% ze(eS)4S|9Pp~ETP|p_Bf@##s2=9^3m(6kw_AIUt zZXN=H0-S{c+%1d}L6!|g$FfYKvaFk^rcpjF%a3b{)qq7_K+K!77nO3^n|@xEi@lF>5oD}YE|fQ|6c~Dh z##;FqPL=xGtD8Pm|9n3(&=_j+?d6@DcI1i^x#C8y)Ngd-gN^y-RD0ZxpL62p-1xcr z&90bfY&2I}4_gmAd1q|O9h=&%+Txre&bi`TeW9C3H&%N1h%5NXgqm#S$cb8w@|4Di&*-I;SX4T28x|vnT z%k#^(^X+`+j&o+(Ju|&GZl`WJsatO97Wm4JHdk5;ZK*BoM4j=g?)cR`WM>zh?4p}p ztlvEln26X2+2ZMT!4b~8!uhU{I7$Anz2*p$t}xld&$bsGVZs$A4kDqUFgQvw@m%9t zi|;(`sJ}++*pd@la$`&Nxo&J2^x;ma^OYT&c4E_RY#Ov;zM-|IcRt=ZWs5V8IOB>l z^#yuaQ->e3kRj2^V@=VD53s)Zu3;_uulAdfwv;7M;&mzQV!wUkz!Co57oX9;9_;wH zZY}`p_wZ4rY3Y6^3UB}@>+mPE2B-%P!*aj9E>YiJY@Y!4M!4p;;X9l zvMk$WEc^esyUYi6|J`Livit8YGh+AO`%J9vT?gS&_R|CAebPXGQte+SKwgG_11bM} AX#fBK literal 0 HcmV?d00001 diff --git a/app/modules/coments/models.py b/app/modules/coments/models.py new file mode 100644 index 0000000..05a94f7 --- /dev/null +++ b/app/modules/coments/models.py @@ -0,0 +1,37 @@ +from sqlalchemy import Column, Integer, String, DateTime, Index, Float, Boolean, Text, ForeignKey, Enum as SQLEnum +from sqlalchemy.sql import func +from sqlalchemy.orm import relationship +from database import Base + +import enum + + +class Coments(Base): + __tablename__= "coments" + + id = Column(Integer, primary_key=True, nullable=False) + texto = Column(Text, nullable=False) + user_id = Column(Integer, ForeignKey("users.id"), nullable=True) + feed_id= Column( Integer, ForeignKey("feed.id"), nullable=True) + + is_active = Column(Boolean, nullable=False, default=True) + + created_at = Column( DateTime(timezone=True), server_default=func.now(), nullable=False) + deactivate_at = Column(DateTime(timezone=True), onupdate=func.now(), nullable=True) + + created_by = Column(Integer, ForeignKey("users.id"), nullable=False) + deactivated_by = Column(Integer, ForeignKey("users.id"), nullable=True) + + #index + __table_args__ = ( + Index('idx_coments_feed', 'feed_id'), + Index('idx_coments_user', 'user_id'), + Index('idx_coments_created', 'created_at'), + Index('idx_coments_active', 'is_active') + ) + + #Relaciones + user = relationship("Users", foreign_keys=[user_id], back_populates="coments") + feed = relationship("Feed", foreign_keys=[feed_id]) + creator = relationship("Users", foreign_keys=[created_by]) + diff --git a/app/modules/coments/repository.py b/app/modules/coments/repository.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/coments/route.py b/app/modules/coments/route.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/coments/schema.py b/app/modules/coments/schema.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/coments/services.py b/app/modules/coments/services.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/configuration/__pycache__/models.cpython-311.pyc b/app/modules/configuration/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b414f7b12d86ffecd1bb911fffd15b31d9ccffd3 GIT binary patch literal 2235 zcmb^yOK99iRMPJ1x8AknY-}eKT1eV@Q`p^xVhU**NKzLkt)b2KP#G%Z)!0$0&t{~x z*AA3D^pN8g3U#4}oLm>@(4zxBq>w`{0SR z6C6Q=vK4nb)d&U%yx{+g; zEtqN3PMC!oOjUeJ(W*@NE1unPTq4Z60a`+s@HYZ%xl2U6Y6PfhIfxWyY|jXYaM|;0 zWVoa->zb%b@=eqYh&K8f!8dXR=z3V^AIyOb&YPetrYIxdk(A8c(S(1+%ch0tIH;Ozu9v z0kXDWrcD!vk`7U%*N`H=25&L>;)t08ZG;2{y;I>MhPj^}<&3o29+#5i{-GAA?2;b@K@OPK)5q**ja_Q!vb z)lQiu$oVoUUGdySYpH{itR$t@t7vnknj^(Fwj2Ym>fb^>myAx}S#Ap>2e|aUM>92=Kz7w?do1PzJVB5n(umgIMQo_(Se1FBmCKjoPH|RpE6ql%soa?O+89V~I zn>zdzl z=J{0JXt$G@+V0o)1+EUK$o6aPRjdMgvQTk8t*+jSKB%1OeZDJ=-W~4?_nO<4$AwUu zi=??&n(JNO;~90L&-a)6%j>Nl?)-RXvl%MqBjtRoobSyJlyRV~Y*s?07Adt@sSVGL z;jf(T&#X_cPv1N9^Mzk7{9Xww(@|wQu1tdlnXHrB`B1qSDHmhqVs93{KQ`GPUl-TK z&9mWTJ({e?ll6zU{6o0CYXbyFImic4z9rxt(*5gHUZo zYBN@w$FQCa)k~3jDON85L!wW&-v#R?YSBb3o~Uhiw}YMTgSDNtu=#bUXpy4Diq@Om zO};B^z8$KKNNvPw1MY*~@2pR3eh`i|qOnFi*63YN^%KAnwH~SUSgrT2frV1}ZXqnc zb??1Mti@t&FUO7Zy*b)(V;HzKBMmtl->(5H2~B6ef7}84ldIxB2=QW0B;i>rDz_(^)!|0RBWQ#`v5%`KS~qu zWBehY^v&@f!m^iRS$4qS@c$SC=FM>b7%=aK`^SJe749FqOtF{rJ^mES?=gp${?)(f L=|4x15)1zYK)qH7 literal 0 HcmV?d00001 diff --git a/app/modules/configuration/models.py b/app/modules/configuration/models.py new file mode 100644 index 0000000..1bb4298 --- /dev/null +++ b/app/modules/configuration/models.py @@ -0,0 +1,34 @@ +from sqlalchemy import Column, Integer, String, DateTime, Float, Boolean, Index, Text, ForeignKey, Enum as SQLEnum +from sqlalchemy.sql import func +from sqlalchemy.orm import relationship +from database import Base + +import enum + + +class Configuration(Base): + __tablename__ = "configuration" + + id = Column(Integer, primary_key=True, autoincrement=True, nullable=False) + + #SMTP data + smtp_host = Column(String(100), nullable=True) + smtp_port = Column(Integer, nullable=True) + smtp_user = Column(String(100), nullable=True) + smtp_password = Column(String(100), nullable=True) + + is_active = Column(Boolean, nullable=False, default=True) + + + created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) + updated_at = Column(DateTime(timezone=True), onupdate=func.now(), nullable=True) + deleted_at = Column(DateTime(timezone=True), nullable=True) + last_send = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) + + created_by = Column(Integer, nullable=False) + updtated_by = Column(Integer, nullable=True) + deleted_by = Column(Integer, nullable=True) + + __table_args__ = ( + Index('idx_cofig_active', 'is_active'), + ) \ No newline at end of file diff --git a/app/modules/configuration/repository.py b/app/modules/configuration/repository.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/configuration/route.py b/app/modules/configuration/route.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/configuration/schema.py b/app/modules/configuration/schema.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/credits/__pycache__/models.cpython-311.pyc b/app/modules/credits/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b551feaf1440d51fff3bd1b277e74a698c616fba GIT binary patch literal 3087 zcmb_eO>7&-6<(4{a=9c$k(4M>mTg6KWs^1}qc$WU5sFGQl`y6#R8kCQjd-!*jO2~? zhu&SfmeB(bJ?O{)2Eqb8@T4km4m##ks6Yiit_$YB+zNLl173wRm4?y{xK@9RKjOr77s$cM{0U@BW z0;>jvpc)cFYFG%XoWQ9OA)@j^)JO4tC8oxOxTgn{5j7zsJe^fW)ufR0^q`UwQhw%r z28I5@pfFD3)TiE%vHf~$%yStPuo>k5`M!fxSb7U5TQ1IxSGUA#xw zlGaf3KEm!av>JS42390X);05yT&MLbl8LLe{y@BsuU~lmJ5WAnFeCU7BlwX|2q3?} zqJR)YtPny$A&f!-hr&VxaRQGbLKN{ZjVOt1HtG#*TDnR2nxxgBOXTUghIGx-Lp!pe zVz8SS=BU+V$s`f{Ox~5Z6{t=d*wPJ67iH}!HcS{V#qJnVtp>XIjKog$r?N~&0IBFT zNs*sZ@6~*aaH6P5Di%e;i=wKdhC=n2C_ZUO%D_@wt`e>0S?P{lX;$fNTpPF>+HW{I-u3z){Ji!kC(f~q_AUN1Gfn=i1&G4)cD7C0vxLv-2S}s0184df_beH7 z*4sp{4ah8tp0k3()RUuVcqhSAY#0r-7e1XD>>(x6mxBV`TZ3 z?#|5mGFYb32=aZIcq_u*L8HJuNfIvhP7w+BkJHv_K0u;%LslhYPkaoV7?&EBE^A;{ zfqV$3H55ex+Ie*erkD2_^YD3(U{r;~EV-_Wbzo4ST@v2> zV}+&zk5LFWn3h3vCCTmJ+9OG%>x2#XHntVHCQ-H}zv-LaREY}S6D>p1%(`TF>`jtA zP@nF-cH?KN#F~jn3Km^P5)yYXg03w2sYHURt^wiG+(|;J4t2UgIggw!t_ZWGOl8Dr zbwJbq1i8iA1tdZWhzU^4AArJs zLa?a|&9##d!;kJ~TH(&tS7ZMQ+L5Xgsk)JBbEO+g?XR{LS_|!&&)$9U?qSf5%{j3- zH#XNSb)n5{E7Quf{hv*~nCxJizv1vVT>eJ0*iGl4&1`$Nlm6@Vzg<5H+UW%+z2K%7 zfU1AVmKfiE+S+aHw&lae#|=BP=495~%$j}w6Fc$HNj!8D51Su!r)N8%j&+3X=_O}+ z$(>%Z*^Ax6&BNKFv18UQl$=7zEtG8bYBzhS9co*L$j;t!vbWsqEt?(hPR+DeJGYOn z*i(zn)S^4JXtTL)?s7ZR2_22wxkV?p=;oj@)0_TcXUmQioLIq)6=2%&^!~lpoz87L ze$9zrbK}<_FQu}r+4e-I&?y{#;>^6~&b)Vw?Nr4{Roqmic^4KK5ScdD<_@pe*?A{B z?`G$ZxnuTg?w|bEy#0}2Cm%S;2X68Kre*ZfzZasz zVf!Pu)ca@nz3~w=p^4Z00mMnb=ks-$C-(p2=`xq?;it=7wTGWBle34PZ6@4en`Enn9=UKl08N0W4{{boJOJ4v0 literal 0 HcmV?d00001 diff --git a/app/modules/credits/models.py b/app/modules/credits/models.py new file mode 100644 index 0000000..eb5d1ed --- /dev/null +++ b/app/modules/credits/models.py @@ -0,0 +1,50 @@ +from sqlalchemy import Column, Integer, String, Date, DateTime, Float, Boolean, Text, ForeignKey, Enum as SQLEnum +from sqlalchemy.sql import func +from sqlalchemy.orm import relationship +from database import Base + +import enum + +# Articulo 69 de codigo fiscal de la federacion!!! +# 69 article of fiscal code of federation, mexican united states. + +class Supuestos(str, enum.Enum): + CANCELADOS ="cancelados" + CONDONADOS ="condonados" + FIRMES ="firmes" + SENTENCIAS ="sentencias" + EXIGIBLES ="exigibles" + RETORNO_INVERSIONES ="retorno_inversiones" + FRACCION_X ="fraccion_x" + FRACCION_VII ="fraccion_vii" + NO_LOCALIZADOS ="no_localizados" + + +class Credits(Base): + __tablename__="credits" + + + id = Column(Integer, primary_key=True, autoincrement=True, nullable=False) + title = Column(String(30), nullable=False) + rfc = Column(String(25), nullable=False) + razon_social = Column(String(100), nullable=False) + tipo_persona = Column(String(100), nullable=False) + supuesto = Column(SQLEnum(Supuestos, name="supuestos", create_type=False), nullable=False) + fecha_primera_publicacion = Column(Date, nullable=False) + fecha_publicacion_ley_transparencia = Column(Date, nullable=True) + fecha_cancelacion = Column(Date, nullable=True) + fecha_cancelacion_csd = Column(Date, nullable=True) + entidad_federativa = Column(String, nullable=False) + monto = Column(Integer, nullable=True) + motivo = Column(Text, nullable=True) + location_id = Column(Integer, ForeignKey("location.id"), nullable=False) + + + created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=True) + + uploaded_by = Column(Integer, nullable=True) + + # relationships + location = relationship("Locations", back_populates="credits") + \ No newline at end of file diff --git a/app/modules/credits/repository.py b/app/modules/credits/repository.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/credits/route.py b/app/modules/credits/route.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/credits/schema.py b/app/modules/credits/schema.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/diot/__pycache__/models.cpython-311.pyc b/app/modules/diot/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7e61d9ea59218dec83a0a4251bdb75acfe9267ec GIT binary patch literal 140 zcmZ3^%ge<81ex(iGePuY5CH>>P{wCAAY(d13PUi1CZpd--& z=BJeAq!#O^WagLX139TV#d-ymzc_3l5_UzbK=mLKi}``X2WCb_#t#fIqKFwN1^|&k B9gF|~ literal 0 HcmV?d00001 diff --git a/app/modules/diot/models.py b/app/modules/diot/models.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/diot/repository.py b/app/modules/diot/repository.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/diot/route.py b/app/modules/diot/route.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/diot/schema.py b/app/modules/diot/schema.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/edos/__pycache__/models.cpython-311.pyc b/app/modules/edos/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1efdb9cc709e44f55e15bdeef76647f925c4f1da GIT binary patch literal 2098 zcmb_d%}*Og6yII1?cFttG5#bUO%G8jHwYApphZ!o5O72xMhz}f_R!Vh9mux(VP?*@r8rU!oG4Xt;Lqr#(pvIiHK#~Dq6v^pXJr4_?MBw8{Ztxn?ej$hGV;rj2e~CYc48Rdx?Y}wzoLW0rQkWjKb=S!s)CM(K&_JBakXj#5JSd(5euL zq+JAg+EqhSH?s%|$lv!_X~ zEtV^b<-1FF)Y6^x)dzRxR~Cu1uvl6uFIAS-SAS)WX=?ATO=`7TIJ<$|Nu)dOBrK5W zPSkdB3amns0LC2y;+;If*o4pLgXCrJ3;N(oecOREh=Y`lS=>udRvi^O;mlPy(cCkwZ?{NRJAHiT6Lk*hfjS~-Pt7aiBGsp_8q7_)%We8 zFR4GKz0igHWy)RclO!WKDh2@Cfk|GPG-`(ALOnyH(Qm11wVJIox9b^-SSrdDTY_}%H1QRr&r7o$=xDzY}hEugTBxXW&9_9`{cmM-i4~17_R*Ki+`3<`2FB^N~OJ z2F#d0_`YHiUichI9DC)6IZv9AX65IHZ>Z+&salZPPmh@M7fK6kgkFd%d<3nOsz&@- zt%7bMOnmtYE@?LjJ1bQX;V3PXR7AWPMVBhXKcnjkk~G2=(DxO>XLO88H?^y1jj(C0 zswQ28U9M_H_=Yf2q)JKGj9aBLmCr~9$`ua+u|KYZ!Tv|UJY*0fxMW6f%Pzqqy9HME z2ws^Le0In_63CY-RjCLb2`{7Nl2)peR&~l7t5hTFgezlYRJDpudJF_R(uyTXT$WaK zEG;Q0m-G`(6g5djqDXjARCT$kP(CDzcdC-o*370e*<7}eU7RDq*>rv}J6~9so}DAy zxAW=zLas1Jdh+R9A)ULFofb3Gi*xhSGuP6;GW&EtuaBpsa@p>Veq!?$) zYd8v4HVFb_dv+v}A??C5y?A>%jhCp{7!A~{NIoY=f9v`(Yi2*O~+6E47{r_sqAKG%A zwjd9pkQ@O2d}oyQik^>4p_XjnR4{D+x)IhD-r_ci{1&_qrd%LfFUbN~T zwFmT!LY;dMV*lAw_#IFEcC_C2+(TUMf~xL@8qCb)2}|>>kV(2pu#8KpgxACsC{RwT zDhjQGqzjKi#4l(*y^txn2!sTTc74H2mev9Z7BK<@5G!kCm_@h>K*2p-LtxNlAiRpO z2$d*Rm5S{V#GI~G%QDQd1-aw^j)8C7Pf47v5*Eb@4Z^84Opyt*RN*ek#0L<9u#N#^ z8REgqMbfM3>Jmm`QP%*Nma1#CtcgGW-8Bj~PAQ$FJ}3;6o^o|bDT!?qFhm0|E9MAo zn7}qpIP$)>ybUFexYrRMqJ}NSe!5234olPT65cr&Sp@&QvtgSL07}Y87MIrWFr8tS z>~YGSXcJof#xhjZTSecSsC}~==w6T2{f)wQ+6-K11}?M$7iu$BxMzK_ez}3n@OU#k z-U^S`G8P|MAFIdeu?_eA#DfIr`0*w`-r~n=m#o1P8xxJO#@JTRPp5xAz2h|pr<#LP zt-&dija!je-B(|&uWl^ezxUu?W3n;1_2tf2X5qRyA~Z*Y)`(!D+h$~?8ChvXR%%%* z@$tr3qj#G%6X%+VbFIWVfFf&nWMi>0yM5LiKHnTZ-x@w|vI%SGlZ{N{?DnWRG}#=Q zYza)7X&kU zqZz%?ir%0=c+__1VTs46U?*$sgy>9?*W5+tmobHBU<%|2WBdhV&*O5rEar~+ ze>96ZZgw_{`ONHW7BgUWwp}Jzv)?_Qb*1*0qonSs7am-HNj0zDs6!Fg=X=aivVQg@ MmA-n9%D%CG0NpEscmMzZ literal 0 HcmV?d00001 diff --git a/app/modules/efos/models.py b/app/modules/efos/models.py new file mode 100644 index 0000000..51e0aef --- /dev/null +++ b/app/modules/efos/models.py @@ -0,0 +1,33 @@ +from sqlalchemy import Column, Integer, String, Date, DateTime, Float, Boolean, Text, ForeignKey, Enum as SQLEnum +from sqlalchemy.sql import func +from sqlalchemy.orm import relationship +from database import Base + +import enum + +#Articulo 69-B del codigo fiscal de la federacion +#69-B article of fiscal code of federation of mexican united states + +class Situacion(str, enum.Enum): + DEFINITIVO ="definitivo" + DESVIRTUADO ="desvituado" + PRESUNTO ="presunto" + SENTENCIA_FAVORABLE ="sentencia_favorable" + +class EFOS(Base): + __tablename__ = "efos" + + id = Column(Integer, primary_key=True, nullable=False) + numero = Column(Integer, nullable=False) + rfc = Column(String(30), nullable=False) + nombre_contribuyente = Column(String(100), nullable=False) + situacion = Column(String(60), SQLEnum(Situacion, name="situacion", create_type=False), nullable=False) + publi_presuntos_sat = Column(Date, nullable=True) + publi_desvirtuados_sat = Column(Date, nullable=True) + publi_definitivos_sat = Column(Date, nullable=True) + publi_favorables_sat = Column(Date, nullable=True) + + created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) + updated_at = Column(DateTime(timezone=True), onupdate=func.now(), nullable=False) + + loaded_by = Column(Integer, nullable=False) diff --git a/app/modules/efos/repository.py b/app/modules/efos/repository.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/efos/route.py b/app/modules/efos/route.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/efos/schema.py b/app/modules/efos/schema.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/entreprise/models.py b/app/modules/entreprise/models.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/entreprise/repository.py b/app/modules/entreprise/repository.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/entreprise/route.py b/app/modules/entreprise/route.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/entreprise/schema.py b/app/modules/entreprise/schema.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/feed/__pycache__/models.cpython-311.pyc b/app/modules/feed/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fb1320ad0cdb62dbd142ec6517734b4e68f6fa4c GIT binary patch literal 2107 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 RZrDEaDx$pp2YQt1e*wpWM!^68 literal 0 HcmV?d00001 diff --git a/app/modules/interactions/models.py b/app/modules/interactions/models.py new file mode 100644 index 0000000..8fcdebb --- /dev/null +++ b/app/modules/interactions/models.py @@ -0,0 +1,34 @@ +from sqlalchemy import Column, Integer, String, Date, DateTime, Float, Boolean, Text, ForeignKey, Enum as SQLEnum +from sqlalchemy.sql import func +from sqlalchemy.orm import relationship +from database import Base + +import enum + +class Interaccion(str, enum.Enum): + LIKE = "like" + DONT_LIKE = "dont_like" + APPROVED = "approved" + DISAPRROVE = "disapproved" + NONE = "none" + + +class Interacction(Base): + __tablename__="interacctions" + + id = Column(Integer, primary_key=True, nullable=False) + feed_id = Column(Integer, ForeignKey("feed.id"), nullable=False) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + type_iteractions = Column(SQLEnum(Interaccion, name="type_interactios", create_type=False), default=Interaccion.NONE, nullable=False) + + created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) + updated_at = Column(DateTime(timezone=True), onupdate=func.now(), nullable=True) + + #relationships + user = relationship("Users", back_populates="interactions") + + + + + + diff --git a/app/modules/interactions/repository.py b/app/modules/interactions/repository.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/interactions/route.py b/app/modules/interactions/route.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/interactions/schema.py b/app/modules/interactions/schema.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/invoices/__pycache__/models.cpython-311.pyc b/app/modules/invoices/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..784624148ff106e75df8666f3c0b0420b12cb221 GIT binary patch literal 1685 zcmb_czi%T&6y9I9*WTUuyb0i30R`d?+mT~(2pU8Y62r-02_;;yL|SdU918>%jLlUHGn4Nj^y_s*`yqWiQ ze$uoI2jvg^yYoY3l~$|(QJKkg2P2xOy1o_A)Uxlp$nvPP zj-CZHQ}qdQw!9kJqtdF^!3Ca5&5qZAUM0x20>}5-Pn{MUU$)w)-k3NNcgcxz`HW{* zf;fY>IYY2{Llih9p@c2iqAfg^`Vt%h+PD}=HVZ9fC;G7R1lqXR;`3x*fDvfpV#rf& z>XqE|lqU5rDhPC@jmRz{rj43b#|`2Y^};Nb zP1Cb5GEJH>P3+qpmtoa3cRH3knbAx$V4IIs!V;RWgrI_B6ZUJ-B*ISHCsZb=fm#6| zsnc<6nh5;Ba;X$JEuW+qQ?eneiN>Jl`QK6noN}5DvQ5bFYh`oqXYLY6+{?EYtyU{; zWAECFSJ$)%1ITTcTYDr2%vhuWv_;6taQcnr-VDDuOJ}>U_fkjezpVZ?7p3cibbXkv zhsz^vw)>=4?NtvS?bi-!$L~a1WuR4tS|zNGvafY_d(B?+aBYA4VEcGA$}SDEOT+9^ z_}NIwbwB7m=sh@`-7g)KjuVkm9w_CZQVv%}s@|>kK0Nv=QcDB1G*nCB>NQ{AFCG++ zf=F2yC<{Yn0r;`4caPR1r8rQEL!}t5oauS6oj=SUzaPz42J@BSeC0HMI`<;~>-`t^ zqbFZS`o=)t80s5gZ4`6g0H$6U=#`;f32W>lrK?*h%+XAH$F%~)fi#+$DFYk_^526#!DNOlks;95COkVN%Tx)$@^PGs z%xvu20-%@jN8`YLOdf&Co|E<|hzpVD`4P7h{U3G2y%pW8Bko~zvyQm==w?0Rlra7- Wk}Ch+1$S3;gFo5mU;i-`pYR{sjlj|X literal 0 HcmV?d00001 diff --git a/app/modules/invoices/models.py b/app/modules/invoices/models.py new file mode 100644 index 0000000..5f520a2 --- /dev/null +++ b/app/modules/invoices/models.py @@ -0,0 +1,23 @@ +from sqlalchemy import Column, Integer, String, Date, DateTime, Float, Boolean, Text, ForeignKey, Enum as SQLEnum +from sqlalchemy.sql import func +from sqlalchemy.orm import relationship +from database import Base + +import enum + + +class Invoices(Base): + __tablename__ = "invoices" + + id = Column(Integer, primary_key=True, nullable=False, autoincrement=True) + emisor = Column(String(100), nullable=False) + receptor = Column(String(110), nullable=False) + uuid = Column(String(255), nullable=False) + total = Column(Integer, nullable=False) + tipo = Column(String(50), nullable=True) + date = Column(DateTime, nullable=True) + + verified_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) + verified_by = Column(Integer, nullable=True) + + diff --git a/app/modules/invoices/repository.py b/app/modules/invoices/repository.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/invoices/route.py b/app/modules/invoices/route.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/invoices/schema.py b/app/modules/invoices/schema.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/license/__pycache__/models.cpython-311.pyc b/app/modules/license/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f28e181eb43d3aecf8adb2b894f267c43a11e200 GIT binary patch literal 2813 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 w57tW?Q-99>P{wCAAY(d13PUi1CZpd--& z=BJeAq!#NJr55BDl@#j(`KdX@dIgogIBXztc15f}Eg(aS`GLd-W=2NF4-7D(h#4pb E027iP?f?J) literal 0 HcmV?d00001 diff --git a/app/modules/reports/models.py b/app/modules/reports/models.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/reports/repository.py b/app/modules/reports/repository.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/reports/route.py b/app/modules/reports/route.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/reports/schema.py b/app/modules/reports/schema.py new file mode 100644 index 0000000..e69de29 diff --git a/app/modules/suppliers/__pycache__/models.cpython-311.pyc b/app/modules/suppliers/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8d09a9614b0e701b11b6f757315f1b7b5b569073 GIT binary patch literal 3418 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 P)$n`L{=;i}iGuwfu3*=V literal 0 HcmV?d00001 diff --git a/app/modules/users/models.py b/app/modules/users/models.py new file mode 100644 index 0000000..9049efe --- /dev/null +++ b/app/modules/users/models.py @@ -0,0 +1,79 @@ +from sqlalchemy import Column, Integer, String, DateTime, Float, Boolean, Text, ForeignKey, Enum as SQLEnum +from sqlalchemy.sql import func +from sqlalchemy.orm import relationship +from database import Base + +import enum + +class Rol(str, enum.Enum): + COMPRAS ="compras" + VENTAS ="ventas" + LOGISTICA ="logistica" + ADUANA_SOFT ="aduana_soft" + CLIENTE ="cliente" + OPERATIVO ="operativo" + +class TipoUsuario(str, enum.Enum): + ROOT ="root" + ADMIN ="admin" + PRIVILEGED ="privileged" + ADMIN_LICENCIAS ="admin_licencias" + UPDATER = "updater" + USER ="user" + + +class Users(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, nullable=False, index=True, autoincrement=True) + #identity + name = Column(String(100), nullable=False) + middle_Name = Column(String(100), nullable=True) + last_Name = Column(String(120), nullable=False) + rfc = Column(String(60), nullable=False, unique=True) + + #==== CONEXIONS ====# + email = Column(String(120), nullable=False, unique=True) + password = Column(String(255), nullable=False) + + #=== credentials + rol_operativo = Column(SQLEnum(Rol), default="operativo", nullable=False) + tipo_usuario = Column(SQLEnum(TipoUsuario), default="user", nullable=False) + permite_diot = Column(Boolean, default=False, nullable=False) + + #==== WHERES + enterprise_id = Column(Integer, nullable=True) + firma = Column(String(110), nullable=True, unique=True) + is_active = Column(Boolean, nullable=False, default=True) + + + #timestamsp + created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) + updated_at = Column(DateTime(timezone=True), onupdate=func.now(), nullable=True) + deleted_at = Column(DateTime(timezone=True), nullable=True) + last_login = Column(DateTime(timezone=True), server_default=func.now(), nullable=False) + + #trace + created_by = Column(Integer, nullable=True) + updated_by = Column(Integer, nullable=True) + deleted_by = Column(Integer, nullable=True) + + + #===== + # PASSWORD RESET + #===== + + password_reset_token = Column(String, nullable=True) + password_reset_expires = Column(String, nullable=True) + + + #relationships + clients = relationship("Client", back_populates="user") + branch = relationship("Branches", back_populates="user") + interactions = relationship("Interacction", back_populates="user") + moves = relationship("Moves", back_populates="user", primaryjoin="Users.id == Moves.user_id") + licenses = relationship("License", back_populates="user") + feed = relationship("Feed", back_populates="user", primaryjoin="Users.id == Feed.user_id") + coments = relationship("Coments", back_populates="user", primaryjoin="Users.id == Coments.user_id") + affidavit_logs = relationship("AffidavitRecord", back_populates="user", primaryjoin="Users.id == AffidavitRecord.user_id") + diff --git a/app/modules/users/repository.py b/app/modules/users/repository.py new file mode 100644 index 0000000..a75e05a --- /dev/null +++ b/app/modules/users/repository.py @@ -0,0 +1,212 @@ +# modules/users/repositories.py +from typing import Optional, List, Dict, Any +from datetime import datetime +from sqlalchemy.orm import Session +from sqlalchemy import and_, or_ + +from app.core.baseRepository import BaseRepository +from app.modules.users.models import Users, TipoUsuario, Rol +from app.core.security import get_password_hash, verify_password + + +class UserRepository(BaseRepository[Users]): + """Repositorio específico para Users""" + + def __init__(self, db: Session): + super().__init__(db, Users) + + # ============================================ + # MÉTODOS DE AUTENTICACIÓN + # ============================================ + + def get_by_email(self, email: str, include_deleted: bool = False) -> Optional[Users]: + """Obtiene usuario por email""" + return self._base_query(include_deleted).filter(Users.email == email).first() + + def get_by_firma(self, firma: str, include_deleted: bool = False) -> Optional[Users]: + """Obtiene usuario por firma digital""" + return self._base_query(include_deleted).filter(Users.firma == firma).first() + + def authenticate(self, email: str, password: str) -> Optional[Users]: + """Autentica usuario por email y contraseña""" + user = self.get_by_email(email) + if not user: + return None + if not user.is_active or user.deleted_at is not None: + return None + if not verify_password(password, user.password): + return None + return user + + def update_last_login(self, user_id: int, ip_address: str) -> Optional[Users]: + """Actualiza timestamp e IP del último login""" + user = self.get_by_id(user_id) + if user: + user.last_login = datetime.utcnow() + user.last_ip = ip_address + self.db.commit() + self.db.refresh(user) + return user + + def change_password(self, user_id: int, new_password: str, updated_by: int = None) -> bool: + """Cambia la contraseña del usuario""" + user = self.get_by_id(user_id) + if not user: + return False + + user.password = get_password_hash(new_password) + if updated_by: + user.updated_by = updated_by + + self.db.commit() + return True + + # ============================================ + # MÉTODOS POR TIPO DE USUARIO + # ============================================ + + def get_by_tipo_usuario(self, tipo_usuario: str, include_deleted: bool = False) -> List[Users]: + """Obtiene usuarios por tipo (root, admin, admin_licencias, user)""" + return self._base_query(include_deleted).filter(Users.tipo_usuario == tipo_usuario).all() + + def get_root(self, include_deleted: bool = False) -> List[Users]: + """Usuarios tipo root""" + return self.get_by_tipo_usuario(TipoUsuario.ROOT, include_deleted) + + def get_admin(self, include_deleted: bool = False) -> List[Users]: + """Usuarios tipo admin""" + return self.get_by_tipo_usuario(TipoUsuario.ADMIN, include_deleted) + + def get_admin_licencias(self, include_deleted: bool = False) -> List[Users]: + """Usuarios tipo admin_licencias""" + return self.get_by_tipo_usuario(TipoUsuario.ADMIN_LICENCIAS, include_deleted) + + def get_regular_users(self, include_deleted: bool = False) -> List[Users]: + """Usuarios tipo user (regulares)""" + return self.get_by_tipo_usuario(TipoUsuario.USER, include_deleted) + + # ============================================ + # MÉTODOS POR ROL OPERATIVO + # ============================================ + + def get_by_rol_operativo(self, rol: str, include_deleted: bool = False) -> List[Users]: + """Obtiene usuarios por rol operativo""" + return self._base_query(include_deleted).filter(Users.rol_operativo == rol).all() + + def get_by_permiso_diot(self, include_deleted: bool = False) -> List[Users]: + """Obtiene usuarios que tienen permiso DIOT""" + return self._base_query(include_deleted).filter(Users.permite_diot == True).all() + + # ============================================ + # MÉTODOS POR ASOCIACIONES + # ============================================ + + def get_by_enterprise(self, enterprise_id: int, include_deleted: bool = False) -> List[Users]: + """Obtiene usuarios de una empresa específica""" + return self._base_query(include_deleted).filter(Users.enterprise_id == enterprise_id).all() + + def get_by_sucursal(self, sucursal_id: int, include_deleted: bool = False) -> List[Users]: + """Obtiene usuarios de una sucursal específica""" + return self._base_query(include_deleted).filter(Users.sucursales_id == sucursal_id).all() + + def get_by_licencia(self, licencia_id: int, include_deleted: bool = False) -> List[Users]: + """Obtiene usuarios asociados a una licencia""" + return self._base_query(include_deleted).filter(Users.licencia_id == licencia_id).all() + + def get_by_perfil(self, perfil_id: int, include_deleted: bool = False) -> List[Users]: + """Obtiene usuarios por perfil""" + return self._base_query(include_deleted).filter(Users.perfil_id == perfil_id).all() + + # ============================================ + # MÉTODOS DE ESTADO + # ============================================ + + def get_active(self, skip: int = 0, limit: int = 100) -> List[Users]: + """Obtiene usuarios activos (no eliminados)""" + return self.get_all(skip=skip, limit=limit, include_deleted=False) + + def get_inactive(self, skip: int = 0, limit: int = 100) -> List[Users]: + """Obtiene usuarios inactivos o eliminados""" + return self._base_query(include_deleted=True).filter( + or_( + Users.is_active == False, + Users.deleted_at.isnot(None) + ) + ).offset(skip).limit(limit).all() + + def activate(self, user_id: int, updated_by: int = None) -> Optional[Users]: + """Activa un usuario inactivo""" + user = self.get_by_id(user_id, include_deleted=True) + if not user: + return None + + user.is_active = True + if user.deleted_at: + user.deleted_at = None + if updated_by: + user.updated_by = updated_by + + self.db.commit() + self.db.refresh(user) + return user + + def deactivate(self, user_id: int, updated_by: int = None) -> Optional[Users]: + """Desactiva un usuario (sin soft delete)""" + user = self.get_by_id(user_id) + if not user: + return None + + user.is_active = False + if updated_by: + user.updated_by = updated_by + + self.db.commit() + self.db.refresh(user) + return user + + # ============================================ + # MÉTODOS DE BÚSQUEDA + # ============================================ + + def search(self, term: str, skip: int = 0, limit: int = 100) -> List[Users]: + """Búsqueda por nombre, email o RFC""" + search_term = f"%{term}%" + return self._base_query().filter( + or_( + Users.name.ilike(search_term), + Users.last_name.ilike(search_term), + Users.email.ilike(search_term), + Users.rfc.ilike(search_term) + ) + ).offset(skip).limit(limit).all() + + def count_by_enterprise(self, enterprise_id: int) -> int: + """Cuenta usuarios por empresa""" + return self._base_query().filter(Users.enterprise_id == enterprise_id).count() + + def count_by_tipo(self, tipo_usuario: str) -> int: + """Cuenta usuarios por tipo""" + return self._base_query().filter(Users.tipo_usuario == tipo_usuario).count() + + # ============================================ + # SOBRESCRITURA DE MÉTODOS BASE CON TRAZABILIDAD + # ============================================ + + def create(self, data: Dict[str, Any], created_by: int = None) -> Users: + """Crea usuario con hash de contraseña""" + create_data = data.copy() + + # Hashear contraseña si existe + if "password" in create_data: + raw_password = create_data["password"] + print(f"HASHEANDO PASSWORD: {raw_password} (longitud: {len(raw_password)})") + create_data["password"] = get_password_hash(raw_password) + print(f"HASH GENERADO: {create_data['password'][:20]}...") + + return super().create(create_data, created_by) + + def update(self, id: int, data: Dict[str, Any], updated_by: int = None, include_deleted: bool = False) -> Optional[Users]: + """Actualiza usuario, hashea contraseña si viene""" + if "password" in data: + data["password"] = get_password_hash(data["password"]) + return super().update(id, data, updated_by, include_deleted) \ No newline at end of file diff --git a/app/modules/users/route.py b/app/modules/users/route.py new file mode 100644 index 0000000..13ad8cd --- /dev/null +++ b/app/modules/users/route.py @@ -0,0 +1,188 @@ +from fastapi import APIRouter, Depends, HTTPException, Query, Request, Header, status +# +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer +from sqlalchemy.orm import Session +from typing import Optional, List +# +from database import get_db +from app.core.auth import get_current_user +from app.modules.users.models import Users +# +from app.modules.users.schema import UserCreate, UserResponse, UserLogin, LoginResponse, DeleteResponse, UpdateUser +from app.modules.users.services import UserCrudService +from app.core.security import create_access_token, verify_password + +router = APIRouter(prefix="/users", tags=["users"]) +security = HTTPBearer() + +#================ Login ==================== +@router.post("/login", response_model=LoginResponse) +async def login( + login_data: UserLogin, + db: Session = Depends(get_db) +): + """Authenticate user and return JWT token""" + + user = db.query(Users).filter(Users.email == login_data.email).first() + + #check email + if not user: + raise HTTPException( + status_code = status.HTTP_401_UNAUTHORIZED, + detail="Credentials invalid - user not found", + headers={"WWW-Authenticate": "Bearer"}, + ) + + + #check password ... + if not verify_password(login_data.password, user.password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid credentials = worng password", + headers={"WWW-Athenticate": "Bearer"} + ) + + #check is active + if not user.is_active: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="This user is ianctive, contact with support" + ) + + #update last login + from datetime import datetime + user.last_login = datetime.utcnow() + db.commit() + + #create token + token_data = { + "sub": str(user.id), + "email": user.email, + "user_type": user.tipo_usuario, + "rol_operativo": user.rol_operativo + } + access_token = create_access_token(token_data) + + return LoginResponse(access_token=access_token, token_type="bearer") + + + +#================ Register ==================== + +@router.post("/register", response_model=UserResponse) +async def register(data: UserCreate, db: Session = Depends(get_db)): + + user_count = db.query(Users).count() + + if user_count == 0: + service = UserCrudService() + return UserCrudService.create_user(db = db, user_create=data, current_user=None) + else: + raise HTTPException( + status_code=403, + detail="Root user already exists, use create_users enpoint" + ) + +#================ Create User ==================== +@router.post("/create_users", response_model=UserResponse) +async def new_user( + data: UserCreate, + db: Session = Depends(get_db), + credentials: HTTPAuthorizationCredentials = Depends(security) +): + + """Create a new user - Requeres authentication """ + + from app.core.auth import AuthService + + token = credentials.credentials + auth_service = AuthService() + payload = auth_service.decode_token(token) + user_id = int(payload.get("sub")) + current_user = db.query(Users).filter(Users.id == user_id).first() + + + if not current_user: + raise HTTPException( + status_code=401, + detail="User not found - invalid token" + ) + + if current_user.tipo_usuario not in ["root", "admin"]: + raise HTTPException(status_code=403, detail="you dont have permission to create users") + + if current_user.tipo_usuario == "admin" and data.tipo_usuario != "user": + raise HTTPException(status_code=403, detail="Admin can only create users type user") + + return UserCrudService.create_user( + db = db, user_create = data, current_user = current_user + ) + +#================ Get Users ==================== +@router.get("/get", response_model=List[UserResponse]) +def obtain_users( + db: Session = Depends(get_db), + current_user: Users = Depends(get_current_user), + skip: int = Query(0, ge=0, description="Number of records to skip for pagination"), + limit: int = Query(100, ge=1, le= 1000, description="Maximun number of records to return") +): + """Get a list of user with pagination - requeres authentication""" + return UserCrudService.get_users(db = db, skip=skip, limit=limit) + +#================ Delete Users ==================== +@router.delete("/delete/{user_id}", response_model=DeleteResponse) +async def delete( + db:Session = Depends(get_db), + user_id= int, + current_user: Users = Depends(get_current_user) +): + """Delete a user by id""" + + current_user = db.query(Users).filter(Users.id == user_id).first() + + if current_user.tipo_usuario not in ["root", "admin"]: + raise HTTPException(status_code=403, + detail="You dont have permitions to delete ") + try: + UserCrudService.delete_user(db = db, user_id=user_id, current_user=current_user) + return {"message": "ok"} + except ValueError as e: + raise HTTPException(status_code=400, detail=str(e)) + +#================ Update Users ==================== + +@router.patch("/update/{user_id}", response_model=UserResponse) +async def user_update( + user_id: int, + data: UpdateUser, + db: Session = Depends(get_db), + current_user: Users = Depends(get_current_user) +): + """ Update existing user""" + + user = db.query(Users).filter(Users.id == user_id).first() + + current_user = db.query(Users).filter(Users.id == user_id).first() + + if not user: + raise ValueError("Usuario no encontrado") + + if current_user.id != user_id: + if current_user.tipo_usuario not in ["root", "admin"]: + raise PermissionError("Admin only can update user ur enterprise") + try: + updated_user = await UserCrudService.Update_user(db=db, user_id=user_id, user_data=data, current_user=current_user) + print(f"TIPO DE updated_user: {type(updated_user)}") + print(f"VALOR: {updated_user}") + print(f"ID: {updated_user.id if updated_user else 'None'}") + return UserResponse.model_validate(updated_user) + + except ValueError as e: + raise HTTPException(status_code=404, detail=str(e)) + + except PermissionError as e: + raise HTTPException(status_code=403, detail=str(e)) + + + + \ No newline at end of file diff --git a/app/modules/users/schema.py b/app/modules/users/schema.py new file mode 100644 index 0000000..c2a9088 --- /dev/null +++ b/app/modules/users/schema.py @@ -0,0 +1,96 @@ +# modules/users/schemas.py +from pydantic import BaseModel, EmailStr, Field, validator +from datetime import datetime +from typing import Optional +# +import re +from enum import Enum + +class TipoUsuario(str, Enum): + ROOT = "root" + ADMIN = "admin" + ADMIN_LICENCIAS = "admin_licencias" + USER = "user" + +class RolOperativo(str, Enum): + COMPRAS ="compras" + VENTAS ="ventas" + LOGISTICA ="logistica" + ADUANA_SOFT ="aduana_soft" + CLIENTE ="cliente" + OPERATIVO ="operativo" + +# ============================================ +# BASE SCHEMAS +# ============================================ +class UserBase(BaseModel): + name: str = Field(...) + middle_Name: Optional[str] = Field(None) + last_Name: str = Field(...) + rfc: str = Field(..., example="PORF8513JH3") + email: EmailStr + tipo_usuario: TipoUsuario + rol_operativo: RolOperativo + permite_diot: bool = False + +class UserCreate(UserBase): + password: str = Field(..., min_length=8) + enterprise_id: Optional[int] = Field(None) + firma: Optional[str] = Field(None) + + + @validator("password") + def validate_password(cls, v): + if len(v) < 8: + raise ValueError("La contrasena debe tener al menos 8 caracteres") + if not re.search(r"[A-Z]", v): + raise ValueError("La contrasena debe contenera la emnos una mayuscula") + return v + + +class UserResponse(UserBase): + id: int + is_active: bool + last_login: datetime + created_by: Optional[int] + updated_by: Optional[int] + deleted_by: Optional[int] + created_at: datetime + updated_at: Optional[datetime] + deleted_at: Optional[datetime] + + class Config: + from_attributes = True + + +class UpdateUser(UserBase): + name: Optional[str] = None + middle_Name: Optional[str] = None + last_Name: Optional[str] = None + rfc: Optional[str] = None + email: Optional[EmailStr] = None + permite_diot: bool = None + rol_operativo: Optional[RolOperativo] = None + tipo_usuario: Optional[TipoUsuario] = None + firma: Optional[str] = Field(None, max_length=110) + brnaches_id: Optional[int] = None + license_id: Optional[int] = None + is_active: Optional[bool] = None + + class Config: + from_attributes = True + +#============================================ +class UserLogin(BaseModel): + email: EmailStr = Field(..., examples=["User@example.com"]) + password: str = Field(..., min_length=8) + + + + +class LoginResponse(BaseModel): + access_token: str + token_type: str = "bearer" + +class DeleteResponse(BaseModel): + message: str diff --git a/app/modules/users/services.py b/app/modules/users/services.py new file mode 100644 index 0000000..cc43f6f --- /dev/null +++ b/app/modules/users/services.py @@ -0,0 +1,158 @@ +from datetime import datetime +from typing import List, Optional +# +from sqlalchemy.orm import Session +import logging +from app.modules.users.models import Users, TipoUsuario, Rol +# +from app.modules.users.schema import UserCreate, UserResponse, UpdateUser +from app.modules.users.repository import UserRepository +from app.core.security import get_password_hash +# +from app.core.auth import get_current_user +from app.core.baseRepository import BaseRepository +from app.pipelines.crud import CreatePipe + +class UserCrudService: + + @staticmethod + def create_user(db:Session, user_create: UserCreate, current_user: Optional[Users] = None) -> UserResponse: + + """ Create a new user in the database and approved if is root user""" + first = db.query(Users).count() + is_first_user = (first == 0) + + user_data = user_create.dict() + # + # + model_data = { + "name": user_data.get("name"), + "middle_Name": user_data.get("middle_Name"), + "last_Name": user_data.get("last_Name"), + "rfc": user_data.get("rfc"), + "email": user_data.get("email"), + "rol_operativo": user_data.get("rol_operativo"), + "tipo_usuario": user_data.get("tipo_usuario"), + "permite_diot": user_data.get("permite_diot", False), + "enterprise_id": user_data.get("enterprise_id"), + "firma": user_data.get("firma"), + "is_active": True, + } + + #Password comes raw since schema + plain_pass = user_data.get("password") + + + # this step is for hash the pasword + hashed_pass = get_password_hash(plain_pass) + model_data["password"] = hashed_pass + + # first user or not that, this is a joke :) + + + if is_first_user: + # if first user is being created, it must be root and approved by default + model_data["tipo_usuario"] = TipoUsuario.ROOT.value + model_data["created_by"] = None + + #this point is for make one instance + new_user = Users(**model_data) + db.add(new_user) + db.commit() + db.refresh(new_user) + + else: + # Case B: aditional User + if not current_user: + raise ValueError("requeries authentication for create user") + + #validate who is creating or if intent create a root user + if model_data["tipo_usuario"] == TipoUsuario.ROOT.value: + if current_user.tipo_usuario != TipoUsuario.ROOT.value: + raise ValueError(" only ROOT user can create another Root user") + + model_data["created_by"] = current_user.id + new_user = Users(**model_data) + db.add(new_user) + db.commit() + db.refresh(new_user) + + #to this point validate the next users if can be used the app, admin, license_admin and users + + + + + + return UserResponse.model_validate(new_user) + # if first is registered allthem users only can set admin or user role. + + + @staticmethod + def get_users(db: Session, skip: int =0, limit: int = 100) -> List[UserResponse]: + """"Get a list of users whith pagination""" + userRes = db.query(Users).offset(skip).limit(limit).all() + + return [UserResponse.model_validate(user) for user in userRes] + + @staticmethod + def delete_user(db: Session, user_id: int, current_user: Users) -> None: + """ this is soft delete, for traceability""" + user = db.query(Users).filter(Users.id == user_id).first() + + if not user: + raise ValueError( + "User not found") + if user.tipo_usuario == TipoUsuario.ROOT.value: + raise ValueError("Root user cannot be deleted") + user.is_active = False + user.deleted_by = current_user.id + user.deleted_at = datetime.utcnow() + + db.commit() + return { + "message": f"User has been deleted" + } + + #=========== UPDATE ===================== + @staticmethod + async def Update_user(db: Session, user_id: int, user_data: UpdateUser, current_user: Users) -> Users: + """Function to updates users, only personal data""" + user = db.query(Users).filter(Users.id == user_id).first() + + if not user: + raise ValueError( + "User not found") + + if current_user.id != user_id: + + if current_user.tipo_usuario not in ["root", "admin"]: + raise PermissionError("You don't have permition to updated") + + if current_user.tipo_usuario == "admin": + if user.enterprise_id != current_user.enterprise_id: + raise PermissionError(" Admin only can't update user enterprise ") + + update_data = user_data.dict(exclude_unset=True) + + if user.tipo_usuario == "root" and update_data["tipo_usuario"] != "root": + root_count = db.query(Users).filter( + Users.tipo_usuario == "root", + Users.deleted_at.is_(None) + ).count + if root_count <= 1: + raise ValueError("Can't downgrade ROOT user") + + for field, value in update_data.items(): + if hasattr(user, field): + setattr(user, field, value) + + user.updated_by = current_user.id + user.updated_at = datetime.utcnow() + + db.commit() + db.refresh(user) + + return user + + + \ No newline at end of file diff --git a/app/pipelines/__pycache__/base.cpython-311.pyc b/app/pipelines/__pycache__/base.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..577faa805936ff8f996e413526fdc4f30b8735de GIT binary patch literal 4027 zcmb6cOKcn0@$GWCyZnjN$C6CR@kWXxGat&0-P%a~X<#)@nm{Qk)dvCvvF3Y{*I0i1 zc4}Gyx67r-+6OP+ z&U-U&-t4@Yc{7i{&Sa7Vp5kBsW|=VhCnDWP3kS!4fWcEj36*S8l}b`oF3HtcDOOcV zN;O`JSJjeQ)k>O#JhH7<6QzV0$LwS^RZ5AmVyCN_Qbvs9_E0rj%1T5gOr`1<5+T2Z zH+V`pLDdGR!!o%=sQ#GH1k+fK4X?4g*&~jW~TV89h+a~w5Tdc;oRVhfmw!|C<*kPEu+2own z+#frP1Dal9{DD}vtOTnRN3of;y?TQGP^7%54Vlw>NE zVpL`djj=dYn7X3U_+zD{QI+Xbqhv*a-7BR8?7xdA0Z-BtOEI-)i;y=Bb7`81c!vPb z&@9+H$A$!tN{4}#rKjj9%Tcf?d|~WyyfjQlzE^6E{9cJ8b zGNW0uR;-F?fL*vr%{Pn&uUD93RxO~^jWxqDZHt;kvD}yG9bajJ{rp6|!MKTzs3@T^ zu#@3MCm#O_z*Dj=t&=j0f@fU@EDDnK7+}aRo>c-^k=wVCbOKGu?>8HxD z^QtAH%1@Tfl@+kh1IsNazTz-@Wk9@x+d$iYlb<&m4e>TYuQ>DQhhk&R*URuN%Pp5% z!(AU0yQCZla25e<5N~9xtvu1Zu}SC>-su_WhP+qLLznW3Z;9^0CFs(g@k(1c)F-`( zDS^IL4A8=xfM(n9I|hlwSH$L2&fF%iRU&u5ROCuD(*L=VKzvE9sRgzI(Ph|m#{fYH zSvQml63@fRg3Monj?VyaP^Mw7L~)jJqNO(K3egOcCC!q6H$PlbX3$ftLMDq^n1UV!)Il4)y z$XO?nMVExqoO1vm|?jj zkqQX;6yD=y*!TxT5;OHfpOH{rJY(R?)1hiO4i!ut(l#mt~9BA>7hF=p8 z(A1C?rF@`iAuUS$K+^?HdIV+VUQ_}W$WOleC?=Ej7)|sl>iU3#K82gmPh+>q)7Z}y zSTFBHMWd?+_B7e=R-U0LaCfpNm!`MniY$S%pIoJx=gJxCmqWALGK5F2iPthwS;4o) z_wTVv(}hypixHzzuQqJv*58h}zEw1C_j1&Ud>e7R1ssqGu&j$bhCE@Ga3ZFQ#o&8A z4hYZQ^rfQlBls+@nUGkVrtO-v9<5y*C~ z{-x~#4AsxF-r3phpTRJ7pie`cJv1%=l0MMSc+k;UG-pU{%~r3|I}b>zw2dB{j&9o*4ErXcIGfU(;-r3 zLGotL?|tX1xj)_5pSX!F7q(6eXFDpHHg?B<^%IEmB_a_pYpyZE~|#q?J{cs^H5OzkC~ z%dxd2rgmW;Aa$?wY{q>2Muz~{Sr*-1(24Q0Kwv*K-C|+XSKYOSRa*@lA*?KXAS{mw zl4F+Rcq4|}*L~Zx@kYB=tn;eyr=NlHQ>@fEE1FIBZs6WiNP`=H^jE2fcUGQ8A>$|J zg1{y7`~A%$ z=l1*C5h?EXH;-J{?{7zmTzg`3@*t5vOyoCIFFD+v+bkR;rw^0U8(K$MkR))?vtj$; X=7X(=o1YwJ=l(5N{`(w8VTu0%XYjzx literal 0 HcmV?d00001 diff --git a/app/pipelines/__pycache__/crud.cpython-311.pyc b/app/pipelines/__pycache__/crud.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2420b27ede4d262b750e67981f259fd36f27d27a GIT binary patch literal 17867 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 literal 0 HcmV?d00001 diff --git a/app/pipelines/base.py b/app/pipelines/base.py new file mode 100644 index 0000000..7bb8b41 --- /dev/null +++ b/app/pipelines/base.py @@ -0,0 +1,79 @@ +from typing import Any, Dict, Optional, Callable, List, TypeVar, Generic +from sqlalchemy.orm import Session +from app.core.auth import CurrentUser + +from app.serviceInput.base import ServiceInput +from app.serviceOutput.base import ServiceOutput + +InputT = TypeVar("InputT") +OutputT = TypeVar("OutputT") + +class Pipeline(Generic[InputT, OutputT]): + """ + Pipeline base que unifica entrada, procesamiento y salida. + """ + + def __init__(self, db: Session, user: CurrentUser, operation: str): + self.db = db + self.user = user + self.operation = operation + self._steps: List[Callable] = [] + self._input: Optional[ServiceInput] = None + self._output: Optional[ServiceOutput] = None + self._input_data: Any = None + self._schema = None + self._permission: Optional[str] = None + self._affidavit: Optional[Any] = None + + def set_input_data(self, data: Any) -> 'Pipeline': + """Define los datos de entrada""" + self._input_data = data + return self + + def with_schema(self, schema: Any) -> 'Pipeline': + """Define el schema de validación""" + self._schema = schema + return self + + def with_permission(self, permission: Optional[str]) -> 'Pipeline': + """Define el permiso requerido""" + self._permission = permission + return self + + def with_affidavit(self, affidavit: Any) -> 'Pipeline': + """Define el certificador legal""" + self._affidavit = affidavit + return self + + def add_step(self, name: str, func: Callable) -> 'Pipeline': + """Agrega un paso al pipeline""" + self._steps.append({"name": name, "func": func}) + return self + + async def execute(self, context: Dict = None) -> Dict: + """ + Ejecuta el pipeline completo: + 1. Validación de entrada + 2. Verificación de permisos + 3. Ejecución de pasos + 4. Retorna resultado + """ + if context is None: + context = {} + + context["db"] = self.db + context["user"] = self.user + context["operation"] = self.operation + context["input_data"] = self._input_data + context["schema"] = self._schema + context["permission"] = self._permission + context["affidavit"] = self._affidavit + + result = context + + for step in self._steps: + result = await step["func"](result) + if result.get("error"): + return {"success": False, "error": result["error"]} + + return {"success": True, "data": result.get("output")} \ No newline at end of file diff --git a/app/pipelines/crud.py b/app/pipelines/crud.py new file mode 100644 index 0000000..2d4d889 --- /dev/null +++ b/app/pipelines/crud.py @@ -0,0 +1,289 @@ +from typing import Type, Dict, Any, List +from sqlalchemy.orm import Session +from app.pipelines.base import Pipeline +# # +from app.serviceInput.base import ServiceInput +from app.core.auth import CurrentUser + + +class CreatePipe(Pipeline): + """Pipe prefab to create enititys""" + + def __init__(self, db: Session, user: CurrentUser, model: Type, repository): + super().__init__(db, user, f"CREATE_{model.__tablename__.upper()}") + self.model = model + self.repository = repository + self._build() + + def _build(self): + """Build steps to Pipe""" + self.add_step("Validate", self._validate) + self.add_step("check_permissions", self._check_permissions) + self.add_step("execute", self._execute) + self.add_step("format_output", self._format) + + async def _validate(self, ctx): + data = ctx.get("input_data", {}) + if ctx.get("schema"): + validated = ctx["schema"](**data) + ctx["validated_data"] = validated.dict() + else: + ctx["validated_data"] = data + return ctx + + + async def _check_permissions(self, ctx): + if ctx.get("permission"): + if not ctx.get("user"): + ctx["error"] = "Usuario no autenticado" + return ctx + + + async def _execute(self, ctx): + if ctx.get("error"): + return ctx + + try: + created_by = ctx["user"].id if ctx.get("user") else None + entity = self.repository.create(ctx["validated_data"], created_by) + ctx["entity"] = entity + except Exception as e: + ctx["error"] = str(e) + return ctx + + + async def _format(self, ctx): + if ctx.get("error"): + ctx["output"] = None + return ctx + + if ctx.get("entity"): + ctx["output"] = {c.name: getattr(ctx["entity"], c.name) + for c in self.model.__table__.columns} + print(f"FORMAT: output creado con {len(ctx['output'])} campos") + else: + print("FORMAT: No hay entity para formatear") + ctx["output"] = None + return ctx + + +class ReadPipe(Pipeline): + """ Read prefab """ + + def __init__(self, db: Session, user: CurrentUser, model: Type, repository): + super().__init__(db, user, f"READ_{model.__tablename__.upper()}") + self.model = model + self.repository = repository + self._build() + + def _build(self): + self.add_step("validate_id", self._validate_id) + self.add_step("check_permissions", self._check_permissions) + self.add_step("execurte", self._execute) + self.add_step("format_output", self._format) + + async def _validate_id(self, ctx): + input_data = ctx["input"].raw_data + entity_id = input_data.get("id") + if not entity_id: + raise ValueError("se requiere ID") + ctx["entity_id"] = entity_id + return ctx + + async def _check_permissions(self, ctx): + return ctx + + async def _execute(self, ctx): + entity = self.repository.get_by_id(ctx["entity_id"]) + if not entity: + raise ValueError(f"{self.model.__name__} no encontrado") + ctx["entity"] = entity + ctx["output"] = entity + return ctx + async def _format(self, ctx): + ctx["output"] = {c.name: getattr(ctx["entity"], c.name) + for c in self.model.__table__.columns} + return ctx + + +class UpdatePipe(Pipeline): + + """Pipe prefab to Update""" + + def __init__(self, db:Session, user:CurrentUser, model: Type, repository): + super().__init__(db, user, f"UPDATE_{model.__tablename__.upper()}") + self.model = model + self.repository = repository + self._build() + + def _build(self): + self.add_step("validate", self._validate) + self.add_step("validate_id", self._validate_id) + self.add_step("check_permissions", self._check_permissions) + self.add_step("execute", self._execute) + self.add_step("certify", self._certify) + self.add_step("format_output", self._format) + + async def _validate(self, ctx): + data = ctx["input"].get_validated() + ctx["validated_data"] = data.dict() if data else {} + return ctx + + async def _validate_id(self, ctx): + entity_id = ctx["validated_data"].get("id") + if not entity_id: + raise ValueError("Se rqeuiere ID") + ctx["entity_id"] = entity_id + return ctx + + async def _check_permissions(self, ctx): + return ctx + async def _execute(self, ctx): + entity = self.repository.update(ctx["entity_id"], ctx["validated_data"]) + if not entity: + raise ValueError(f"{self.model.__name__} no encontrado") + ctx["entity"] = entity + ctx["output"] = entity + return ctx + + async def _certify(self, ctx): + return ctx + + + async def _format(self, ctx): + ctx["output"] = { + "id": ctx["entity"].id, + "message": f"{self.model.__name__} actualizado exitosamente" + } + return ctx + +class SoftDelete(Pipeline): + + """Pipe prefab to SoftDelete""" + + def __init__(self, db:Session, user:CurrentUser, model: Type, repository): + super().__init__(db, user, f"DELETE_{model.__tablename__.upper()}") + self.model = model + self.repository = repository + self._build() + + def _build(self): + self.add_step("validate", self._validate) + self.add_step("validate_id", self._validate_id) + self.add_step("check_permissions", self._check_permissions) + self.add_step("check_already_deleted", self._check_already_deleted) + self.add_step("execute", self._execute) + self.add_step("certify", self._certify) + self.add_step("format_output", self._format) + + async def _validate_id(self, ctx): + input_data = ctx["input"].raw_data + entity_id = input_data.get("id") + if not entity_id: + raise ValueError("se requiere ID") + ctx["entity_id"] = entity_id + return ctx + + async def _check_permissions(self, ctx): + return ctx + + async def _check_already_deleted(self, ctx): + """Verify entity has no deleted""" + entity = self.repository.get_by_id(ctx["entity_id"]) + if not entity: + raise ValueError(f"{self.model.__name__} no encontrado") + + if not entity.is_active or entity.deleted_at is not None: + raise ValueError(f"{self.model.__name__} ya esta eliminado") + + ctx["entity"] = entity + return ctx + + async def _execute(self, ctx): + """ SoftDelete implemented""" + + entity = ctx["entity"] + from datetime import datetime + + entity.is_active = False + entity.deleted_at = datetime.utcnow() + entity.deleted_by = ctx["user"].id + + self.db.commit() + self.db.refresh(entity) + + ctx["deleted"] = True + ctx["output"] = {"id": entity.id, "deleted": True} + return ctx + + + async def _certify(self, ctx): + return ctx + + + async def _format(self, ctx): + + ctx["output"]["message"] = f"{self.model.__name__} eliminado corectamente" + ctx["output"]["deleted_at"] = ctx["entity"].deleted_at.isoformat() + ctx["output"]["deleted_by"] = ctx["entity"].delted_by + return ctx + +class ListPipe(Pipeline): + """Pipeline prefab to list entitys""" + + def __init__(self, db: Session, user: CurrentUser, model: Type, repository): + super().__init__(db, user, f"LIST_{model.__tablename__.upper()}") + self.model = model + self.repository = repository + self._skip = 0 + self._limit = 100 + self._filters = {} + self._include_inactive = False + self._build() + + def with_pagination(self, skip: int, limit: int) -> 'ListPipe': + self._skip = skip + self._limit = limit + return self + + def with_filters(self, filters: Dict) -> 'ListPipe': + self._filters = filters + return self + + def include_inactive(self, value: bool = True) -> 'ListPipe': + self._include_inactive = value + return self + + def _build(self): + self.add_step("parse_params", self._parse_params) + self.add_step("check_permissions", self._check_permissions) + self.add_step("execute", self._execute) + self.add_step("format_output", self._format) + + async def _parse_params(self, ctx): + ctx["skip"] = self._skip + ctx["limit"] = self._limit + ctx["filters"] = self._filters + ctx["include_inactive"] = self._include_inactive + return ctx + + async def _check_permissions(self, ctx): + return ctx + + async def _execute(self, ctx): + entities = self.repository.get_all( + skip=ctx["skip"], + limit=ctx["limit"], + filters=ctx["filters"] + ) + ctx["entities"] = entities + ctx["output"] = entities + return ctx + + async def _format(self, ctx): + ctx["output"] = [ + {c.name: getattr(e, c.name) for c in self.model.__table__.columns} + for e in ctx["entities"] + ] + return ctx + \ No newline at end of file diff --git a/app/serviceInput/__init__.py b/app/serviceInput/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/serviceInput/__pycache__/__init__.cpython-311.pyc b/app/serviceInput/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..12a0304465442d27c87dafd15d55179604c33bed GIT binary patch literal 142 zcmZ3^%ge<81d}3;W`gL)AOZ#$p^VRLK*n^26oz01O-8?!3`I;p{%4TnFGc;tf&wru zPAw|SOiuO8D=00|kB`sH%PfhH*DI*}#bJ}1pHiBWYFESx)B!THm>)=dU}j`w{J;Ps IikN|70QJirg#Z8m literal 0 HcmV?d00001 diff --git a/app/serviceInput/__pycache__/base.cpython-311.pyc b/app/serviceInput/__pycache__/base.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d5133a227c3c8742801d12431fae31a2efc0f4b0 GIT binary patch literal 2014 zcma)7&uiOO9DmP}EXlExvZPsTnnYzZ;4o6W7Pi7R*cgN|nhi2~&_z@AUOIJ_HGbB zzmk6{l?nvb!tQVWuL>c5q0(RZo^ZAa!X9CS(K-pJO@o}B3lv)kR9g)+Tcha9)$@UF z>#3~Njli%CN^*p2Oy&B|+F_26V>q*An>n&fSbm!@ovYj|_Z{RAvN(pb7gu1$Dyf=( zqgvIZ@=3TZ)z5q{k;WH|#E(L^F7;&|a^ZVY|7yL#@4BK&rTM8F^E(mabx;iECOq*2 zF3sCvqnThBn6-ORGh|-xQR$}jlrsjx9^u5MjMzCwZG{z?!c{0nTT_{~t=V~&=Q=me zQ-@Lv@EO~xZGv1F_O0cZ3HC9t7eOw7Tw-HTT1ggHxp?S%JWXxmiiMe5&Cp--J(sx_ z;}#DS0b*=P#clY~5bh%fN^)gBj+(;b-heO28aYP&>}!BMvPn0I15&m&bD+__mh{U7 z!-K&+4naHahY*NY`wByCa1A4?q^amt)Kr8qLGITh%v3NHv-h*+)b^;Ai3{ADX2pfy0 zW@u$O96@}>%w@Pa$kVWi3P6lmS(bPkHfi+fyg)_YjLnGO0oWohD%So&yHbOrb@SOr znS}LsCMKUeZX0s~C)gWg3<}C=&3GFap`lt=iFgZ)RVpyQBbw$H;|0CUO80qk-t3La zNT)`8c!L;w2JL!nDmZ&-Dqn&yn#wTEt>KnIWVS9&^7)K2dZ1uyR;o%ixKnV2RPm*% zszVmjkE69n1a6Ww#e%TmYV{;?7<`R3rbg=QS!Xc%T>$99bmi%{o$~C#+#luI@8#M_ z`NnDaMrUf~>7A~wUMh4+24*2`#|jWU4i{F(aq2@i;En??krPC$Sx33(INvqhde5Uf z4vRd;5lEF(;zS61wp0`N(h1~P%pu?~i>nC8tGI?Ri-04LPL?R4grLJ2{|K-(_;u8U zwz|+&Rc#*f)#h81>Gs@9w~J6zwE6v{b^G9Pmw@=ot#rEx1qxB$KQ~Y3-|G^PFG60^ z3j0%C0^4gLOCOV7sgB7!6zzxiQ!cVP)3Wj3Okh#cXsk0gOngrmXv1$-#!*`PC?7A4 z4>Qg`7E2I^gC4g4x(cPVLq2G~+0h|$?a`w{X4|93i{j+Y^6uxY^>(p#Qmma8Yg=Yl aou*Lt|6%w2{adG#*Zx7*zpwD8l>7(SJH;pf literal 0 HcmV?d00001 diff --git a/app/serviceInput/base.py b/app/serviceInput/base.py new file mode 100644 index 0000000..e979aa8 --- /dev/null +++ b/app/serviceInput/base.py @@ -0,0 +1,29 @@ +from typing import Any, Dict, Optional, Generic, TypeVar +from pydantic import BaseModel +from datetime import datetime + +InputType = TypeVar("InputType", bound=BaseModel) + +class ServiceInput(Generic[InputType]): + """ Base unificada de entradas """ + + def __init__(self, raw_data: Any, source: str = "api"): + self.raw_data = raw_data + self.source = source + self.timestamp = datetime.utcnow() + self._validated = None + + def validate(self, schema: type[InputType]) -> InputType: + """Validacion de datos en contra de un schema""" + self._validated = schema(**self.raw_data) + return self._validated + + def get_validated(self) -> Optional[InputType]: + return self._validated + + def to_dict(self) -> Dict: + return { + "source": self.source, + "timestamp": self.timestamp.isoformat(), + "data": self.raw_data, + } \ No newline at end of file diff --git a/app/serviceOutput/__init__.py b/app/serviceOutput/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/app/serviceOutput/__pycache__/__init__.cpython-311.pyc b/app/serviceOutput/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f38cd7491aeaf95817eb01d428f5eb957d0e84b GIT binary patch literal 143 zcmZ3^%ge<81d}3;W`gL)AOZ#$p^VRLK*n^26oz01O-8?!3`I;p{%4TnFD3oNf&wru zPAw|SOiuMLEh#81(T|VM%*!l^kJl@x{Ka9Do1apelWJGQ3e*BJw3r`Ad|+l|WcN!$L{efyliKLgR z3z2-vwt`4I>jieLYC2JBhE*B2ttj>8g2&!6c}0jcY6dK@T^41|hJhCbIA!(T8naYS z*?b>_tAr6lD_N_2wPNnK4MOSGGbd9BGni8;OF3Gw^(=0tF(h-m| zAS)neK_2PJIgqn-6e8FW<<2pF(Y9E3JM|;wmrb9QJ(HVdD>R)8q0M;NHyxXrWy;D< z%`bbl#~i!L{3zvzmc@KOk|8cLO4mHbO$A`Q5v%Xn|8fOb6H?wKovQXedf#!E%b2Je*8fckA46cPI(KFW*BzW z4h*AS+ye7d-=2my`Nx1PlJ?lHrhGZwF6{?4+Acm0Y~Z?G)MR@uvD9lKjP)+!y}C>@82xpJnHRvsPg5FkyplO@?5mtW`*A$_#H z?}_UZ%~zLBw6c5J`JK(&cFeJiW6bA3fW`@p#vy=^u$tfv6Uk_h)dc&*2*OsxgXG*1 za$8)36}OUAF|Hg{XoV@t=1jl^581pAEkmK9>-l^iOh=L*@JbgyJ_&|x;NgmgfQRP2 z=wAT>g{MKW;RRLqS^d-c^5i#1zB=;d)XLQN2b%R(etIK6y{=7fhX#X)q0vD=Xa)=N z;qYvQ;TrsD28$JRXle6F$zCI0;~zw>7vI2E+iS3_TDEFczvh?^gm>!Vd*!-#q6~$I z?J812587+D^01S^dBub3W3qaOQ-k{PxLpl~`%T||02e$B1PUuZv-HfB=kFCJ?-nLo zg@YS~gX{SjxNl6}p6W_Joci%hS6bI*VvuIxqghy9$zgA+nl3YpC}S9IjfM`&S;M#h zzm48Z$}ninG7OG=jvqww7!rhmPawe>j8o1Jq4Xq>n?T9IHz{9k}9CRbaY zUYDmkvZCyU1EB0))?;?(m6-ihh}ke16_v47sY8I>07qbV&cCB`;eNk?k1;Mvs=yU@g_V4LC?Hpkyz{I}}j7-RAj z+OUnrI6cr#c2n>o$LwNB_+J3&NP-}=$#d)f?{1Tc^{w~)+*tGNrFUDo{TsRcipd}>$ literal 0 HcmV?d00001 diff --git a/app/serviceOutput/base.py b/app/serviceOutput/base.py new file mode 100644 index 0000000..aa45358 --- /dev/null +++ b/app/serviceOutput/base.py @@ -0,0 +1,37 @@ +from typing import Any, Dict, Optional, Generic, TypeVar +from datetime import datetime + +OutputType = TypeVar("OutputType") + +class ServiceOutput(Generic[OutputType]): + """ Base para cualquier salida de los pipelines""" + + def __init__(self, data: OutputType, operation: str, status: str = "success"): + self.data = data + self.operation = operation + self.status = status + self.timestamp = datetime.utcnow() + self.metadata = {} + + def add_metadata(self, key: str, value: Any) -> 'ServiceOutput': + self.metadata[key] = value + return self + + def to_dict(self) -> Dict: + result = { + "operation": self.operation, + "status": self.status, + "timestamp": self.timestamp.isoformat(), + "data": self.data + } + if self.metadata: + result["metadata"] = self.metadata + return result + + def to_json(self) -> str: + import json + return json.dumps(self.to_dict(), default=str) + + def to_print(self) -> str: + """Para salida en consola""" + return f"[{self.status.upper()}] {self.operation}: {self.data}" \ No newline at end of file diff --git a/database.py b/database.py new file mode 100644 index 0000000..8eb8586 --- /dev/null +++ b/database.py @@ -0,0 +1,104 @@ + +# Database.py + +""" +Configuracion de la base de datos para XMA en PostgresSQL + +""" +""" +Configuracion de la base de datos para XMA en PostgresSQL +""" + +import os +from sqlalchemy import create_engine, event, text +from sqlalchemy.orm import sessionmaker, Session +from sqlalchemy.pool import QueuePool + +from sqlalchemy.ext.declarative import declarative_base +from typing import Generator +import logging + + + +logger = logging.getLogger(__name__) + +# URL +DATABASE_URL = os.getenv( + "DATABASE_URL", + "postgresql+psycopg2://postgres:postgres@localhost:5432/postgres", +) + +# Configuracion de las conexiones +POOL_SIZE = int(os.getenv("DB_POOL_SIZE", "20")) +MAX_OVERFLOW = int(os.getenv("DB_MAX_OVERFLOW", "10")) +POOL_TIMEOUT = int(os.getenv("DB_POOL_TIMEOUT", "30")) +POOL_RECYCLE = int(os.getenv("DB_POOL_RECYCKE", "3600")) +ECHO = os.getenv("DB_ECHO", "False").lower() == "true" + +# Create Engine +engine = create_engine( + DATABASE_URL, + poolclass=QueuePool, + pool_size=POOL_SIZE, + max_overflow=MAX_OVERFLOW, + pool_timeout=POOL_TIMEOUT, + pool_recycle=POOL_RECYCLE, + pool_pre_ping=True, + echo=ECHO, + connect_args={ + "connect_timeout": 10, + "keepalives": 1, + "keepalives_idle": 30, + "keepalives_interval": 10, + "keepalives_count": 5 + } +) +Base = declarative_base() + +def test_connection(): + """Prueba de conexion a la bd""" + try: + with engine.connect() as conn: + result = conn.execute(text("SELECT 1")) + result.fetchone() + logger.info("Conexion a postgres establecida") + return True + except Exception as e: + logger.error(f"Error al conectar a postgres: {e}") + return False + +# Session Local +sessionLocal = sessionmaker( + autocommit=False, + autoflush=False, + bind=engine, + expire_on_commit=False +) + +def get_db() -> Generator[Session, None, None]: + """Dependencia para FastAPI""" + db = sessionLocal() + try: + yield db + except Exception as e: + logger.error(f"Error en session de base de datos: {e}") + db.rollback() + raise + finally: + db.close() + +@event.listens_for(engine, "connect") +def receive_connect(dbapi_connection, connection_record): + logger.debug("Nueva conexión a PostgreSQL establecida") + +@event.listens_for(engine, "checkout") +def receive_checkout(dbapi_connection, connection_record, connection_proxy): + logger.debug("Conexión tomada del pool") + +@event.listens_for(engine, "checkin") +def receive_checkin(dbapi_connection, connection_record): + logger.debug("Conexión devuelta al pool") + +def dispose_engine(): + engine.dispose() + logger.info("Pool de conexiones cerrado") \ No newline at end of file diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..98d304d --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,50 @@ +# docker-compose.dev.yml +version: '3.8' + +services: + postgres: + image: postgres:15-alpine + container_name: verificaEfos_postgres_1 + environment: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"] + interval: 5s + timeout: 5s + retries: 5 + + app: + build: + context: . + dockerfile: Dockerfile.dev + container_name: verificaEfos_app-1 + environment: + DATABASE_URL: postgresql+psycopg2://postgres:postgres@postgres:5432/postgres + ports: + - "8000:8000" + volumes: + - .:/app + - /app/__pycache__ + working_dir: /app + depends_on: + postgres: + condition: service_healthy + command: > + sh -c " + echo 'Esperando a que PostgreSQL esté listo...' && + sleep 3 && + echo 'Ejecutando migraciones...' && + alembic upgrade head && + echo 'Iniciando aplicación...' && + uvicorn main:app --host 0.0.0.0 --port 8000 --reload + " + +volumes: + postgres_data: \ No newline at end of file diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..e69de29 diff --git a/main.py b/main.py new file mode 100644 index 0000000..88ea359 --- /dev/null +++ b/main.py @@ -0,0 +1,58 @@ +import os +import logging +from contextlib import asynccontextmanager +# +from fastapi import FastAPI +from app.core.servo import get_servo +from database import test_connection + +from app.modules.users.route import router as user_router + +logging.basicConfig(level=logging.INFO, + format= "%(asctime)s - %(levelname)s - %(message)s", + datefmt= "%H:%M:%S") + +logger = logging.getLogger(__name__) + + + +@asynccontextmanager +async def lifespan(app: FastAPI): +# STARTUP + logger.info("=" * 50) + logger.info(" XMA - Xtended Module Architecture") + logger.info("=" * 50) + + servo = get_servo() + await servo.start() + + logger.info(f" Sistema listo | Estado: {servo.state.value.upper()} | Modo: {servo.mode.upper()}") + logger.info("=" * 50) + + yield + + # SHUTDOWN + logger.info(" Apagando XMA...") + await servo.stop() + logger.info(" XMA detenido correctamente") + + +app = FastAPI( + title="XMA - Xtended Module Architecture", + version="1.0.0", + lifespan=lifespan +) + + + +@app.get("/healt") +async def health(): + servo = get_servo() + return servo.get_status() + +@app.get("/") +async def root(): + return{"message": "XMA running", "status": get_servo().state.value} + + +app.include_router(user_router) diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..98e4f9c --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Generic single-database configuration. \ No newline at end of file diff --git a/migrations/__pycache__/env.cpython-311.pyc b/migrations/__pycache__/env.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e7f1698e308040320e97f2c4d91e53780b0ad216 GIT binary patch literal 2777 zcmaJ?-D}%c6u+{rZAt!2;<%|}g4=w!?zW@}Ye(s}(zH<6z!bWN)-OV_lvGBRy(^_{ zN(!hrR3%NXH%rauL)B41qoE&7tWCecHKFWLs&n>#L)4&!>EJ zen-C~5(0vixb>Hs4$rkl9&6!x zq!MxYF!0ez)a4@@SLZ7{gA`PA^U=tgF+b7pJ%p4vTp=Z~1zDfN<*i!seJ*GbJh^$J zTB@F|q#-X~8}a=sBg#l^)IWWbhHQ9P@?M;n%s9}uC> zJ{FY0lQ1_C$TLUDOYwiGce%tk_=aTOfxED(YRc7yzN)UlX;jhIR9z8QNuw@SeJO4> z8X9P$ATgEuCa@_}B5R5%))iBdB~x-DL^0QiUhO{}F!Y_WaJ~5#$TMUD_19*<*1m+^ zWhPjUN63Ue@mGz;a@Rv-ay}1UA(@r2%Df0~VWN1eQkB{{NwThsb#;wM^k|eAk^oPGzLCPa?1I#6 zE)28_ihg&#x#47kPGVzqRfA3;S@1eR-TwsALR}&KS}42{3f8H`wysCXp~=^QYVVjLpyV5EDM-k}j9Bxv4f+ zu(^W86?&|%aJ9SCZm(}Va65|I-`_t9^KhbplZ16Tpsqonm^ zFOG!q*L?mJpKtS1Hb2!vk?`#1Sr=0$J10*4e!0hncwrX-*`uUK$y*L3$L+*iJ8{NN zoB??7vo7hz$L#p&c6`Q;&sgypfZ~C|)dCdxS%BiOY~VZKJQTif6@y1}pU)+`^hx(9 z8OV<1+>rlgYFvOaZf6#jFR?G#%<^>j<%u}ur}1);e_3QHf0m*A2QGgJFJF%S5n_Or zLWBqBaw+Su#)jcUoACA2)D=Qu=p?nq8VuRBd9O_f{Q^0{paZ?OavLTJqrASNR=r`3 zX&5I06Ian?h-?yv6Gi$(7Deb;i72}1#6Ue?Z^-MKVt75}HfZ{wCtYj$5phQ`DbU<0 z)$j+j2%gHX=naUG+h^~%PrYEDlXD=WlgqdO6ka65^dg9fwFLU>pb0CuJ80Gl?j2OJ zg1dtzt>E4X9zjc!;K$r|sc%zHSomFga_y&yAIne6cBW)!&b8Bvc6zammTa_Sp(Sd4 z#zMv6y~C$}m}*6KSl(uH7CYC$$<`O&-?dY7ZCtc*(ZaE2x| literal 0 HcmV?d00001 diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..89c7805 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,55 @@ + + +import sys +from pathlib import Path +from logging.config import fileConfig + +from sqlalchemy import engine_from_config +from sqlalchemy import pool + +from alembic import context + +# Agregar el directorio raíz al path +sys.path.append(str(Path(__file__).parent.parent)) + +# Importar target_metadata desde modules +from app.modules import target_metadata + +config = context.config + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +def run_migrations_offline() -> None: + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + + with context.begin_transaction(): + context.run_migrations() + +def run_migrations_online() -> None: + connectable = engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + + with connectable.connect() as connection: + context.configure( + connection=connection, target_metadata=target_metadata + ) + + with context.begin_transaction(): + context.run_migrations() + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() + + diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..fbc4b07 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/03baecbcb5d9_adjustmen_modules.py b/migrations/versions/03baecbcb5d9_adjustmen_modules.py new file mode 100644 index 0000000..c4a7d9f --- /dev/null +++ b/migrations/versions/03baecbcb5d9_adjustmen_modules.py @@ -0,0 +1,407 @@ +"""adjustmen_modules + +Revision ID: 03baecbcb5d9 +Revises: d41d0457f936 +Create Date: 2026-04-01 18:46:35.084090 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision: str = '03baecbcb5d9' +down_revision: Union[str, None] = 'd41d0457f936' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('configuration', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('smtp_host', sa.String(length=100), nullable=True), + sa.Column('smtp_port', sa.Integer(), nullable=True), + sa.Column('smtp_user', sa.String(length=100), nullable=True), + sa.Column('smtp_password', sa.String(length=100), nullable=True), + sa.Column('is_active', sa.Boolean(), 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), nullable=True), + sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('last_send', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('created_by', sa.Integer(), nullable=False), + sa.Column('updtated_by', sa.Integer(), nullable=True), + sa.Column('deleted_by', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_cofig_active', 'configuration', ['is_active'], unique=False) + op.create_table('credits', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('title', sa.String(length=30), nullable=False), + sa.Column('rfc', sa.String(length=25), nullable=False), + sa.Column('razon_social', sa.String(length=100), nullable=False), + sa.Column('tipo_persona', sa.String(length=100), nullable=False), + sa.Column('supuesto', sa.Enum('CANCELADOS', 'CONDONADOS', 'FIRMES', 'SENTENCIAS', 'EXIGIBLES', 'RETORNO_INVERSIONES', 'FRACCION_X', 'FRACCION_VII', 'NO_LOCALIZADOS', name='supuestos'), nullable=False), + sa.Column('fecha_primera_publicacion', sa.Date(), nullable=False), + sa.Column('fecha_publicacion_ley_transparencia', sa.Date(), nullable=True), + sa.Column('fecha_cancelacion', sa.Date(), nullable=True), + sa.Column('fecha_cancelacion_csd', sa.Date(), nullable=True), + sa.Column('entidad_federativa', sa.String(), nullable=False), + sa.Column('monto', sa.Integer(), nullable=True), + sa.Column('motivo', sa.Text(), nullable=True), + sa.Column('location_id', sa.Integer(), 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=True), + sa.Column('uploaded_by', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('edos', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('numero', sa.Integer(), nullable=False), + sa.Column('razon_social', sa.String(length=100), nullable=False), + sa.Column('situacion', sa.Enum('SENTENCIA_FAVORABLE', 'DEFINITIVO', name='situcion'), nullable=False), + sa.Column('numero_definitivo', sa.String(length=60), nullable=False), + sa.Column('fecha_definitivo', sa.Date(), nullable=False), + sa.Column('publicaccion_sat', sa.Date(), nullable=True), + sa.Column('numero_def_dof', sa.String(length=100), nullable=True), + sa.Column('fecha_def_dof', sa.Date(), nullable=True), + sa.Column('publicacion_dof', sa.Date(), nullable=True), + sa.Column('numero_fav_sat', sa.String(length=100), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('efos', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('numero', sa.Integer(), nullable=False), + sa.Column('rfc', sa.String(length=30), nullable=False), + sa.Column('nombre_contribuyente', sa.String(length=100), nullable=False), + sa.Column('situacion', sa.String(length=60), nullable=False), + sa.Column('publi_presuntos_sat', sa.Date(), nullable=True), + sa.Column('publi_desvirtuados_sat', sa.Date(), nullable=True), + sa.Column('publi_definitivos_sat', sa.Date(), nullable=True), + sa.Column('publi_favorables_sat', sa.Date(), 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=False), + sa.Column('loaded_by', sa.Integer(), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('files', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(length=50), nullable=True), + sa.Column('summary', sa.Text(), nullable=False), + sa.Column('document_url', sa.String(length=100), nullable=False), + sa.Column('file_type', sa.String(length=50), nullable=False), + sa.Column('file_size', sa.Integer(), nullable=False), + sa.Column('feed_id', sa.Integer(), 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), nullable=False), + sa.Column('uploaded_by', sa.Integer(), nullable=False), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('invoices', + sa.Column('id', sa.Integer(), nullable=True), + sa.Column('emisor', sa.String(length=100), nullable=False), + sa.Column('receptor', sa.String(length=110), nullable=False), + sa.Column('uuid', sa.String(length=255), nullable=False), + sa.Column('total', sa.Integer(), nullable=False), + sa.Column('tipo', sa.String(length=50), nullable=True), + sa.Column('date', sa.DateTime(), nullable=True), + sa.Column('verified_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('verified_by', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('location', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('country', sa.String(length=120), nullable=False), + sa.Column('country_id', sa.Integer(), nullable=False), + sa.Column('state', sa.String(length=100), nullable=False), + sa.Column('state_id', sa.Integer(), nullable=False), + sa.Column('city', sa.String(length=100), nullable=False), + sa.Column('city_id', sa.Integer(), nullable=False), + sa.Column('cp_zp', sa.Integer(), nullable=True), + sa.Column('street', sa.String(length=120), nullable=True), + sa.Column('is_department', sa.Boolean(), nullable=False), + sa.Column('number_ext', sa.Integer(), nullable=True), + sa.Column('number_int', sa.Integer(), nullable=True), + sa.Column('is_active', sa.Boolean(), 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), nullable=True), + sa.Column('deleted_at', sa.DateTime(), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.Column('deleted_by', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_location_active', 'location', ['is_active'], unique=False) + op.create_index('idx_location_city', 'location', ['city'], unique=False) + op.create_index('idx_location_country', 'location', ['country'], unique=False) + op.create_index('idx_location_cp', 'location', ['cp_zp'], unique=False) + op.create_index('idx_location_state', 'location', ['state'], unique=False) + op.create_table('users', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('middle_Name', sa.String(length=100), nullable=True), + sa.Column('last_Name', sa.String(length=120), nullable=False), + sa.Column('rfc', sa.String(length=60), nullable=False), + sa.Column('email', sa.String(length=120), nullable=False), + sa.Column('password', sa.String(length=255), nullable=False), + sa.Column('client_id', sa.Integer(), nullable=False), + sa.Column('rol_operativo', sa.Enum('COMPRAS', 'VENTAS', 'LOGISTICA', 'ADUANA_SOFT', 'CLIENTE', 'OPERATIVO', name='rol'), nullable=False), + sa.Column('tipo_usuario', sa.Enum('ROOT', 'ADMIN', 'PRIVILEGED', 'ADMIN_LICENCIAS', 'UPDATER', 'USER', name='tipousuario'), nullable=False), + sa.Column('permite_diot', sa.Boolean(), nullable=False), + sa.Column('enterprise_id', sa.Integer(), nullable=True), + sa.Column('firma', sa.String(length=110), nullable=True), + sa.Column('branches_id', sa.Integer(), nullable=True), + sa.Column('license_id', sa.Integer(), nullable=True), + sa.Column('is_active', sa.Boolean(), 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), nullable=True), + sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('last_login', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.Column('deleted_by', sa.Integer(), nullable=True), + sa.Column('password_reset_token', sa.String(), nullable=True), + sa.Column('password_reset_expires', sa.String(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint('firma'), + sa.UniqueConstraint('rfc') + ) + op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False) + op.create_table('clients', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('rfc', sa.String(length=60), nullable=False), + sa.Column('email', sa.String(length=150), nullable=False), + sa.Column('short_name', sa.String(length=255), nullable=True), + sa.Column('razon_social', sa.String(length=255), nullable=False), + sa.Column('fiscal_number', sa.String(length=15), nullable=False), + sa.Column('cellphone', sa.String(length=20), nullable=False), + sa.Column('medio', sa.Enum('MANUAL', 'DIOT', 'EXCEL', 'FACTURA', name='medio_enum'), nullable=False), + sa.Column('third_type', sa.Enum('NACIONAL', 'EXTRANJERO', 'GLOBAL', name='tercero_enum'), nullable=False), + sa.Column('operation_type', sa.Enum('PRESTACIONSERVICIOSPROFECIONALES', 'ARRENDAMIENTOSINMUENBLES', 'OTROS', name='operacion_enum'), nullable=False), + sa.Column('is_foreign', sa.Boolean(), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('location_id', 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), nullable=True), + sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.Column('deleted_by', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['location_id'], ['location.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint('fiscal_number'), + sa.UniqueConstraint('rfc') + ) + op.create_index(op.f('ix_clients_id'), 'clients', ['id'], unique=False) + op.create_index(op.f('ix_clients_razon_social'), 'clients', ['razon_social'], unique=False) + op.create_table('feed', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('title', sa.String(length=50), nullable=False), + sa.Column('body', sa.Text(), nullable=True), + sa.Column('document_url', sa.String(), nullable=True), + sa.Column('publication_date', sa.Date(), nullable=True), + sa.Column('is_important', sa.Boolean(), 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), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=False), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.Column('file_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('interacctions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('feed_id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('type_iteractions', sa.Enum('LIKE', 'DONT_LIKE', 'APPROVED', 'DISAPRROVE', 'NONE', name='type_interactios'), 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), nullable=True), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('affidavit_logs', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('operation', sa.Enum('DELETED', 'CREATED', 'UPDATED', 'TRIED', 'MOST', 'LESS', 'MINUS', 'CANCELED', name='operation'), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('client_id', sa.Integer(), nullable=True), + sa.Column('description', sa.Text(), nullable=False), + sa.Column('adjustment', sa.String(length=100), nullable=True), + sa.Column('status', sa.Enum('COMPLETED', 'PARTIAL', 'INITIAL', 'NO_PROCECED', 'INVALID', name='status'), nullable=True), + sa.Column('sign', sa.String(), 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), nullable=True), + sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.Column('deleted_by', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['client_id'], ['clients.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_affidavit_client', 'affidavit_logs', ['client_id'], unique=False) + op.create_index('idx_affidavit_created', 'affidavit_logs', ['created_at'], unique=False) + op.create_index('idx_affidavit_operation', 'affidavit_logs', ['operation'], unique=False) + op.create_index('idx_affidavit_status', 'affidavit_logs', ['status'], unique=False) + op.create_index('idx_affidavit_user', 'affidavit_logs', ['user_id'], unique=False) + op.create_table('branches', + sa.Column('id', sa.Integer(), autoincrement=True, nullable=False), + sa.Column('direccion', sa.String(length=255), nullable=False), + sa.Column('cp', sa.Integer(), nullable=False), + sa.Column('is_physical', sa.Boolean(), nullable=False), + sa.Column('location_id', sa.Integer(), nullable=False), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('client_id', 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), nullable=True), + sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.Column('deleted_by', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['client_id'], ['clients.id'], ), + sa.ForeignKeyConstraint(['location_id'], ['location.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_branches_active', 'branches', ['is_active'], unique=False) + op.create_index('idx_branches_client', 'branches', ['client_id'], unique=False) + op.create_index('idx_branches_location', 'branches', ['location_id'], unique=False) + op.create_table('coments', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('texto', sa.Text(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('feed_id', sa.Integer(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('deactivate_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=False), + sa.Column('deactivated_by', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['feed_id'], ['feed.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_index('idx_coments_active', 'coments', ['is_active'], unique=False) + op.create_index('idx_coments_created', 'coments', ['created_at'], unique=False) + op.create_index('idx_coments_feed', 'coments', ['feed_id'], unique=False) + op.create_index('idx_coments_user', 'coments', ['user_id'], unique=False) + op.create_table('license', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('titular', sa.Integer(), nullable=False), + sa.Column('begins_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('ends_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('token_license', sa.String(length=30), nullable=True), + sa.Column('location_id', sa.Integer(), nullable=False), + sa.Column('client_id', sa.Integer(), 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), nullable=True), + sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.Column('deleted_by', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['client_id'], ['clients.id'], ), + sa.ForeignKeyConstraint(['titular'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('moves', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('action_type', sa.Enum('UPLOAD', 'MATCH', 'QUERY', 'CONSUMPTION', 'CREATE', 'DELETE', 'SOLD', 'INTERACTION', 'COMMENT', 'CONFIG', 'EMAIL', name='action_type'), nullable=False), + sa.Column('target_type', sa.Enum('CLIENT', 'INVOICES', 'FEED', 'EFOS', 'CREDITS', 'USERS', 'EMAIL', name='target_type'), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('move_metadata', postgresql.JSONB(astext_type=sa.Text()), nullable=True), + sa.Column('ip_address', sa.String(length=45), nullable=True), + sa.Column('user_agent', sa.String(length=255), nullable=True), + sa.Column('client_id', sa.Integer(), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('now()'), nullable=False), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('deleted_by', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['client_id'], ['clients.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('suppliers', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('rfc', sa.String(length=60), nullable=False), + sa.Column('email', sa.String(length=150), nullable=False), + sa.Column('short_name', sa.String(length=255), nullable=True), + sa.Column('razon_social', sa.String(length=255), nullable=False), + sa.Column('fiscal_number', sa.String(length=15), nullable=False), + sa.Column('cellphone', sa.String(length=20), nullable=False), + sa.Column('supplier_type', sa.Enum('NACIONAL', 'EXTRANJERO', 'GLOBAL', name='suppliertype'), nullable=False), + sa.Column('location_id', sa.Integer(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False), + sa.Column('client_id', sa.Integer(), 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), nullable=True), + sa.Column('deleted_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.Column('deleted_by', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['client_id'], ['clients.id'], ), + sa.ForeignKeyConstraint(['location_id'], ['location.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('email'), + sa.UniqueConstraint('fiscal_number'), + sa.UniqueConstraint('rfc') + ) + op.create_index('idx_suppliers_active', 'suppliers', ['is_active'], unique=False) + op.create_index('idx_suppliers_client', 'suppliers', ['client_id'], unique=False) + op.create_index('idx_suppliers_location', 'suppliers', ['location_id'], unique=False) + op.create_index('idx_suppliers_type', 'suppliers', ['supplier_type'], unique=False) + op.create_index(op.f('ix_suppliers_razon_social'), 'suppliers', ['razon_social'], unique=False) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_index(op.f('ix_suppliers_razon_social'), table_name='suppliers') + op.drop_index('idx_suppliers_type', table_name='suppliers') + op.drop_index('idx_suppliers_location', table_name='suppliers') + op.drop_index('idx_suppliers_client', table_name='suppliers') + op.drop_index('idx_suppliers_active', table_name='suppliers') + op.drop_table('suppliers') + op.drop_table('moves') + op.drop_table('license') + op.drop_index('idx_coments_user', table_name='coments') + op.drop_index('idx_coments_feed', table_name='coments') + op.drop_index('idx_coments_created', table_name='coments') + op.drop_index('idx_coments_active', table_name='coments') + op.drop_table('coments') + op.drop_index('idx_branches_location', table_name='branches') + op.drop_index('idx_branches_client', table_name='branches') + op.drop_index('idx_branches_active', table_name='branches') + op.drop_table('branches') + op.drop_index('idx_affidavit_user', table_name='affidavit_logs') + op.drop_index('idx_affidavit_status', table_name='affidavit_logs') + op.drop_index('idx_affidavit_operation', table_name='affidavit_logs') + op.drop_index('idx_affidavit_created', table_name='affidavit_logs') + op.drop_index('idx_affidavit_client', table_name='affidavit_logs') + op.drop_table('affidavit_logs') + op.drop_table('interacctions') + op.drop_table('feed') + op.drop_index(op.f('ix_clients_razon_social'), table_name='clients') + op.drop_index(op.f('ix_clients_id'), table_name='clients') + op.drop_table('clients') + op.drop_index(op.f('ix_users_id'), table_name='users') + op.drop_table('users') + op.drop_index('idx_location_state', table_name='location') + op.drop_index('idx_location_cp', table_name='location') + op.drop_index('idx_location_country', table_name='location') + op.drop_index('idx_location_city', table_name='location') + op.drop_index('idx_location_active', table_name='location') + op.drop_table('location') + op.drop_table('invoices') + op.drop_table('files') + op.drop_table('efos') + op.drop_table('edos') + op.drop_table('credits') + op.drop_index('idx_cofig_active', table_name='configuration') + op.drop_table('configuration') + # ### end Alembic commands ### diff --git a/migrations/versions/37e3df20754e_xma_architecture_all_modules_with_.py b/migrations/versions/37e3df20754e_xma_architecture_all_modules_with_.py new file mode 100644 index 0000000..8e483e0 --- /dev/null +++ b/migrations/versions/37e3df20754e_xma_architecture_all_modules_with_.py @@ -0,0 +1,36 @@ +"""XMA architecture - all modules with bidirectional relationships + +Revision ID: 37e3df20754e +Revises: 8e2919155783 +Create Date: 2026-04-01 19:35:18.366513 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '37e3df20754e' +down_revision: Union[str, None] = '8e2919155783' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_foreign_key(None, 'credits', 'location', ['location_id'], ['id']) + op.drop_column('users', 'license_id') + op.drop_column('users', 'branches_id') + op.drop_column('users', 'client_id') + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('users', sa.Column('client_id', sa.INTEGER(), autoincrement=False, nullable=False)) + op.add_column('users', sa.Column('branches_id', sa.INTEGER(), autoincrement=False, nullable=True)) + op.add_column('users', sa.Column('license_id', sa.INTEGER(), autoincrement=False, nullable=True)) + op.drop_constraint(None, 'credits', type_='foreignkey') + # ### end Alembic commands ### diff --git a/migrations/versions/84e064c18a0f_adjustmen_modules.py b/migrations/versions/84e064c18a0f_adjustmen_modules.py new file mode 100644 index 0000000..a3b029f --- /dev/null +++ b/migrations/versions/84e064c18a0f_adjustmen_modules.py @@ -0,0 +1,42 @@ +"""adjustmen_modules + +Revision ID: 84e064c18a0f +Revises: 03baecbcb5d9 +Create Date: 2026-04-01 19:09:19.420667 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '84e064c18a0f' +down_revision: Union[str, None] = '03baecbcb5d9' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('branches', sa.Column('user_id', sa.Integer(), nullable=True)) + op.create_index('idx_branches_user', 'branches', ['user_id'], unique=False) + op.create_foreign_key(None, 'branches', 'users', ['user_id'], ['id']) + op.alter_column('invoices', 'id', + existing_type=sa.INTEGER(), + nullable=True, + autoincrement=True) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.alter_column('invoices', 'id', + existing_type=sa.INTEGER(), + nullable=False, + autoincrement=True) + op.drop_constraint(None, 'branches', type_='foreignkey') + op.drop_index('idx_branches_user', table_name='branches') + op.drop_column('branches', 'user_id') + # ### end Alembic commands ### diff --git a/migrations/versions/8e2919155783_base_adjustment_1_5.py b/migrations/versions/8e2919155783_base_adjustment_1_5.py new file mode 100644 index 0000000..f5e38f1 --- /dev/null +++ b/migrations/versions/8e2919155783_base_adjustment_1_5.py @@ -0,0 +1,30 @@ +"""base_adjustment_1.5 + +Revision ID: 8e2919155783 +Revises: b7441b91f586 +Create Date: 2026-04-01 19:14:53.327618 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '8e2919155783' +down_revision: Union[str, None] = 'b7441b91f586' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/migrations/versions/__pycache__/03baecbcb5d9_adjustmen_modules.cpython-311.pyc b/migrations/versions/__pycache__/03baecbcb5d9_adjustmen_modules.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..00f2e95bbac43eb13609b6427bcc7a3b83e4468a GIT binary patch literal 40584 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 ZQbU<-H6kUq(SFqvktT)jg{poS{{Z6D4(R{@ literal 0 HcmV?d00001 diff --git a/migrations/versions/__pycache__/84e064c18a0f_adjustmen_modules.cpython-311.pyc b/migrations/versions/__pycache__/84e064c18a0f_adjustmen_modules.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4f77944e95efc12a5c9bfc7959fb5cd5194e2531 GIT binary patch literal 2216 zcma)7OKclO7@l2^ch?&`Zlkoc6;0eGJRC^sxJhU%mrGv?Qj@BW1Ey%T-kGLb)@yfm z9TEgm4n6ef0}=rRDW{MsIkZx__rReSq_yNjG*S@~;=s))aOsJE{7URn#AxR8Kl4B4 z@qhpNhgeKU(2T;*_NQTleiM^+!!2R&ClJ0tw-7-&L?j|RQbEqig-|XeiFwGug>Ww1 zkg=oWR1(fJ8g|5`BPk{iL77;5Sb24U~+3;h5xJ+4Bq3H3~&icBrT8*Xfe&*+n-ocQKPVH$_u3 zQ`Y1;Gj*q_p4eJlZnaMOlmeWIXf|x zPR-1GpzD8#+a_hM-Jti&)U_xN-*RE2q{QW7X`V+9ZpJm%ItPy)+>Xac@pIScC}VkM zy4GF8G4s^%xK3z^y2LYzE>{@!%gnVpj|_qaBLrh_MiNQ%0o(0fM<$XF$#1uN!oJan z+_l~&1lk1ccFP759NNK~;IrNF+mvAjquHeHT{}CGuprZ-M3fzM6Jl?7;`ewc?MOt> zO>6i>7Nt#ywB3o;hA_DddZp~zka->!3m%tk!Zq7nD%uwH9));}F58}OyNiauQljh- zoQlWHvR|}ai&5aHpNueZL9UyG7*^3K7hEoTCRZ*rWp&Q==^|x33J$<^!$S zog$<5qHBCcS9sKPd^mV3h&nfa^U{?|*B_xh@fI&LXUtOROu=4crVkAD&Md)!8^S@3 z8n0B(L}>+Hqy%n+vQdb11cvt=z$*G3D-W(##=g(5+gp(z(@!%`vQM+mM}wh->d-|!FRS!JhmTy^884ELAOxD3m|~5hL5j}Zs=QAg!b9gv$v~wu7>9V zJlAlne6)dIoE%=yeS3TT_U^GacTb+GtD*47p9tVD0rhTRr{Q=Oj7}3|5#-(b1>p5X z(o+umbO*14h1VfE5LzMXj@Dg1Jyh;coJ62h^#3fb{Zdg#U$140%uM8n z4FX)g$&L!SAAsvEwg6dxx?w>{gB=D9%yeI{+;GhTWpS~jHh9T~h4%>o2>>1^tXKky zxt`BV+x6k=Bo>;a>}x3niP<5 zHGH(ha1;`23p9t5#EU&*YkRB^I~lR>*T4wjTz z6izUoJj7MQAVteCxB`y?<;q0nnMfj6O@|ipw$&6YT%jkD|HTZ6%o@mhQ$K-18xXnJ=dy{bRFR2iu(Z-l=7YJ&s=W3}je0ZK!!qI6x+ TB&f%3IHpNsV(nj{k&J%;ka7^_ literal 0 HcmV?d00001 diff --git a/migrations/versions/__pycache__/8e2919155783_base_adjustment_1_5.cpython-311.pyc b/migrations/versions/__pycache__/8e2919155783_base_adjustment_1_5.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fec32ea8739df099309a4aa3de6fce792dbc300a GIT binary patch literal 1098 zcma)5y>HV%6hD6?aol{gg@O>Gjami*%7>GVOx^$z5rX?WAX?P$Z-b z3>`Bdm4Trfbl^W=WJPkM4$)1OnAjq9>%_Yx{Rj+jdiV2t{=N6^y?6HKcw9xWzPdk} z-$aCda^qsj6XonXD2HeZVPqgIV9^#F(GVTUkOXc^w(NwA&`6i((ugm==E3@qCB4zNuw(ReetOnqITzrk-t7Q)N=9XcbM@*UH5)L%eFLzE&z} z^@_Hwmsiy%lvqAVZ2(kLg?wQ(lP_iRT1u-_wNh0tW{ZWjRjsV5zj(iCktsE@+aYd) zu+WwZ2c-ojw%VI4cKtA>&`BLEcKtXO$F0|{c}d3W)N&g;rft=U?J*UTHgU0MwpKOx%p>qH%%3B|2=b7(v z;syuqk``$U9-HY|7BNlSYM3Sq`}^%CMDUI&|AJY>vWZh~HfV@js<&%fc4LP)`%LsK z%1dRE=Tnw6P0Mv#{&)nYNuxXk|2^mo2i_g!Mf_3Yxw7uGaK|Q(={;cixIKRFLrD;X zp%@nAA-aSF5>^7V{I{HM92iy9m-pi-YBSH-6CR)jrCM?Jb`bt;~W1U zzFOehPh$6Rv7hvlI6M;sub@=o>-Cc1P~9Nwb;qqx+tsvB={?git9EkXWA74+)BY59P02j6Lhsh8U8X#A+lE3ZiObd6B9EOe#v`orJ9zALIUXm~uWY?%jF*^U zm+$D7QKFW^H9~9DCXQaUxyqp{l{#;Y9SJ%~Bz#qG&(g56%y*{Z!tBqX4;CK2^8ya%}tdil>qsNGCN zfGrZlMAxJ}$aK)l2H-<5U~Q2{+8v?}57!;abd!7r9go;`3wmZzK46j!z3Zb3mk+_N z)@>7}#6v>iaM>iOFcaCiW)Py6t5&^YbJ;PtdZR6e7i^cVQO0BC01>)r6S~0%JEzqu zqvo2ee@QogLMP%1FJ>o=T5Ymot}(*}95|Erpj057MuL70p%b-DxJeCy2xTLXX9%{l z4r~iO$LjrCjjqq9% z7f**g47Md7FM$9yA79!T-PQI#64uAL$Jd&8!N&_;m)M(7t_-`kL~F#rPrd{8ufPhv z7Kh4Nc$^z=4=4l^=`i%4;ORjOfS{iD3;+;bWc~1qxYp@Ev>&D$ayM&KX9Hk?ljaH_ z=(bUzEFo05J?R8t)f$Z2c3ftdwhM2UP_*aT*D(qlus0wo=m~pE6pM;?v^}6}a27h} zV_?C6d_3H6cZ&Ppc(~BS1s@kc5c9a@4ZeLe=HdA!p7-%Q2tvEq$nR`6amvRjuS-Fx zl+0p~b1`*+hjpD)%eu~0xR(i)MPHbBe|XriXr*M9JJ@yZT834=Lo1tXSonm6j{^7O z30*gAyXv-~sOu~$?BdUW^$8}B#>8#TUG_z!P^pr-Mc-%RU=<$`=PzI_MUteJtV+0r zdaNHwS3Pv)Rr(zbduQnwz2?+@XyGy*mF+cXMhq6FT Ylx?Y4`b27>SNH`ijS2NXvS1*81IqQpEdT%j literal 0 HcmV?d00001 diff --git a/migrations/versions/__pycache__/b7bce6fc0d7e_fix_moves_relations.cpython-311.pyc b/migrations/versions/__pycache__/b7bce6fc0d7e_fix_moves_relations.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..402bb41a66fc6eaca775684d40d6ed86946a562d GIT binary patch literal 1098 zcma)5J8u&~5Z-&(K41Ju0s$dJVi5`qvVC!|L#&8~P|&2nO2IX!yW2H6=^oCW9g;{0 z7ZfR7AccY=9ppby(juLu6xLResOT)EOU111JPZY_X1|-cZ)SF8_U=nMts+?O_kTFw zM1+2F<6_BUN?8WrhP?z1fp1?6(AxWa*_!>mw zgg!+ilEiXDymOHo0jmsISx<46h$MN%g0FOCfp2o`oBj`9b?BSHsSmQA!5N%}XBNwK zl+W(_4QIFD1v?}xP~w^`C-6g6eMNSh5Y*hu^;*tav20Rp*cx6TBZh>vT*I=g)s@wT zxm>BL&nYomB)1Mw%ayfqwV*vIXiK2hv`TGhwOG}bwdzw<{l)vui%hAL?Kbgk!eW~~ z9F!NB7&JFn>iS_!q2oGO>iTgkjf1zoaY?2vYWnt;;hGk4L#ATVBt8y}z-MtvT5amv zmtzJ+fGxp3ivt|OCDzfguyKrNmcN9J&euw2vsv<-H*_?S(hi|~9%1QveFn_V7|srA z8PBfR++&G$69O@zvyf|!Crto!(5b9+*6B3p|Eh;(z-d^|2zU&g0bohq`Y1cke3uh9 zIB=J|NK^3G$WOC`Vc@_v3>I(gH64iH9aH`VvxMmq&vI-Umu^M|lx{)OfC}c>!*_Q%kU_#?gzo<>r|4ERDSdQ1x{!f_W)?ci0mROO^w50&zSc<}w7d0B54x{UvI`N? zKzc~)FIC3G!=vTTg|9cGxxz`d7@-PC4^{fbaz{OQ(OvBB9!VeH9pPy9(MjrYgvua2 UR35}dAvr*kkeW0n+2LLM25-X;oB#j- literal 0 HcmV?d00001 diff --git a/migrations/versions/__pycache__/c01f58ebc7d4_fix_relationships_foreign_keys.cpython-311.pyc b/migrations/versions/__pycache__/c01f58ebc7d4_fix_relationships_foreign_keys.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dcd3c81db401af38cad756c02703cbce50aa00ab GIT binary patch literal 1120 zcma)5y>HV%6hD6?aol{gg@O>Gjami*O5&tV15w375n|~;)B!J+VNQ5WlyE(tuA9JwCSLqlJ7!g>UUbcIBTio$jW+*vqU7(D;9F4s|a$V=ehC!$W@2A2^@Pb>j|8|aafaB zuAp@CO{-?_r6_UChV8k*w(SS0nn#JfcN}Vm3%>9iW^lXob~`wvy5E;Fj|%Wv!Ggm6n$ak5u&+zj|6^ zN`>q+iE9xS+H~Qdw7^8q-(a!v!>pI}g@F^ZiAw9p7(u zDxdClULGeGx<~`-A+4V;o)h;Dmp^B|UhmFjj+5CgDuVS;v7arp)%~ZPrOw`=^zq#x f?#|vnjy>q20$2|f24PW%4$vgTCc{Z~_%40}KYbWt literal 0 HcmV?d00001 diff --git a/migrations/versions/__pycache__/d41d0457f936_initial_schema.cpython-311.pyc b/migrations/versions/__pycache__/d41d0457f936_initial_schema.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3707777a62ec1de869a1e872da8f2d0c45218885 GIT binary patch literal 1062 zcma)4y>HV%6hD8&vE4Xns}_VnZPcQQfb!k6YE-dM29^#)mB13R+}xE~vK{xF6pDnD zfuUmtq%ts+3HpDqA~{lr=%z|cY>~Qk;$4z{1O_<0``vr~z4!dyz58kyI)b(J;)nA^ zMCd0sE|)w~&cB0lgtibyCc*+1UBMGg(UVL`;I`z-Ud)UQblFwRIF6Yr(TI+fb%?~W zIYA_%VR=WqbD0|f8y~R=&JvMm*DUx-*B1DyBVXe`e6@kEfhRu5W)d4X0cQ%!HIzxd zd*%2})3IGEtnU)f*7fIP-wBNrU8+FTKO7kFa zAc8kb`RinH+a=z%Q>QU*>EWJhyL_+*Obl(xOJ!2nq%37ww(mEZ!|pAMsyv0pfxrJj zca#_LM~xTiy4S!hmprETfaNzG@*(#nK@j?)BFKGo1xX|JWy3rlU|u+^!3y5D(unwpQ$5=a*<^@^1d@!+`pIs0uUn$4c3auKS4 wbWx?3E4B5*r=7*l+hghD`(qqUKRiu5ickrpi%NY(6tq4Xhs3x!&JJer8@W;c$^ZZW literal 0 HcmV?d00001 diff --git a/migrations/versions/__pycache__/fbbb979fa356_fixed_relations_ships.cpython-311.pyc b/migrations/versions/__pycache__/fbbb979fa356_fixed_relations_ships.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1a38d029a9e9c00e45580a5bc3df3394a3b73701 GIT binary patch literal 3369 zcmb_eO>7fK6rQ!mYkT7#pb$z6aSADrmL$#}!6Ax7ASo%3NC8@5MOtk<S{NSQK{cVFee%k1 zdwOneE-{oyEB%A#s|YoR+xv%TKS}q+hXx0zblISaMcc1{7;f*2_nq&G4|K(Q+j|qk zgT2GOiSE8cZ=ydgNq_P`?)0$0O?p31^%P~k+d2%?DKJkqH^o9b2V()F76%LM9FB!a z_JJ-p!0?=*=&8H1rp!^zWD=n{suNSr>P$4Kl{fTMW1>OvL-9g+`NYE)(h|18dkvkq zOG~`-0>Sgbnh&9MDAgqF?c;j~AF)XvKk<;jJNWEp9GI1AB0(Zre70)oUi}E5HE3C{ zHzOiFZ&?Fuy@BsBLqUjqKT+&l3GUPEtcD=^aFa6>AyIOmNuPu7@ITyVmz_lr9Lz&g zETW{-Dp3|xOV+aUCiCS@YPfq_qZS<5xkctt36t_U0*B76ODXphy1GA=%~0JkS%gp} zWvL5wBb=s`+l>q+S2oW(c&3bJY&`SgJ)o<>!jL^UZVyas9^Mok zJYB}qHlD6`&Eeu~ajJByG~wWDWqi%X*8qGpy*#mU{l$sc)8tRHPiMD2JYMm6d~JUr z5Px$~;o=_=`TXt=tcnGH@m#U3)KO}6@YOQDYU8VoLG}cr#nz3^jgL!KDed4JWqiZN zHyWP7!th48`2E{s_ZN;9M~dC0fzoLQPnPkdjVBwPZvz2`YOd) zbMG2hu5)WQ?ZGH;1vg=ho3i#bWi7sE>un|$iGN>A9C#kwHxo-uMcbTpOH+Nn2Wyu% zLsLc!b_d0AAb*ZjdFLF|&q6Urw|S>^*;@I!aNWiU2Peun0g8Ki_G-UV z&}@9s!57Q;A}9@exd{DSa`2@xz6466{j0)=jW0O(LK$BGWsmkZ80Q3MoET@E7-yUq zXPj7#ad?Qkg|bV#eWI&4YvVBokCpKlDD_zO4}cS7cY-@TMhK>Js`CKz%QDHPWSNPu zo^Wp&oEr@Onk+&}Ze)=Qiqt?4Z7i7!|%0D(DS) OIx1Xt+jp1lviu9yhWbMQ literal 0 HcmV?d00001 diff --git a/migrations/versions/b7441b91f586_base_adjustment.py b/migrations/versions/b7441b91f586_base_adjustment.py new file mode 100644 index 0000000..1e0821f --- /dev/null +++ b/migrations/versions/b7441b91f586_base_adjustment.py @@ -0,0 +1,34 @@ +"""base_adjustment + +Revision ID: b7441b91f586 +Revises: 84e064c18a0f +Create Date: 2026-04-01 19:12:39.530941 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'b7441b91f586' +down_revision: Union[str, None] = '84e064c18a0f' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('branches', sa.Column('user_id', sa.Integer(), nullable=True)) + op.create_index('idx_branches_user', 'branches', ['user_id'], unique=False) + op.create_foreign_key(None, 'branches', 'users', ['user_id'], ['id']) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'branches', type_='foreignkey') + op.drop_index('idx_branches_user', table_name='branches') + op.drop_column('branches', 'user_id') + # ### end Alembic commands ### diff --git a/migrations/versions/b7bce6fc0d7e_fix_moves_relations.py b/migrations/versions/b7bce6fc0d7e_fix_moves_relations.py new file mode 100644 index 0000000..6856768 --- /dev/null +++ b/migrations/versions/b7bce6fc0d7e_fix_moves_relations.py @@ -0,0 +1,30 @@ +"""fix-moves-relations + +Revision ID: b7bce6fc0d7e +Revises: fbbb979fa356 +Create Date: 2026-04-01 20:05:19.603068 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'b7bce6fc0d7e' +down_revision: Union[str, None] = 'fbbb979fa356' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/migrations/versions/c01f58ebc7d4_fix_relationships_foreign_keys.py b/migrations/versions/c01f58ebc7d4_fix_relationships_foreign_keys.py new file mode 100644 index 0000000..ee1a79d --- /dev/null +++ b/migrations/versions/c01f58ebc7d4_fix_relationships_foreign_keys.py @@ -0,0 +1,30 @@ +"""fix relationships foreign_keys + +Revision ID: c01f58ebc7d4 +Revises: b7bce6fc0d7e +Create Date: 2026-04-01 20:14:07.773329 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'c01f58ebc7d4' +down_revision: Union[str, None] = 'b7bce6fc0d7e' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/migrations/versions/d41d0457f936_initial_schema.py b/migrations/versions/d41d0457f936_initial_schema.py new file mode 100644 index 0000000..6895b32 --- /dev/null +++ b/migrations/versions/d41d0457f936_initial_schema.py @@ -0,0 +1,30 @@ +"""initial_schema + +Revision ID: d41d0457f936 +Revises: +Create Date: 2026-04-01 17:39:33.561392 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'd41d0457f936' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/migrations/versions/fbbb979fa356_fixed_relations_ships.py b/migrations/versions/fbbb979fa356_fixed_relations_ships.py new file mode 100644 index 0000000..2c1b361 --- /dev/null +++ b/migrations/versions/fbbb979fa356_fixed_relations_ships.py @@ -0,0 +1,56 @@ +"""fixed_Relations_ships + +Revision ID: fbbb979fa356 +Revises: 37e3df20754e +Create Date: 2026-04-01 19:51:19.291930 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'fbbb979fa356' +down_revision: Union[str, None] = '37e3df20754e' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_foreign_key(None, 'affidavit_logs', 'users', ['deleted_by'], ['id']) + op.create_foreign_key(None, 'affidavit_logs', 'users', ['updated_by'], ['id']) + op.create_foreign_key(None, 'affidavit_logs', 'users', ['created_by'], ['id']) + op.create_foreign_key(None, 'coments', 'users', ['deactivated_by'], ['id']) + op.create_foreign_key(None, 'coments', 'users', ['created_by'], ['id']) + op.alter_column('feed', 'created_by', + existing_type=sa.INTEGER(), + nullable=True) + op.create_foreign_key(None, 'feed', 'users', ['created_by'], ['id']) + op.create_foreign_key(None, 'feed', 'users', ['updated_by'], ['id']) + op.create_foreign_key(None, 'interacctions', 'feed', ['feed_id'], ['id']) + op.create_foreign_key(None, 'license', 'location', ['location_id'], ['id']) + op.create_foreign_key(None, 'moves', 'users', ['created_by'], ['id']) + op.create_foreign_key(None, 'moves', 'users', ['deleted_by'], ['id']) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'moves', type_='foreignkey') + op.drop_constraint(None, 'moves', type_='foreignkey') + op.drop_constraint(None, 'license', type_='foreignkey') + op.drop_constraint(None, 'interacctions', type_='foreignkey') + op.drop_constraint(None, 'feed', type_='foreignkey') + op.drop_constraint(None, 'feed', type_='foreignkey') + op.alter_column('feed', 'created_by', + existing_type=sa.INTEGER(), + nullable=False) + op.drop_constraint(None, 'coments', type_='foreignkey') + op.drop_constraint(None, 'coments', type_='foreignkey') + op.drop_constraint(None, 'affidavit_logs', type_='foreignkey') + op.drop_constraint(None, 'affidavit_logs', type_='foreignkey') + op.drop_constraint(None, 'affidavit_logs', type_='foreignkey') + # ### end Alembic commands ### diff --git a/requerimiento_minimos.txt b/requerimiento_minimos.txt new file mode 100644 index 0000000..82b815e --- /dev/null +++ b/requerimiento_minimos.txt @@ -0,0 +1,38 @@ +qeuitar condenados_contenados + +ajustar el envio de notificacions +ajustar los matchs de visualizacion +ajustar las listas y el cargado de las listas CSV del sat(dejar listo para automatizacion) + + + +ajustar dos enpoints para ingresar las listas tal cual las entrega el sat y hacer el handle si cambia el formato +ajustar un proceso para automatizarlo + +las notificacion iran integradas a las interacciones. Coments tiene que estar ligados aun id del feed(publicaciones de inicio), asi como las interacciones. + +En la configuracion agregamos el smtp para el envio de correos. y dejamos espacio para ootras configuraciones. + +cada cliente es el nodo principal de la rastreabilidad, un cliente anida, sus provedores(suppliers) una licencia(license). +con sus sitema tokenizado de activacion ligado al client id. +las acciones del client se tienen que registrar en moves, +si se da el caso, un cliente puede registrar su sucursal (branch), aun si no la tuviera, cada cliente tiene que estar ligado a una +hubicacion registrada el location, para regitrar los movimientos, el cliente registrara su usuario, y con esto el; usuario ligado a un cliente. +registra movimientos de cada consulta que haga. + +enterprises queda como una opcion futura a interacciones complejas entre empresas anidadas al sistema. cliente - cliente(posible multitenant) + +no le veo caso a guaradad impuestos(taxes) ni a los condenados a pagarlos(textpayers) +pero usaremos los taxes como un catalogo de monedas, asi como un comparardor. + +tenemos que ajustar edos, efos y creditos a como entrega sat las listas. en files guardamos las cantidades registradas de cada uno de estos y las fechas de actialuzacion. +buscamos la manera de hacerlo automatico. + + +diot es un servico especifico que aun falta investigar correctamente + +falta ajustar como se van a comprobar las facturas (invoices), que podemos tomar desde las listas de edos efos, y solo guardar el numero cfdi y su estatus respecto al emisor + +reportes no tiene nada en puerta, pero buscaremos como aprovecharlo, tal vez metricas de uso por semana y mes. + + diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..974b93a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,15 @@ +# requirements.txt +fastapi==0.104.1 +uvicorn[standard]==0.24.0 +sqlalchemy==2.0.23 +alembic==1.12.1 +psycopg2-binary==2.9.9 +python-dotenv==1.0.0 +pydantic==2.5.0 +python-jose[cryptography]==3.3.0 +passlib[bcrypt]==1.7.4 +python-multipart==0.0.6 +pyjwt==2.8.0 +email-validator==2.1.0 +python-dateutil==2.8.2 +bcrypt==4.0.1 \ No newline at end of file