Refactor 'download' field to 'discharge' across models and services

- Renamed the 'download' field to 'discharge' in the FaLineItem model and updated related services and schemas accordingly.
- Adjusted frontend components to reflect the new 'discharge' terminology.
- Ensured consistency in filtering and validation logic across the application to use the updated field name.
This commit is contained in:
2026-03-17 16:25:23 -05:00
parent 5a619e8bc4
commit 3eacbf18f2
15 changed files with 44 additions and 216 deletions

View File

@@ -70,7 +70,7 @@ class FaLineItem(Base, TenantScopedMixin, TimestampMixin):
subitem_number: Mapped[Optional[int]] = mapped_column(Integer) # SUBPARTIDA
# Special flags
download: Mapped[Optional[bool]] = mapped_column(Boolean) # DESCARGA
discharge: Mapped[Optional[bool]] = mapped_column(Boolean) # DESCARGA
own_equipment: Mapped[Optional[bool]] = mapped_column(Boolean) # EQUIPOPROPIO
omit_annex31: Mapped[Optional[bool]] = mapped_column(Boolean) # OMITITENANEXO31

View File

@@ -48,8 +48,8 @@ class FaLineItemService:
query = query.filter(
FaLineItem.own_equipment == filters["own_equipment"]
)
if filters.get("download") is not None:
query = query.filter(FaLineItem.download == filters["download"])
if filters.get("discharge") is not None:
query = query.filter(FaLineItem.discharge == filters["discharge"])
total = query.count()
items = query.offset(skip).limit(limit).all()
@@ -146,7 +146,7 @@ class FaLineItemService:
search_invoice=fa_line_item_data.search_invoice,
search_line=fa_line_item_data.search_line,
search_type=fa_line_item_data.search_type,
download=fa_line_item_data.download,
discharge=fa_line_item_data.discharge,
own_equipment=fa_line_item_data.own_equipment,
omit_annex31=fa_line_item_data.omit_annex31,
)

View File

@@ -23,7 +23,7 @@ from .sub_process.review_rule_octave import (
valida_imp_regla_octava,
descuenta_cupo_r_octava,
)
from .sub_process.review_uma import revisa_uma
from ...common.process.review_uma import revisa_uma
from .sub_process.assing_values import assign_values_lines, assign_values_invoice

View File

