diff --git a/backend/api/v1/modules/a24/fa/fa_item_lines/models.py b/backend/api/v1/modules/a24/fa/fa_item_lines/models.py index 75496b34..f07b73c8 100644 --- a/backend/api/v1/modules/a24/fa/fa_item_lines/models.py +++ b/backend/api/v1/modules/a24/fa/fa_item_lines/models.py @@ -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 diff --git a/backend/api/v1/modules/a24/fa/fa_item_lines/service.py b/backend/api/v1/modules/a24/fa/fa_item_lines/service.py index 890307bf..aaa8e851 100644 --- a/backend/api/v1/modules/a24/fa/fa_item_lines/service.py +++ b/backend/api/v1/modules/a24/fa/fa_item_lines/service.py @@ -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, ) diff --git a/backend/api/v1/modules/a76/invoices/imports/process/main_process.py b/backend/api/v1/modules/a76/invoices/imports/process/main_process.py index cfadf0dd..ea28d7de 100644 --- a/backend/api/v1/modules/a76/invoices/imports/process/main_process.py +++ b/backend/api/v1/modules/a76/invoices/imports/process/main_process.py @@ -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 diff --git a/backend/api/v1/modules/a76/invoices/imports/process/pre_validators.py b/backend/api/v1/modules/a76/invoices/imports/process/pre_validators.py index 1e96b84a..c1c47b7f 100644 --- a/backend/api/v1/modules/a76/invoices/imports/process/pre_validators.py +++ b/backend/api/v1/modules/a76/invoices/imports/process/pre_validators.py @@ -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") diff --git a/backend/api/v1/modules/a76/invoices/imports/process/sub_process/review_rule_octave.py b/backend/api/v1/modules/a76/invoices/imports/process/sub_process/review_rule_octave.py index 3f19cd68..431070f5 100644 --- a/backend/api/v1/modules/a76/invoices/imports/process/sub_process/review_rule_octave.py +++ b/backend/api/v1/modules/a76/invoices/imports/process/sub_process/review_rule_octave.py @@ -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, diff --git a/backend/api/v1/modules/a76/invoices/imports/process/sub_process/review_uma.py b/backend/api/v1/modules/a76/invoices/imports/process/sub_process/review_uma.py deleted file mode 100644 index b4e104b9..00000000 --- a/backend/api/v1/modules/a76/invoices/imports/process/sub_process/review_uma.py +++ /dev/null @@ -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 diff --git a/backend/api/v1/modules/a76/invoices/schemas.py b/backend/api/v1/modules/a76/invoices/schemas.py index f0edde67..e6aee827 100644 --- a/backend/api/v1/modules/a76/invoices/schemas.py +++ b/backend/api/v1/modules/a76/invoices/schemas.py @@ -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 diff --git a/backend/api/v1/modules/a76/items/exports/validators/calculations.py b/backend/api/v1/modules/a76/items/exports/validators/calculations.py index 76dfa6a3..26b73c85 100644 --- a/backend/api/v1/modules/a76/items/exports/validators/calculations.py +++ b/backend/api/v1/modules/a76/items/exports/validators/calculations.py @@ -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: diff --git a/backend/api/v1/modules/a76/items/exports/validators/common.py b/backend/api/v1/modules/a76/items/exports/validators/common.py index 31ace4a5..b5b36fb4 100644 --- a/backend/api/v1/modules/a76/items/exports/validators/common.py +++ b/backend/api/v1/modules/a76/items/exports/validators/common.py @@ -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 diff --git a/backend/api/v1/modules/a76/items/exports/validators/create.py b/backend/api/v1/modules/a76/items/exports/validators/create.py index 8ab754f8..9c19d6bf 100644 --- a/backend/api/v1/modules/a76/items/exports/validators/create.py +++ b/backend/api/v1/modules/a76/items/exports/validators/create.py @@ -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: diff --git a/backend/api/v1/modules/a76/items/exports/validators/update.py b/backend/api/v1/modules/a76/items/exports/validators/update.py index e4f6c121..4f8e2c9d 100644 --- a/backend/api/v1/modules/a76/items/exports/validators/update.py +++ b/backend/api/v1/modules/a76/items/exports/validators/update.py @@ -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: diff --git a/backend/api/v1/modules/a76/items/series/models.py b/backend/api/v1/modules/a76/items/series/models.py index 6801b43a..a71397ab 100644 --- a/backend/api/v1/modules/a76/items/series/models.py +++ b/backend/api/v1/modules/a76/items/series/models.py @@ -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 \ No newline at end of file diff --git a/backend/api/v1/modules/a76/reports/movements/invoices/services/query_builders.py b/backend/api/v1/modules/a76/reports/movements/invoices/services/query_builders.py index e5d53263..e00286cb 100644 --- a/backend/api/v1/modules/a76/reports/movements/invoices/services/query_builders.py +++ b/backend/api/v1/modules/a76/reports/movements/invoices/services/query_builders.py @@ -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""" diff --git a/frontend/src/lib/api/dashboard/a76/items.ts b/frontend/src/lib/api/dashboard/a76/items.ts index 1c7f2a75..d13beac4 100644 --- a/frontend/src/lib/api/dashboard/a76/items.ts +++ b/frontend/src/lib/api/dashboard/a76/items.ts @@ -127,7 +127,7 @@ export interface FaLineItem { subitem_number?: number; // Special flags - download?: boolean; + discharge?: boolean; own_equipment?: boolean; omit_annex31?: boolean; diff --git a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-sheet-fa.svelte b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-sheet-fa.svelte index 2ed9af1f..6a0026c2 100644 --- a/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-sheet-fa.svelte +++ b/frontend/src/lib/components/dashboard/invoices/edit/items/fa/item-sheet-fa.svelte @@ -263,10 +263,10 @@
{ 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 @@
{ editingItem.fa_data = editingItem.fa_data || {}; - editingItem.fa_data.download = v === 'si'; + editingItem.fa_data.discharge = v === 'si'; }} class="flex gap-4" >