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:
@@ -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()
|
||||
|
||||
45
backend/alembic/versions/bccb7f8986c7_iva_factor.py
Normal file
45
backend/alembic/versions/bccb7f8986c7_iva_factor.py
Normal 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 ###
|
||||
Reference in New Issue
Block a user