Refactor invoice financials to change iva_factor type from string to numeric

- Updated the `iva_factor` field in the `InvoiceFinancials` model to use a numeric type with precision and scale.
- Adjusted related frontend components and API interfaces to reflect the new numeric type for `iva_factor`.
- Enhanced Alembic migration scripts to accommodate the schema changes, ensuring proper index management and data type conversions.

These changes improve data integrity and consistency for financial calculations in invoices.
This commit is contained in:
2026-03-23 09:48:33 -05:00
parent ebd555363f
commit fbb24f2cd3
7 changed files with 225 additions and 51 deletions

View File

@@ -6,6 +6,7 @@ from logging.config import fileConfig
from urllib.parse import quote_plus
from alembic import context
from alembic.operations import ops
from core.config import settings
from core.database import Base
from sqlalchemy import engine_from_config, pool
@@ -78,6 +79,125 @@ fileConfig(config.config_file_name)
target_metadata = Base.metadata
def include_object(object_, name, type_, reflected, compare_to):
"""
Keep all objects in autogenerate.
FK noise is cleaned in process_revision_directives.
"""
return True
def _fk_drop_signature(op_):
if not isinstance(op_, ops.DropConstraintOp):
return None
if getattr(op_, "constraint_type", None) != "foreignkey":
return None
return (
getattr(op_, "schema", None),
getattr(op_, "table_name", None),
getattr(op_, "constraint_name", None),
)
def _fk_create_signature(op_):
if not isinstance(op_, ops.CreateForeignKeyOp):
return None
local_cols = tuple(getattr(op_, "local_cols", ()) or ())
remote_cols = tuple(getattr(op_, "remote_cols", ()) or ())
return (
getattr(op_, "source_schema", None),
getattr(op_, "source_table", None),
getattr(op_, "referent_schema", None),
getattr(op_, "referent_table", None),
local_cols,
remote_cols,
)
def _drop_to_create_match(drop_op, create_op):
if not isinstance(drop_op, ops.DropConstraintOp):
return False
if not isinstance(create_op, ops.CreateForeignKeyOp):
return False
if getattr(drop_op, "constraint_type", None) != "foreignkey":
return False
def _normalize_schema(value):
# PostgreSQL reports default schema inconsistently as None/public.
return "public" if value in (None, "") else value
# Prefer structural comparison using Alembic's reverse op when available.
reverse_create = getattr(drop_op, "_reverse", None)
if isinstance(reverse_create, ops.CreateForeignKeyOp):
return (
_normalize_schema(getattr(reverse_create, "source_schema", None))
== _normalize_schema(getattr(create_op, "source_schema", None))
and getattr(reverse_create, "source_table", None) == getattr(create_op, "source_table", None)
and _normalize_schema(getattr(reverse_create, "referent_schema", None))
== _normalize_schema(getattr(create_op, "referent_schema", None))
and getattr(reverse_create, "referent_table", None) == getattr(create_op, "referent_table", None)
and tuple(getattr(reverse_create, "local_cols", ()) or ())
== tuple(getattr(create_op, "local_cols", ()) or ())
and tuple(getattr(reverse_create, "remote_cols", ()) or ())
== tuple(getattr(create_op, "remote_cols", ()) or ())
)
# Fallback for older op payloads: compare source table/schema and name.
return (
_normalize_schema(getattr(drop_op, "schema", None)) == _normalize_schema(getattr(create_op, "source_schema", None))
and getattr(drop_op, "table_name", None) == getattr(create_op, "source_table", None)
and getattr(drop_op, "constraint_name", None) == getattr(create_op, "constraint_name", None)
)
def _prune_fk_churn(container):
if not hasattr(container, "ops"):
return
# First recurse into nested containers.
for op_ in list(container.ops):
_prune_fk_churn(op_)
table_ops = container.ops
kept_ops = []
consumed_indexes = set()
for i, op_i in enumerate(table_ops):
if i in consumed_indexes:
continue
if isinstance(op_i, ops.DropConstraintOp) and getattr(op_i, "constraint_type", None) == "foreignkey":
matched_j = None
for j in range(i + 1, len(table_ops)):
if j in consumed_indexes:
continue
op_j = table_ops[j]
if _drop_to_create_match(op_i, op_j):
matched_j = j
break
if matched_j is not None:
# Drop + recreate same FK detected; remove both.
consumed_indexes.add(i)
consumed_indexes.add(matched_j)
continue
kept_ops.append(op_i)
container.ops = kept_ops
def process_revision_directives(context_, revision, directives):
"""
Remove autogenerate noise where Alembic emits drop/create for equivalent FKs.
Real FK changes are preserved.
"""
if not directives:
return
script = directives[0]
_prune_fk_churn(script.upgrade_ops)
_prune_fk_churn(script.downgrade_ops)
def import_models_from_dir(dir_path: str):
"""Importa recursivamente cualquier archivo models.py desde dir_path y archivos en directorios models/"""
import sys
@@ -132,6 +252,10 @@ def run_migrations_offline() -> None:
context.configure(
url=url,
target_metadata=target_metadata,
compare_type=True,
include_schemas=True,
include_object=include_object,
process_revision_directives=process_revision_directives,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
@@ -153,7 +277,14 @@ def run_migrations_online() -> None:
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True,
include_schemas=True,
include_object=include_object,
process_revision_directives=process_revision_directives,
)
with context.begin_transaction():
context.run_migrations()

View File

@@ -0,0 +1,45 @@
"""iva_factor
Revision ID: bccb7f8986c7
Revises: 9f3c2d1b7a11
Create Date: 2026-03-23 09:44:02.275257
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = 'bccb7f8986c7'
down_revision: Union[str, Sequence[str], None] = '9f3c2d1b7a11'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_public_carta_porte_code'), table_name='carta_porte_codes')
op.create_index(op.f('ix_public_carta_porte_codes_code'), 'carta_porte_codes', ['code'], unique=False, schema='public')
op.alter_column('invoice_financials', 'iva_factor',
existing_type=sa.VARCHAR(length=10),
type_=sa.Numeric(precision=23, scale=8),
postgresql_using='iva_factor::numeric(23,8)',
existing_nullable=True,
schema='a76')
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.alter_column('invoice_financials', 'iva_factor',
existing_type=sa.Numeric(precision=23, scale=8),
type_=sa.VARCHAR(length=10),
existing_nullable=True,
schema='a76')
op.drop_index(op.f('ix_public_carta_porte_codes_code'), table_name='carta_porte_codes', schema='public')
op.create_index(op.f('ix_public_carta_porte_code'), 'carta_porte_codes', ['code'], unique=False)
# ### end Alembic commands ###