@@ -30,7 +30,7 @@ def pre_validators(db: Session, invoice: InvoiceHeader, tenant_id: str, company_
errors.add_required_error("compliance_mx.sold_to_id")
if not invoice.compliance_mx.shipped_to_id:
errors.add_required_error("compliance_mx.shipped_by_id")
errors.add_required_error("compliance_mx.shipped_to_id")
if not invoice.compliance_mx.customs_broker_id:
errors.add_required_error("compliance_mx.customs_broker_id")

View File

@@ -6,13 +6,13 @@ from typing import Optional, List
from sqlalchemy.orm import Session
from api.v1.modules.a76.general_catalogs.fractions.previous_fractions.models import PreviousFraction
from api.v1.modules.a76.general_catalogs.unit_conversions.models import UnitConversion
from api.v1.modules.a76.invoices.models import InvoiceHeader
from api.v1.modules.a76.items.models import LineItem
from api.v1.modules.a76.rule_octave.balances.models import OctaveBalance
from api.v1.modules.a76.rule_octave.country.models import CountryRuleOct
from api.v1.modules.a76.rule_octave.fractions.models import FractionRuleOctave
from api.v1.modules.a76.rule_octave.permissions.models import OctavePermission
from ....common.process.review_equivalence import _get_unit_equivalence
from core.exceptions import ErrorCollector
# Clarion date 80354 ≈ 2010-05-31 (see previous_fractions/models.py)
@@ -50,51 +50,6 @@ class OctavePermitEntry:
# Helpers
# ─────────────────────────────────────────────────────────────────────────────
def _get_unit_equivalence(
db: Session,
from_unit: str,
to_unit: str,
tenant_id: str,
company_id: str,
) -> tuple[str, Decimal]:
"""
Busca una conversión entre dos unidades de medida.
Paridad: REVEQUIVALENCIA (Clarion SCAII).
Retorna (multi_divide, factor_conv):
- ('M', factor) → multiplicar cantidad por factor
- ('D', factor) → dividir cantidad por factor
- ('', 0) → no existe equivalencia
"""
conv = (
db.query(UnitConversion)
.filter(
UnitConversion.tenant_id == tenant_id,
UnitConversion.company_id == company_id,
UnitConversion.from_unit_code == from_unit,
UnitConversion.to_unit_code == to_unit,
)
.first()
)
if conv and conv.conversion_factor:
return "M", conv.conversion_factor
conv_inv = (
db.query(UnitConversion)
.filter(
UnitConversion.tenant_id == tenant_id,
UnitConversion.company_id == company_id,
UnitConversion.from_unit_code == to_unit,
UnitConversion.to_unit_code == from_unit,
)
.first()
)
if conv_inv and conv_inv.conversion_factor:
return "D", conv_inv.conversion_factor
return "", Decimal(0)
def _previous_fraction_exists(
db: Session,
tenant_id: str,

View File

@@ -1,129 +0,0 @@
from decimal import Decimal
from sqlalchemy.orm import Session
from api.v1.modules.a76.general_catalogs.units_of_measure.models import UnitOfMeasureCustoms
from api.v1.modules.a76.items.models import LineItem
from core.exceptions import ErrorCollector
from .review_rule_octave import _get_unit_equivalence
def revisa_uma(
db: Session,
line: LineItem,
tenant_id: str,
company_id: str,
errors: ErrorCollector,
) -> None:
"""
Revisa la unidad de medida americana (UMA) de aduana para una partida.
Paridad: REVISA_UMA (Clarion SCAII).
Flujo:
1. Verifica que la UM de la partida exista en el catálogo GUnimedida.
2. Verifica que esa UM tenga asignada una Clave_AMex (UM aduana mexicana).
3. Verifica que la Clave_AMex exista en el catálogo GUMAduana.
4. Verifica que el registro de aduana tenga una UnidadSCAII (a76_unit_code).
5. Calcula la cantidad UMA:
- Si la UM de la partida coincide con UnidadSCAII → cantidad directa.
- Si no → busca conversión en el catálogo de equivalencias.
6. Escribe CantImpoUMA y ClaveUMA de regreso en la partida.
"""
uom = line.unit_of_measure_info
line_um_code = uom.code if uom else ""
# ── 1. Verificar existencia de la UM en el catálogo ──────────────────────
if uom is None:
errors.add_error(
field=f"line[{line.line_number}].unit_of_measure",
message=(
f"No existe la unidad de medida {line_um_code}"
" en el catálogo de Unidades de Medida."
),
solution=["Capturarla en el catálogo de Unidades de Medida."],
code="UNIMEDIDA",
)
return
# ── 2. Verificar que tenga Clave_AMex asignada ───────────────────────────
customs_code = uom.customs_code or ""
if not customs_code:
errors.add_error(
field=f"line[{line.line_number}].unit_of_measure",
message=(
f"No existe la unidad de medida de la aduana para la unidad de medida "
f"{line_um_code} en el catálogo de Unidades de Medida."
),
solution=["Asignar la U.M.A en el catálogo de Unidades de Medida."],
code="UNIMEDIDA",
)
return
# ── 3. Buscar la UM en el catálogo de Aduana Mex (GUMAduana) ─────────────
customs_uom = (
db.query(UnitOfMeasureCustoms)
.filter(UnitOfMeasureCustoms.code == customs_code)
.first()
)
if customs_uom is None:
errors.add_error(
field=f"line[{line.line_number}].unit_of_measure",
message=(
f"No existe la unidad de medida {customs_code}"
" en el catálogo de Unidades de Medida de la Aduana Mex."
),
solution=["Actualizar sus Catálogos Fijos o llamar a su proveedor."],
code="UNIMEDIDA",
)
return
# ── 4. Verificar que tenga UnidadSCAII asignada ──────────────────────────
scaii_unit = customs_uom.a76_unit_code or ""
if not scaii_unit:
errors.add_error(
field=f"line[{line.line_number}].unit_of_measure",
message=(
f"No tiene asignada la U.M. Equivalente SCAII la unidad de medida "
f"{customs_code} en el catálogo de Unidades de Medida de la Aduana Mex."
),
solution=["Actualizar sus Catálogos Fijos o llamar a su proveedor."],
code="UNIMEDIDA",
)
return
# ── 5. Calcular cantidad UMA ──────────────────────────────────────────────
line_qty = Decimal(str(line.quantity.quantity or 0)) if line.quantity else Decimal(0)
cant_uma = Decimal(0)
if line_um_code == scaii_unit:
cant_uma = line_qty
else:
multi_divide, factor_conv = _get_unit_equivalence(
db, line_um_code, scaii_unit, tenant_id, company_id
)
if multi_divide == "M":
cant_uma = line_qty * factor_conv
elif multi_divide == "D":
cant_uma = line_qty / factor_conv if factor_conv else Decimal(0)
else:
errors.add_error(
field=f"line[{line.line_number}].unit_of_measure",
message=(
f"No hay equivalencia entre la U.M. Partida: {line_um_code}"
f" y la U.M. Aduana {scaii_unit}."
),
solution=[
"Capturar su equivalencia en el Catálogo de Conversiones "
"o configurar la U.M.Aduana correcta en la U.M. Comercial."
],
code="EQUIVALENCIA",
)
return
# ── 6. Escribir resultados en la partida ──────────────────────────────────
if line.quantity:
line.quantity.quantity_uma = cant_uma
line.uma_key = customs_code

View File

@@ -451,6 +451,8 @@ class InvoiceCollectionsCreate(InvoiceCollectionsBase):
class InvoiceHeaderCreate(InvoiceHeaderBase):
"""Schema for creating Invoice Header with nested relations"""
status: Optional[InvoiceStatus] = Field(InvoiceStatus.PENDING, description="Status: pending, processed, reversed")
compliance_mx: Optional[InvoiceComplianceMxCreate] = None
financials: Optional[InvoiceFinancialsCreate] = None
logistics: Optional[InvoiceLogisticsCreate] = None

View File

@@ -95,7 +95,7 @@ def calculate_values(
Traduce el CALCULOS ROUTINE de Clarion:
- Busca la factura de importación por fa_data.search_invoice (TEM → DEF como fallback)
- Copia clase, unidad de medida, fracción (si fa_data.download), país, tipo fracción,
- Copia clase, unidad de medida, fracción (si fa_data.discharge), país, tipo fracción,
bultos y descripción inglés desde la línea de importación encontrada
- Calcula valores en moneda (USD/MXN/MC) según la moneda de la factura
"""
@@ -168,8 +168,8 @@ def calculate_values(
line.class_id = import_line.class_id
line.unit_of_measure = import_line.unit_of_measure
# Fracción: copiar solo si fa_data.download == True (≡ ColumnaV != '')
if fa_data and fa_data.download and import_line.customs:
# Fracción: copiar solo si fa_data.discharge == True (≡ ColumnaV != '')
if fa_data and fa_data.discharge and import_line.customs:
line.customs.fraction = import_line.customs.fraction
if import_line.customs:

View File

@@ -222,7 +222,7 @@ def validate_common(
)
elif (
# Col. H: valida unidad de medida sólo cuando hay descarga
fa_data.download is True
fa_data.discharge is True
and line.unit_of_measure
and import_line.unit_of_measure
and line.unit_of_measure != import_line.unit_of_measure

View File

@@ -83,11 +83,11 @@ def validate_create(
errors.add_required_error(field=f"line[{line_number}].fa_data.movement_type_import")
# Col. F: ¿Descarga la línea? (DescargaPartida) — obligatorio
if fa_data.download is None:
errors.add_required_error(field=f"line[{line_number}].fa_data.download")
if fa_data.discharge is None:
errors.add_required_error(field=f"line[{line_number}].fa_data.discharge")
# Col. D / E: Factura y Línea de Impo — obligatorios sólo si hay descarga
if fa_data.download is True:
if fa_data.discharge is True:
if not fa_data.search_invoice:
errors.add_required_error(field=f"line[{line_number}].fa_data.search_invoice")
if not fa_data.search_line:

View File

@@ -207,8 +207,8 @@ def validate_update(
fa_data.movement_type_import = existing_fa_data.movement_type_import
# Col. F: ¿Descarga la línea? (Descarga)
if fa_data.download is None:
fa_data.download = existing_fa_data.download
if fa_data.discharge is None:
fa_data.discharge = existing_fa_data.discharge
# Col. D: Factura de Importación — obligatoria sólo si hay descarga
if not fa_data.search_invoice:

View File

@@ -1,5 +1,5 @@
from typing import Optional
from sqlalchemy import ForeignKey, Integer, String
from sqlalchemy import Boolean, ForeignKey, Integer, String
from sqlalchemy.orm import Mapped, mapped_column
from api.v1.common.base_models import TenantScopedMixin, TimestampMixin
from core.database import Base
@@ -16,9 +16,9 @@ class Serie(Base, TenantScopedMixin, TimestampMixin):
serial_numbers: Mapped[Optional[str]] = mapped_column(String(50)) # SERIEEXPO
model: Mapped[Optional[str]] = mapped_column(String(50)) # MODELOEXPO
sub_model: Mapped[Optional[str]] = mapped_column(String(50)) # SUBMODELOEXPO
brand: Mapped[Optional[str]] = mapped_column(String(50)) # MARCA
expo_brad: Mapped[Optional[str]] = mapped_column(String(50)) # MARCAEXPO
brand: Mapped[Optional[str]] = mapped_column(String(50)) # MARCAEXPO
number_id: Mapped[Optional[str]] = mapped_column(String(25)) # NUMIDEXPO
discharge: Mapped[Optional[bool]] = mapped_column(Boolean) # MARCA

View File

@@ -433,11 +433,11 @@ class RepairImportQueries:
@staticmethod
def build_aggregated_query(db_name: str, where_str: str, discharge_clause: str = "") -> str:
"""Build optimized query for NORMAL mode (grouped by invoice with totals)."""
# Use fa_item_lines.download field (true = discharged, false = not discharged)
# Use fa_item_lines.discharge field (true = discharged, false = not discharged)
if "SiDes" in discharge_clause:
discharge_filter = "AND fil.download = true"
discharge_filter = "AND fil.discharge = true"
elif "NoDes" in discharge_clause:
discharge_filter = "AND fil.download = false"
discharge_filter = "AND fil.discharge = false"
else:
discharge_filter = ""
@@ -514,11 +514,11 @@ class RepairImportQueries:
@staticmethod
def build_main_query(db_name: str, where_str: str, discharge_clause: str = "") -> str:
"""Build main SQL query for repair import data."""
# Use fa_item_lines.download field (true = discharged, false = not discharged)
# Use fa_item_lines.discharge field (true = discharged, false = not discharged)
if "SiDes" in discharge_clause:
discharge_filter = "AND fil.download = true"
discharge_filter = "AND fil.discharge = true"
elif "NoDes" in discharge_clause:
discharge_filter = "AND fil.download = false"
discharge_filter = "AND fil.discharge = false"
else:
discharge_filter = ""
return f"""
@@ -609,11 +609,11 @@ class RepairImportQueries:
@staticmethod
def build_totals_query(db_name: str, discharge_clause: str = "") -> str:
"""Build query to get totals for a repair import invoice."""
# Use fa_item_lines.download field (true = discharged, false = not discharged)
# Use fa_item_lines.discharge field (true = discharged, false = not discharged)
if "SiDes" in discharge_clause:
discharge_filter = "AND fil.download = true"
discharge_filter = "AND fil.discharge = true"
elif "NoDes" in discharge_clause:
discharge_filter = "AND fil.download = false"
discharge_filter = "AND fil.discharge = false"
else:
discharge_filter = ""
return f"""
@@ -818,11 +818,11 @@ class ExportQueries:
Only sums partidas where is_subitem is false (main partidas, not sub-items).
"""
# Use fa_item_lines.download field (true = discharged, false = not discharged)
# Use fa_item_lines.discharge field (true = discharged, false = not discharged)
if "SiDes" in discharge_clause:
discharge_filter = "AND fil.download = true"
discharge_filter = "AND fil.discharge = true"
elif "NoDes" in discharge_clause:
discharge_filter = "AND fil.download = false"
discharge_filter = "AND fil.discharge = false"
else:
discharge_filter = ""
return f"""
@@ -861,11 +861,11 @@ class ExportRepairQueries:
@staticmethod
def build_aggregated_query(db_name: str, where_str: str, discharge_clause: str = "") -> str:
"""Build optimized query for NORMAL mode (grouped by invoice with totals)."""
# Use fa_item_lines.download field (true = discharged, false = not discharged)
# Use fa_item_lines.discharge field (true = discharged, false = not discharged)
if "SiDes" in discharge_clause:
discharge_filter = "AND fil.download = true"
discharge_filter = "AND fil.discharge = true"
elif "NoDes" in discharge_clause:
discharge_filter = "AND fil.download = false"
discharge_filter = "AND fil.discharge = false"
else:
discharge_filter = ""
@@ -1033,11 +1033,11 @@ class ExportRepairQueries:
@staticmethod
def build_totals_query(db_name: str, discharge_clause: str = "") -> str:
"""Build query to get totals for an export repair invoice."""
# Use fa_item_lines.download field (true = discharged, false = not discharged)
# Use fa_item_lines.discharge field (true = discharged, false = not discharged)
if "SiDes" in discharge_clause:
discharge_filter = "AND fil.download = true"
discharge_filter = "AND fil.discharge = true"
elif "NoDes" in discharge_clause:
discharge_filter = "AND fil.download = false"
discharge_filter = "AND fil.discharge = false"
else:
discharge_filter = ""
return f"""

View File

@@ -127,7 +127,7 @@ export interface FaLineItem {
subitem_number?: number;
// Special flags
download?: boolean;
discharge?: boolean;
own_equipment?: boolean;
omit_annex31?: boolean;

View File

@@ -263,10 +263,10 @@
<div class="space-y-1.5">
<Label class="text-xs font-medium text-muted-foreground">Genera Descarga?</Label>
<RadioGroup
value={editingItem.fa_data?.download === true ? 'si' : 'no'}
value={editingItem.fa_data?.discharge === true ? 'si' : 'no'}
onValueChange={(v) => {
editingItem.fa_data = editingItem.fa_data || {};
editingItem.fa_data.download = v === 'si';
editingItem.fa_data.discharge = v === 'si';
}}
class="flex gap-4"
>
@@ -355,10 +355,10 @@
<div class="space-y-1.5">
<Label class="text-xs font-medium text-muted-foreground">Genera Descarga?</Label>
<RadioGroup
value={editingItem.fa_data?.download === true ? 'si' : 'no'}
value={editingItem.fa_data?.discharge === true ? 'si' : 'no'}
onValueChange={(v) => {
editingItem.fa_data = editingItem.fa_data || {};
editingItem.fa_data.download = v === 'si';
editingItem.fa_data.discharge = v === 'si';
}}
class="flex gap-4"
>