Merge pull request 'feature/invoice-def' (#237) from feature/invoice-def into development
Reviewed-on: ADUANASOFT/anexo76#237
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 ###
|
||||
@@ -395,7 +395,7 @@ def validate_common(
|
||||
value=invoice.document_type,
|
||||
)
|
||||
else:
|
||||
if invoice.document_type == "IMD":
|
||||
if invoice.document_type == "IMD" and invoice.invoice_type.upper() != "DEF":
|
||||
errors.add_error(
|
||||
field="document_type",
|
||||
message="El Tipo de Documento no puede ser 'IMD' a menos que sea un Cambio de Régimen.",
|
||||
|
||||
@@ -25,6 +25,10 @@ from .sub_process.review_rule_octave import (
|
||||
)
|
||||
from ...common.process.review_uma import revisa_uma
|
||||
from .sub_process.assing_values import assign_values_lines, assign_values_invoice
|
||||
from .sub_process.assing_values_def_mex import (
|
||||
assign_values_iva_lines,
|
||||
assign_values_invoice_totals,
|
||||
)
|
||||
from ..balance.create_balance_entries import create_balance_entries
|
||||
|
||||
|
||||
@@ -246,8 +250,14 @@ def main_process(db: Session, invoice: InvoiceHeader, tenant_id: str, company_id
|
||||
review_weights_lbs(db, lines, tenant_id, company_id, errors)
|
||||
|
||||
# Paso 3: Asignación de valores por partida y totalización de factura
|
||||
assign_values_lines(invoice, lines)
|
||||
assign_values_invoice(invoice, lines)
|
||||
# Para IMPO DEF / Compras Mexicanas se usa la versión con IVA por partida.
|
||||
invoice_type = (invoice.invoice_type or "").strip().upper()
|
||||
if invoice_type in {"DEF", "MEX"}:
|
||||
assign_values_iva_lines(invoice, lines)
|
||||
assign_values_invoice_totals(invoice, lines)
|
||||
else:
|
||||
assign_values_lines(invoice, lines)
|
||||
assign_values_invoice(invoice, lines)
|
||||
|
||||
# Paso 4: Validaciones per-línea
|
||||
octave_desc, octave_available = _validate_lines(db, invoice, lines, tenant_id, company_id, errors)
|
||||
@@ -284,6 +294,7 @@ def main_process(db: Session, invoice: InvoiceHeader, tenant_id: str, company_id
|
||||
_update_invoice_totals(invoice, lines)
|
||||
|
||||
# Paso 8: Generar saldos en a24.balance_movement (una entrada por partida)
|
||||
create_balance_entries(db, invoice, lines)
|
||||
if invoice_type not in {"DEF", "MEX"}:
|
||||
create_balance_entries(db, invoice, lines)
|
||||
|
||||
db.flush()
|
||||
@@ -0,0 +1,159 @@
|
||||
from decimal import Decimal
|
||||
from typing import List
|
||||
|
||||
from api.v1.modules.a76.invoices.models import Currency, InvoiceHeader
|
||||
from api.v1.modules.a76.items.models import LineItem
|
||||
|
||||
|
||||
def assign_values_iva_lines(
|
||||
invoice: InvoiceHeader,
|
||||
lines: List[LineItem],
|
||||
) -> None:
|
||||
"""
|
||||
ASIGNAVALORES_IVA_PARTIDA
|
||||
Asigna valores por partida para IMPO DEFINITIVA / COMPRAS MEXICANAS
|
||||
calculando subtotal + IVA + total en ME/MN/MC.
|
||||
|
||||
Nota:
|
||||
- Los contadores legacy CantRetornada/CantRetornadaTemp ya no existen.
|
||||
- La trazabilidad de saldos vive en balance_movement + discharges.
|
||||
"""
|
||||
if not invoice.financials:
|
||||
return
|
||||
|
||||
currency = invoice.financials.currency
|
||||
tc = Decimal(str(invoice.financials.exchange_rate or 0))
|
||||
tc_mm = Decimal(str(invoice.financials.exchange_rate_mm or 0))
|
||||
iva_factor = Decimal(str(invoice.financials.iva_factor or 0))
|
||||
|
||||
for line in lines:
|
||||
fin = line.financial
|
||||
qty_rec = line.quantity
|
||||
if fin is None or qty_rec is None:
|
||||
continue
|
||||
|
||||
qty = Decimal(str(qty_rec.quantity or 0))
|
||||
capture = Decimal(str(fin.unit_cost_capture or 0))
|
||||
if qty <= 0:
|
||||
continue
|
||||
|
||||
if currency == Currency.FOREIGN: # ME
|
||||
# ME base
|
||||
fin.unit_cost_usd = capture
|
||||
fin.sub_import_value_usd = capture * qty
|
||||
fin.vat_usd = fin.sub_import_value_usd * iva_factor / 100
|
||||
fin.value_usd = fin.sub_import_value_usd + fin.vat_usd
|
||||
|
||||
# MN converted from ME
|
||||
fin.unit_cost_mxn = capture * tc
|
||||
fin.sub_import_value_mxn = fin.unit_cost_mxn * qty
|
||||
fin.vat_mxn = fin.sub_import_value_mxn * iva_factor / 100
|
||||
fin.value_mxn = fin.sub_import_value_mxn + fin.vat_mxn
|
||||
|
||||
# MC mirrors capture currency in legacy
|
||||
fin.unit_cost_mc = capture
|
||||
fin.sub_import_value_mc = capture * qty
|
||||
fin.vat_mc = fin.sub_import_value_mc * iva_factor / 100
|
||||
fin.value_mc = fin.sub_import_value_mc + fin.vat_mc
|
||||
|
||||
elif currency == Currency.LOCAL: # MN
|
||||
# MN base
|
||||
fin.unit_cost_mxn = capture
|
||||
fin.sub_import_value_mxn = capture * qty
|
||||
fin.vat_mxn = fin.sub_import_value_mxn * iva_factor / 100
|
||||
fin.value_mxn = fin.sub_import_value_mxn + fin.vat_mxn
|
||||
|
||||
# ME converted from MN
|
||||
fin.unit_cost_usd = (capture / tc) if tc else Decimal(0)
|
||||
fin.sub_import_value_usd = fin.unit_cost_usd * qty
|
||||
fin.vat_usd = fin.sub_import_value_usd * iva_factor / 100
|
||||
fin.value_usd = fin.sub_import_value_usd + fin.vat_usd
|
||||
|
||||
# MC mirrors capture currency in legacy
|
||||
fin.unit_cost_mc = capture
|
||||
fin.sub_import_value_mc = capture * qty
|
||||
fin.vat_mc = fin.sub_import_value_mc * iva_factor / 100
|
||||
fin.value_mc = fin.sub_import_value_mc + fin.vat_mc
|
||||
|
||||
elif currency == Currency.MANUAL: # MC
|
||||
# ME from MC * tc_mm
|
||||
fin.unit_cost_usd = capture * tc_mm
|
||||
fin.sub_import_value_usd = fin.unit_cost_usd * qty
|
||||
fin.vat_usd = fin.sub_import_value_usd * iva_factor / 100
|
||||
fin.value_usd = fin.sub_import_value_usd + fin.vat_usd
|
||||
|
||||
# MN from ME * tc
|
||||
fin.unit_cost_mxn = fin.unit_cost_usd * tc
|
||||
fin.sub_import_value_mxn = fin.unit_cost_mxn * qty
|
||||
fin.vat_mxn = fin.sub_import_value_mxn * iva_factor / 100
|
||||
fin.value_mxn = fin.sub_import_value_mxn + fin.vat_mxn
|
||||
|
||||
# MC base
|
||||
fin.unit_cost_mc = capture
|
||||
fin.sub_import_value_mc = capture * qty
|
||||
fin.vat_mc = fin.sub_import_value_mc * iva_factor / 100
|
||||
fin.value_mc = fin.sub_import_value_mc + fin.vat_mc
|
||||
|
||||
|
||||
def assign_values_invoice_totals(
|
||||
invoice: InvoiceHeader,
|
||||
lines: List[LineItem],
|
||||
) -> None:
|
||||
"""
|
||||
ASIGNAVALORES_FACTURA
|
||||
Totaliza cantidades, pesos y valores/IVA en encabezado para IMPO DEF/MEX.
|
||||
"""
|
||||
if not invoice.financials:
|
||||
return
|
||||
|
||||
total_qty = Decimal(0)
|
||||
total_net = Decimal(0)
|
||||
total_gross = Decimal(0)
|
||||
total_packages = 0
|
||||
|
||||
total_val_mn = Decimal(0)
|
||||
total_val_me = Decimal(0)
|
||||
total_val_mc = Decimal(0)
|
||||
total_iva_mn = Decimal(0)
|
||||
total_iva_me = Decimal(0)
|
||||
total_iva_mc = Decimal(0)
|
||||
total_sub_mn = Decimal(0)
|
||||
total_sub_me = Decimal(0)
|
||||
total_sub_mc = Decimal(0)
|
||||
|
||||
for line in lines:
|
||||
q = line.quantity
|
||||
f = line.financial
|
||||
if q:
|
||||
total_qty += Decimal(str(q.quantity or 0))
|
||||
total_net += Decimal(str(q.net_weight or 0))
|
||||
total_gross += Decimal(str(q.gross_weight or 0))
|
||||
total_packages += int(q.package_quantity or 0)
|
||||
if f:
|
||||
total_val_mn += Decimal(str(f.value_mxn or 0))
|
||||
total_val_me += Decimal(str(f.value_usd or 0))
|
||||
total_val_mc += Decimal(str(f.value_mc or 0))
|
||||
total_iva_mn += Decimal(str(f.vat_mxn or 0))
|
||||
total_iva_me += Decimal(str(f.vat_usd or 0))
|
||||
total_iva_mc += Decimal(str(f.vat_mc or 0))
|
||||
total_sub_mn += Decimal(str(f.sub_import_value_mxn or 0))
|
||||
total_sub_me += Decimal(str(f.sub_import_value_usd or 0))
|
||||
total_sub_mc += Decimal(str(f.sub_import_value_mc or 0))
|
||||
|
||||
fin = invoice.financials
|
||||
fin.total_quantity = float(total_qty)
|
||||
fin.net_weight = float(total_net)
|
||||
fin.gross_weight = float(total_gross)
|
||||
fin.total_packages = total_packages
|
||||
|
||||
fin.value_mn = float(total_val_mn)
|
||||
fin.value_me = float(total_val_me)
|
||||
fin.value_mc = float(total_val_mc)
|
||||
|
||||
fin.iva_mn = float(total_iva_mn)
|
||||
fin.iva_me = float(total_iva_me)
|
||||
fin.iva_mc = float(total_iva_mc)
|
||||
|
||||
# No existe subtotal a nivel encabezado en el modelo actual.
|
||||
# Se conserva en partidas (sub_import_value_*), de donde se agrega cuando se necesite.
|
||||
_ = (total_sub_mn, total_sub_me, total_sub_mc)
|
||||
@@ -1,3 +1,5 @@
|
||||
import logging
|
||||
|
||||
from celery import Task
|
||||
|
||||
from core.celery_app import celery_app
|
||||
@@ -12,9 +14,15 @@ from .sub_process.review_exchange_rate import review_exchange_rate
|
||||
from .sub_process.review_weights import review_weights_kgs, review_weights_lbs
|
||||
from .sub_process.review_rule_octave import valida_imp_regla_octava, descuenta_cupo_r_octava
|
||||
from .sub_process.assing_values import assign_values_lines, assign_values_invoice
|
||||
from .sub_process.assing_values_def_mex import (
|
||||
assign_values_iva_lines,
|
||||
assign_values_invoice_totals,
|
||||
)
|
||||
from ..balance.create_balance_entries import create_balance_entries
|
||||
from .main_process import _validate_sisimp_limits, _update_invoice_totals, _validate_lines
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _progress(task: Task, current: int, status: str) -> None:
|
||||
task.update_state(state="PROGRESS", meta={"current": current, "status": status})
|
||||
@@ -64,8 +72,21 @@ def process_invoice_task(self: Task, invoice_id: int, tenant_id: str, company_id
|
||||
|
||||
# ── Paso 4: Asignación de valores ─────────────────────────────────────
|
||||
_progress(self, 50, "Calculando valores por partida...")
|
||||
assign_values_lines(invoice, lines)
|
||||
assign_values_invoice(invoice, lines)
|
||||
raw_type = invoice.invoice_type
|
||||
invoice_type = (raw_type or "").strip().upper()
|
||||
logger.info(
|
||||
"celery import process invoice_type: invoice_id=%s raw=%r normalized=%r document_type=%r",
|
||||
invoice.id,
|
||||
raw_type,
|
||||
invoice_type,
|
||||
getattr(invoice, "document_type", None),
|
||||
)
|
||||
if invoice_type in {"DEF", "MEX"}:
|
||||
assign_values_iva_lines(invoice, lines)
|
||||
assign_values_invoice_totals(invoice, lines)
|
||||
else:
|
||||
assign_values_lines(invoice, lines)
|
||||
assign_values_invoice(invoice, lines)
|
||||
|
||||
# ── Paso 5: Validaciones por partida ──────────────────────────────────
|
||||
_progress(self, 70, "Validando partidas...")
|
||||
@@ -103,7 +124,8 @@ def process_invoice_task(self: Task, invoice_id: int, tenant_id: str, company_id
|
||||
|
||||
# ── Paso 8: Generar saldos en a24.balance_movement ───────────────────
|
||||
_progress(self, 98, "Generando saldos de inventario...")
|
||||
create_balance_entries(db, invoice, lines)
|
||||
if invoice_type not in {"DEF", "MEX"}:
|
||||
create_balance_entries(db, invoice, lines)
|
||||
|
||||
db.flush()
|
||||
db.commit()
|
||||
|
||||
@@ -131,6 +131,7 @@ def _reset_invoice_financials(invoice: InvoiceHeader) -> None:
|
||||
fin.customs_value_me = 0.0
|
||||
fin.iva_mn = 0.0
|
||||
fin.iva_me = 0.0
|
||||
fin.iva_mc = 0.0
|
||||
|
||||
invoice.status = InvoiceStatus.PENDING
|
||||
invoice.process_method = None
|
||||
|
||||
@@ -517,9 +517,9 @@ class InvoiceFinancials(Base, TenantScopedMixin, TimestampMixin):
|
||||
iva_mc: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(23, 8), default=0, server_default="0"
|
||||
) # IVAEXPOMC / IVA en MC
|
||||
iva_factor: Mapped[Optional[str]] = mapped_column(
|
||||
String(10)
|
||||
) # FACTORIVA / Factor IVA (puede ser varchar en imports)
|
||||
iva_factor: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(23, 8), default=0, server_default="0"
|
||||
) # FACTORIVA / Factor IVA
|
||||
tax_value_me: Mapped[Optional[float]] = mapped_column(
|
||||
Numeric(23, 8), default=0, server_default="0"
|
||||
) # VALORIMPUESTOME / Valor impuesto ME
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -107,16 +107,12 @@ class SitarAPIBaseService:
|
||||
# DEBUG LOGGING for SITAR inspection
|
||||
if "fracciones" in url:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"SITAR API Response Headers for {url}: {dict(response.headers)}")
|
||||
logger = logging.getLogger(__name__)
|
||||
try:
|
||||
data = response.json()
|
||||
if isinstance(data, dict):
|
||||
logger.info(f"SITAR API Response Body Keys: {list(data.keys())}")
|
||||
elif isinstance(data, list) and len(data) > 0:
|
||||
logger.info(f"SITAR API Response List Item Keys: {list(data[0].keys())}")
|
||||
data = response.json()
|
||||
return data
|
||||
except Exception:
|
||||
logger.error(f"Error parsing SITAR API Response Body: {response.text}")
|
||||
pass
|
||||
|
||||
return response.json()
|
||||
|
||||
@@ -110,7 +110,7 @@ export interface InvoiceFinancials {
|
||||
iva_mn?: number | null;
|
||||
iva_me?: number | null;
|
||||
iva_mc?: number | null;
|
||||
iva_factor?: string | null;
|
||||
iva_factor?: number | null;
|
||||
tax_value_me?: number | null;
|
||||
seal_value_2500?: boolean | null;
|
||||
total_quantity?: number | null;
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
freight: null as number | null,
|
||||
insurance: null as number | null,
|
||||
iva_mn: null as number | null,
|
||||
iva_factor: null as string | null,
|
||||
iva_factor: null as number | null,
|
||||
total_quantity: null as number | null,
|
||||
gross_weight: null as number | null,
|
||||
net_weight: null as number | null,
|
||||
|
||||
@@ -266,7 +266,7 @@
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Factor IVA</p>
|
||||
<p class="text-base">{invoice.financials.iva_factor || '-'}</p>
|
||||
<p class="text-base">{formatCurrency(invoice.financials.iva_factor)}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
@@ -296,59 +296,57 @@
|
||||
|
||||
<!-- Logistics Tab -->
|
||||
<Tabs.Content value="logistics" class="space-y-4">
|
||||
{#if invoice.logistics && invoice.logistics.length > 0}
|
||||
<div class="space-y-6">
|
||||
{#each invoice.logistics as logistics, index}
|
||||
<div class="border rounded-lg p-4">
|
||||
<h4 class="font-semibold mb-3">Logística #{index + 1}</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Transportista</p>
|
||||
<p class="text-base">{logistics.carrier_id || '-'}</p>
|
||||
</div>
|
||||
{#if invoice.logistics}
|
||||
<div class="space-y-6">
|
||||
<div class="border rounded-lg p-4">
|
||||
<h4 class="font-semibold mb-3">Logística</h4>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Transportista</p>
|
||||
<p class="text-base">{invoice.logistics.carrier_id || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Tipo de Transporte</p>
|
||||
<p class="text-base">{logistics.transport_type || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Tipo de Transporte</p>
|
||||
<p class="text-base">{invoice.logistics.transport_type || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Modo de Transporte</p>
|
||||
<p class="text-base">{logistics.transport_mode || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Modo de Transporte</p>
|
||||
<p class="text-base">{invoice.logistics.transport_mode || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Conductor</p>
|
||||
<p class="text-base">{logistics.driver_name || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Conductor</p>
|
||||
<p class="text-base">{invoice.logistics.driver_name || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Número de Vehículo</p>
|
||||
<p class="text-base">{logistics.vehicle_num || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Número de Vehículo</p>
|
||||
<p class="text-base">{invoice.logistics.vehicle_num || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Placa</p>
|
||||
<p class="text-base">{logistics.license_plate || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Placa</p>
|
||||
<p class="text-base">{invoice.logistics.license_plate || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Número de Sello</p>
|
||||
<p class="text-base">{logistics.seal_number || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Número de Sello</p>
|
||||
<p class="text-base">{invoice.logistics.seal_number || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Guía</p>
|
||||
<p class="text-base">{logistics.guide_number || '-'}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Guía</p>
|
||||
<p class="text-base">{invoice.logistics.guide_number || '-'}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Fecha Entrada/Salida</p>
|
||||
<p class="text-base">{formatDate(logistics.entry_exit_date)}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p class="text-sm font-medium text-muted-foreground">Fecha Entrada/Salida</p>
|
||||
<p class="text-base">{formatDate(invoice.logistics.entry_exit_date)}</p>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-muted-foreground">No hay información de logística disponible.</p>
|
||||
|
||||
@@ -238,7 +238,7 @@ function buildFinancialsData(generalFormData: any, observationFormData: any, oth
|
||||
currency_type: generalFormData?.currency_type || (generalFormData?.currency === 'foreign' ? 'USD' : null),
|
||||
currency: generalFormData?.currency || null,
|
||||
exchange_rate: generalFormData?.exchange_rate ? Number(generalFormData.exchange_rate) : null,
|
||||
iva_factor: (InvoiceTopFieldsFormData?.iva_factor || generalFormData?.iva_factor) ? String(InvoiceTopFieldsFormData?.iva_factor || generalFormData.iva_factor) : null,
|
||||
iva_factor: (InvoiceTopFieldsFormData?.iva_factor || generalFormData?.iva_factor) ? Number(InvoiceTopFieldsFormData?.iva_factor || generalFormData.iva_factor) : null,
|
||||
// Costs & increments from observationFormData
|
||||
freight: observationFormData?.freight || null,
|
||||
insurance: observationFormData?.insurance || null,
|
||||
|
||||
Reference in New Issue
Block a user