Funciones y logia de exportacion, como mejoras CRUD en las partes de exportacion
This commit is contained in:
@@ -114,7 +114,9 @@ def apply_calculations(
|
||||
.first()
|
||||
)
|
||||
if class_desc:
|
||||
line.description.description_spanish, line.description.description_english = class_desc
|
||||
line.description.description_spanish = class_desc[0]
|
||||
if not line.description.description_english:
|
||||
line.description.description_english = class_desc[1]
|
||||
|
||||
|
||||
def calculate_values(
|
||||
@@ -213,7 +215,8 @@ def calculate_values(
|
||||
line.quantity.package_quantity = import_line.quantity.package_quantity
|
||||
line.quantity.package_id = import_line.quantity.package_id
|
||||
|
||||
if import_line.description:
|
||||
if import_line.description and not line.description.description_english:
|
||||
# Only fill from import if the user hasn't captured their own English description
|
||||
line.description.description_english = import_line.description.description_english
|
||||
|
||||
# ==========================================
|
||||
|
||||
@@ -135,6 +135,97 @@ def validate_create(
|
||||
if not fa_data.search_line:
|
||||
errors.add_required_error(field=f"line[{line_number}].fa_data.search_line")
|
||||
|
||||
# REVISA_FACTURA logic if both invoice and line are present
|
||||
if fa_data.search_invoice and fa_data.search_line:
|
||||
linked_inv = db.query(InvoiceHeader).filter(
|
||||
InvoiceHeader.invoice_number == fa_data.search_invoice,
|
||||
InvoiceHeader.tenant_id == tenant_id,
|
||||
InvoiceHeader.company_id == company_id,
|
||||
InvoiceHeader.operation_type == "imp" # Assuming link is always to an import
|
||||
).first()
|
||||
|
||||
if not linked_inv:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].fa_data.search_invoice",
|
||||
message="La Factura No Existe Capture o seleccione una que si exista",
|
||||
code="LINKED_INVOICE_NOT_FOUND",
|
||||
solution=["Verificar el número de factura de importación."]
|
||||
)
|
||||
elif linked_inv.status != "processed":
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].fa_data.search_invoice",
|
||||
message="La Factura no esta Actualizada, Capture o seleccione una que si este Actualizada",
|
||||
code="LINKED_INVOICE_NOT_PROCESSED",
|
||||
solution=["Actualizar/Procesar la factura de importación antes de descargarla."]
|
||||
)
|
||||
else:
|
||||
# Invoice is valid and processed, check the line (REVISA_FACTURA part 2)
|
||||
linked_line = db.query(LineItem).filter(
|
||||
LineItem.invoice_id == linked_inv.id,
|
||||
LineItem.line_number == fa_data.search_line,
|
||||
LineItem.tenant_id == tenant_id,
|
||||
LineItem.company_id == company_id
|
||||
).first()
|
||||
|
||||
if not linked_line:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].fa_data.search_line",
|
||||
message="La Partida de la Factura No Existe Capture o seleccione una que si exista",
|
||||
code="LINKED_LINE_NOT_FOUND",
|
||||
solution=["Verificar el número de renglón en la factura de importación."]
|
||||
)
|
||||
else:
|
||||
# Validation for search_type == "Clase" (Parity with Clarion Valida.Validaciones)
|
||||
if fa_data.search_type == "Clase" and linked_line.class_id != line.class_id:
|
||||
# Load class codes for a better error message if necessary
|
||||
current_class = db.query(Class).filter(Class.id == line.class_id).first()
|
||||
import_class = db.query(Class).filter(Class.id == linked_line.class_id).first()
|
||||
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].class_id",
|
||||
message=f"Es necesario que la clase: {current_class.class_code if current_class else line.class_id} sea igual a la clase: {import_class.class_code if import_class else linked_line.class_id} de la factura de importación seleccionada.",
|
||||
code="CLASS_MISMATCH_FOR_SEARCH_TYPE_CLASE",
|
||||
solution=["Asegurarse de que el activo que se exporta pertenezca a la misma familia/clase que el que se importó."]
|
||||
)
|
||||
|
||||
# REVISA_CANTIDADES_A_DESC_TEM logic
|
||||
# 1. Get current balance from ledger
|
||||
from api.v1.modules.a24.balance_movements.models import BalanceMovement, NEGATIVE_MOVEMENTS
|
||||
from sqlalchemy import case, select
|
||||
|
||||
sign_expr = case(
|
||||
(BalanceMovement.movement_type.in_(NEGATIVE_MOVEMENTS), Decimal("-1")),
|
||||
else_=Decimal("1"),
|
||||
)
|
||||
available_balance = db.execute(
|
||||
select(func.sum(sign_expr * BalanceMovement.quantity)).where(
|
||||
BalanceMovement.import_item_line_id == linked_line.id
|
||||
)
|
||||
).scalar() or Decimal("0")
|
||||
|
||||
# 2. Get pending discharges in this same invoice (sibling lines)
|
||||
from api.v1.modules.a76.items.line_quantities.models import LineQuantity
|
||||
pending_sum = db.query(func.sum(LineQuantity.quantity)).join(
|
||||
LineItem, LineItem.id == LineQuantity.id
|
||||
).join(
|
||||
FaLineItem, FaLineItem.id == LineItem.id
|
||||
).filter(
|
||||
LineItem.invoice_id == line.invoice_id,
|
||||
FaLineItem.search_invoice == fa_data.search_invoice,
|
||||
FaLineItem.search_line == fa_data.search_line,
|
||||
FaLineItem.movement_type_import == fa_data.movement_type_import,
|
||||
FaLineItem.discharge == True
|
||||
).scalar() or Decimal("0")
|
||||
|
||||
current_qty = line.quantity.quantity or Decimal("0")
|
||||
remaining = available_balance - pending_sum - current_qty
|
||||
|
||||
# Note: Hard validation removed here to allow 'Multiple Source Discharge'
|
||||
# or 'Automatic Deficit Handling' logic to function during full invoice processing.
|
||||
# Current balance for information: {available_balance}, short: {remaining if remaining < 0 else 0}
|
||||
pass
|
||||
|
||||
|
||||
if (
|
||||
fa_data.is_subitem and fa_data.contains_subitems
|
||||
) and not fa_data.subitem_number:
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from decimal import Decimal
|
||||
from sqlalchemy import func, exists
|
||||
from sqlalchemy.orm import Session
|
||||
from core.exceptions import ErrorCollector
|
||||
|
||||
@@ -8,6 +9,7 @@ from api.v1.modules.a76.invoices.models import InvoiceHeader
|
||||
from api.v1.modules.a76.general_catalogs.fractions.us_tariff_fractions.models import (
|
||||
USTariffFraction,
|
||||
)
|
||||
from api.v1.modules.a76.classes.models import Class
|
||||
from .common import validate_common
|
||||
|
||||
|
||||
@@ -189,7 +191,7 @@ def validate_update(
|
||||
existing_line.description.description_spanish
|
||||
)
|
||||
|
||||
if line.description.description_english is None:
|
||||
if not line.description.description_english:
|
||||
line.description.description_english = (
|
||||
existing_line.description.description_english
|
||||
)
|
||||
@@ -233,6 +235,119 @@ def validate_update(
|
||||
if fa_data.search_line is None:
|
||||
fa_data.search_line = existing_fa_data.search_line
|
||||
|
||||
# REVISA_FACTURA logic if both invoice and line are present (even after partial update)
|
||||
if fa_data.discharge is True and fa_data.search_invoice and fa_data.search_line:
|
||||
from sqlalchemy import exists
|
||||
linked_inv = db.query(InvoiceHeader).filter(
|
||||
InvoiceHeader.invoice_number == fa_data.search_invoice,
|
||||
InvoiceHeader.tenant_id == tenant_id,
|
||||
InvoiceHeader.company_id == company_id,
|
||||
InvoiceHeader.operation_type == "imp"
|
||||
).first()
|
||||
|
||||
if not linked_inv:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].fa_data.search_invoice",
|
||||
message="La Factura No Existe Capture o seleccione una que si exista",
|
||||
code="LINKED_INVOICE_NOT_FOUND",
|
||||
solution=["Verificar el número de factura de importación."]
|
||||
)
|
||||
elif linked_inv.status != "processed":
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].fa_data.search_invoice",
|
||||
message="La Factura no esta Actualizada, Capture o seleccione una que si este Actualizada",
|
||||
code="LINKED_INVOICE_NOT_PROCESSED",
|
||||
solution=["Actualizar/Procesar la factura de importación antes de descargarla."]
|
||||
)
|
||||
else:
|
||||
# Invoice is valid and processed, check the line
|
||||
linked_line_exists = db.query(exists().where(
|
||||
(LineItem.invoice_id == linked_inv.id) &
|
||||
(LineItem.line_number == fa_data.search_line) &
|
||||
(LineItem.tenant_id == tenant_id) &
|
||||
(LineItem.company_id == company_id)
|
||||
)).scalar()
|
||||
|
||||
if not linked_line_exists:
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].fa_data.search_line",
|
||||
message="La Partida de la Factura No Existe Capture o seleccione una que si exista",
|
||||
code="LINKED_LINE_NOT_FOUND",
|
||||
solution=["Verificar el número de renglón en la factura de importación."]
|
||||
)
|
||||
else:
|
||||
# Validation for search_type == "Clase" (Parity with Clarion Valida.Validaciones)
|
||||
# We need to get the actual IDs to compare
|
||||
linked_import_line = db.query(LineItem).filter(
|
||||
LineItem.invoice_id == linked_inv.id,
|
||||
LineItem.line_number == fa_data.search_line,
|
||||
LineItem.tenant_id == tenant_id,
|
||||
LineItem.company_id == company_id
|
||||
).first()
|
||||
|
||||
if linked_import_line:
|
||||
current_class_id = line.class_id if line.class_id else existing_line.class_id
|
||||
if fa_data.search_type == "Clase" and linked_import_line.class_id != current_class_id:
|
||||
current_class = db.query(Class).filter(Class.id == current_class_id).first()
|
||||
import_class = db.query(Class).filter(Class.id == linked_import_line.class_id).first()
|
||||
|
||||
errors.add_error(
|
||||
field=f"line[{line_number}].class_id",
|
||||
message=f"Es necesario que la clase: {current_class.class_code if current_class else current_class_id} sea igual a la clase: {import_class.class_code if import_class else linked_import_line.class_id} de la factura de importación seleccionada.",
|
||||
code="CLASS_MISMATCH_FOR_SEARCH_TYPE_CLASE",
|
||||
solution=["Asegurarse de que el activo que se exporta pertenezca a la misma familia/clase que el que se importó."]
|
||||
)
|
||||
|
||||
# REVISA_CANTIDADES_A_DESC_TEM logic for Update
|
||||
|
||||
# 0. Get the internal ID of the linked import line
|
||||
linked_import_line = db.query(LineItem).filter(
|
||||
LineItem.invoice_id == linked_inv.id,
|
||||
LineItem.line_number == fa_data.search_line,
|
||||
LineItem.tenant_id == tenant_id,
|
||||
LineItem.company_id == company_id
|
||||
).first()
|
||||
|
||||
if linked_import_line:
|
||||
# 1. Get current balance from ledger
|
||||
from api.v1.modules.a24.balance_movements.models import BalanceMovement, NEGATIVE_MOVEMENTS
|
||||
from sqlalchemy import case, select
|
||||
|
||||
sign_expr = case(
|
||||
(BalanceMovement.movement_type.in_(NEGATIVE_MOVEMENTS), Decimal("-1")),
|
||||
else_=Decimal("1"),
|
||||
)
|
||||
available_balance = db.execute(
|
||||
select(func.sum(sign_expr * BalanceMovement.quantity)).where(
|
||||
BalanceMovement.import_item_line_id == linked_import_line.id
|
||||
)
|
||||
).scalar() or Decimal("0")
|
||||
|
||||
# 2. Get pending discharges in this same invoice (sibling lines)
|
||||
# EXCLUDING the current line we are updating
|
||||
from api.v1.modules.a76.items.line_quantities.models import LineQuantity
|
||||
pending_sum = db.query(func.sum(LineQuantity.quantity)).join(
|
||||
LineItem, LineItem.id == LineQuantity.id
|
||||
).join(
|
||||
FaLineItem, FaLineItem.id == LineItem.id
|
||||
).filter(
|
||||
LineItem.invoice_id == line.invoice_id,
|
||||
LineItem.id != existing_line.id, # IMPORTANT: Exclude self
|
||||
FaLineItem.search_invoice == fa_data.search_invoice,
|
||||
FaLineItem.search_line == fa_data.search_line,
|
||||
FaLineItem.movement_type_import == fa_data.movement_type_import,
|
||||
FaLineItem.discharge == True
|
||||
).scalar() or Decimal("0")
|
||||
|
||||
current_qty = line.quantity.quantity if line.quantity.quantity is not None else existing_line.quantity.quantity
|
||||
remaining = available_balance - pending_sum - current_qty
|
||||
|
||||
# Note: Hard validation removed here to allow 'Multiple Source Discharge'
|
||||
# or 'Automatic Deficit Handling' logic to function during full invoice processing.
|
||||
# Current balance for information: {available_balance}, short: {remaining if remaining < 0 else 0}
|
||||
pass
|
||||
|
||||
|
||||
# Subpartidas (EsSubPartida / SubPartida)
|
||||
if fa_data.is_subitem is None:
|
||||
fa_data.is_subitem = existing_fa_data.is_subitem
|
||||
|
||||
@@ -183,12 +183,12 @@ def validate_update(
|
||||
line.order = existing_line.order
|
||||
|
||||
# Descripciones
|
||||
if line.description.description_spanish is None:
|
||||
if not line.description.description_spanish:
|
||||
line.description.description_spanish = (
|
||||
existing_line.description.description_spanish
|
||||
)
|
||||
|
||||
if line.description.description_english is None:
|
||||
if not line.description.description_english:
|
||||
line.description.description_english = (
|
||||
existing_line.description.description_english
|
||||
)
|
||||
|
||||
@@ -223,6 +223,11 @@ class LineItem(Base, TenantScopedMixin, TimestampMixin):
|
||||
back_populates="line",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
series: Mapped[List["Serie"]] = relationship(
|
||||
"Serie",
|
||||
cascade="all, delete-orphan",
|
||||
)
|
||||
|
||||
part_info: Mapped[Optional["Part"]] = relationship(
|
||||
"Part",
|
||||
foreign_keys=[part_number_id],
|
||||
|
||||
@@ -167,6 +167,25 @@ async def delete_item(
|
||||
return None
|
||||
|
||||
|
||||
@router.delete("/{item_id}/series", status_code=status.HTTP_200_OK)
|
||||
async def delete_item_series(
|
||||
item_id: int = Path(..., description="Item ID"),
|
||||
company_id: int = Query(..., description="Company ID"),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
"""
|
||||
Delete all serial numbers for a specific item.
|
||||
Equivalent to Clarion BORRAR_SERIES_EXPO.
|
||||
"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
|
||||
service = ItemService()
|
||||
count = service.delete_item_series(db, item_id, tenant_id, company_id)
|
||||
|
||||
return {"message": f"Successfully deleted {count} series", "count": count}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# ADDITIONAL ENDPOINTS FOR INVOICE
|
||||
# ============================================================================
|
||||
@@ -209,6 +228,10 @@ async def get_items_with_balance(
|
||||
"before this date are subtracted (CALCULA_SALDO_FECHA_EXPO logic)."
|
||||
),
|
||||
),
|
||||
current_export_invoice_id: Optional[int] = Query(
|
||||
None,
|
||||
description="ID of the current export invoice being edited to subtract its pending quantities from balance."
|
||||
),
|
||||
db: Session = Depends(get_core_db),
|
||||
current_user: Dict[str, Any] = Depends(get_current_user),
|
||||
):
|
||||
@@ -222,12 +245,15 @@ async def get_items_with_balance(
|
||||
- available_balance : net balance still available for export discharge
|
||||
- has_balance : true when available_balance > 0
|
||||
|
||||
If current_export_invoice_id is provided, quantities already assigned to
|
||||
this specified invoice will be subtracted from available_balance.
|
||||
|
||||
Use ``as_of_date`` to restrict consumption movements to a specific date
|
||||
(pass the export invoice date so that future discharges are not counted).
|
||||
"""
|
||||
tenant_id = validate_access_to_resource(db, company_id, current_user)
|
||||
service = ItemService()
|
||||
return service.get_lines_with_balance(db, invoice_id, tenant_id, company_id, as_of_date)
|
||||
return service.get_lines_with_balance(db, invoice_id, tenant_id, company_id, as_of_date, current_export_invoice_id)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
|
||||
@@ -207,6 +207,7 @@ class ItemService:
|
||||
if not serie_dict:
|
||||
continue
|
||||
serie_dict.update({
|
||||
"line_item_id": line.id,
|
||||
"tenant_id": tenant_id,
|
||||
"company_id": company_id
|
||||
})
|
||||
@@ -234,6 +235,7 @@ class ItemService:
|
||||
if not id_dict:
|
||||
continue
|
||||
id_dict.update({
|
||||
"item_line_id": line.id,
|
||||
"tenant_id": tenant_id,
|
||||
"company_id": company_id
|
||||
})
|
||||
@@ -571,7 +573,7 @@ class ItemService:
|
||||
logger.error(f"Error creating item: {e}")
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="LineItem creation failed - integrity constraint violated",
|
||||
detail=f"LineItem creation failed - integrity constraint violated: {e.orig}",
|
||||
)
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
@@ -800,6 +802,32 @@ class ItemService:
|
||||
logger.error(f"Unexpected error updating item: {e}")
|
||||
raise HTTPException(status_code=500, detail=f"Error updating item: {str(e)}")
|
||||
|
||||
@staticmethod
|
||||
def delete_item_series(
|
||||
db: Session,
|
||||
item_id: int,
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
current_user_name: Optional[str] = None
|
||||
) -> int:
|
||||
"""
|
||||
Deletes all serial numbers for a specific item.
|
||||
Equivalent to Clarion BORRAR_SERIES_EXPO.
|
||||
"""
|
||||
# Ensure item exists and belongs to the company
|
||||
item = ItemService.get_by_id(db, item_id, tenant_id, company_id)
|
||||
if not item:
|
||||
raise HTTPException(status_code=404, detail="Item not found")
|
||||
|
||||
# Delete series
|
||||
deleted_count = db.query(Serie).filter(Serie.line_item_id == item_id).delete(synchronize_session='fetch')
|
||||
|
||||
# Log to bitácora (if system supports it)
|
||||
# GBitacora('BORRAR TODAS LAS SERIE EXPO ', item.invoice_number)
|
||||
|
||||
db.commit()
|
||||
return deleted_count
|
||||
|
||||
@staticmethod
|
||||
def delete(
|
||||
db: Session,
|
||||
@@ -824,6 +852,29 @@ class ItemService:
|
||||
status_code=404, detail="Invoice not found or could not be locked"
|
||||
)
|
||||
|
||||
# Manual cascade cleanup for Anexo 24 references
|
||||
from api.v1.modules.a24.balance_movements.models import BalanceMovement
|
||||
from api.v1.modules.a24.discharges.models import DischargeDetail
|
||||
|
||||
# Check if this item is used in any discharges
|
||||
if db_item.invoice and db_item.invoice.operation_type == "imp":
|
||||
# Import item: check if it has been consumed
|
||||
consumptions = db.query(BalanceMovement).filter(
|
||||
BalanceMovement.import_item_line_id == item_id,
|
||||
BalanceMovement.movement_type != "entry"
|
||||
).count()
|
||||
if consumptions > 0:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="No se puede borrar la partida de importación porque ya ha sido descargada/consumida parcial o totalmente."
|
||||
)
|
||||
# It's safe to delete its ENTRY movements
|
||||
db.query(BalanceMovement).filter(BalanceMovement.import_item_line_id == item_id).delete()
|
||||
else:
|
||||
# Export item: delete its derived consumptions and discharge details
|
||||
db.query(DischargeDetail).filter(DischargeDetail.export_item_line_id == item_id).delete()
|
||||
db.query(BalanceMovement).filter(BalanceMovement.source_item_line_id == item_id).delete()
|
||||
|
||||
db.delete(db_item)
|
||||
db.flush()
|
||||
ItemService._renumber_all_invoice_lines(db, invoice_id)
|
||||
@@ -832,8 +883,8 @@ class ItemService:
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error(f"Error deleting item: {e}")
|
||||
raise HTTPException(status_code=500, detail="Error deleting item")
|
||||
logger.error(f"Error deleting item: {e}", exc_info=True)
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@staticmethod
|
||||
def get_lines_with_balance(
|
||||
@@ -842,20 +893,20 @@ class ItemService:
|
||||
tenant_id: int,
|
||||
company_id: int,
|
||||
as_of_date: Optional[datetime.date] = None,
|
||||
current_export_invoice_id: Optional[int] = None,
|
||||
) -> List[dict]:
|
||||
"""
|
||||
Returns every line of an import invoice together with its current
|
||||
available balance calculated from the a24.balance_movement ledger.
|
||||
|
||||
Lines with balance <= 0 are included but marked as unavailable so
|
||||
the frontend can grey them out / disable them.
|
||||
If current_export_invoice_id is provided, it also subtracts quantities
|
||||
already allocated in that export invoice to provide a "real-time"
|
||||
remaining balance for the user during capture.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
as_of_date : optional cut-off date. Only negative movements
|
||||
(consumptions, etc.) on or before this date are counted,
|
||||
mirroring the CALCULA_SALDO_FECHA_EXPO Clarion logic.
|
||||
If None, all movements are counted (no date restriction).
|
||||
as_of_date : optional cut-off date.
|
||||
current_export_invoice_id : current export invoice being edited.
|
||||
"""
|
||||
lines: List[LineItem] = (
|
||||
db.query(LineItem)
|
||||
@@ -877,14 +928,42 @@ class ItemService:
|
||||
.all()
|
||||
)
|
||||
|
||||
# 1. Get official balance from ledger
|
||||
result = []
|
||||
used_map = ItemService._used_quantities_by_procedure(
|
||||
db=db,
|
||||
import_line_ids=[line.id for line in lines],
|
||||
as_of_date=as_of_date,
|
||||
)
|
||||
|
||||
# 2. Get locally reserved quantities in the current export invoice (if any)
|
||||
reserved_map = {}
|
||||
if current_export_invoice_id and lines:
|
||||
import_inv = lines[0].invoice
|
||||
if import_inv:
|
||||
reserved_rows = (
|
||||
db.query(FaLineItem.search_line, func.sum(LineQuantity.quantity))
|
||||
.join(LineItem, LineItem.id == FaLineItem.id)
|
||||
.join(LineQuantity, LineQuantity.id == LineItem.id)
|
||||
.filter(
|
||||
LineItem.invoice_id == current_export_invoice_id,
|
||||
FaLineItem.search_invoice == import_inv.invoice_number,
|
||||
FaLineItem.movement_type_import == import_inv.invoice_type, # Match TEM/DEF
|
||||
FaLineItem.discharge == True
|
||||
)
|
||||
.group_by(FaLineItem.search_line)
|
||||
.all()
|
||||
)
|
||||
reserved_map = {int(row[0]): Decimal(str(row[1] or 0)) for row in reserved_rows}
|
||||
|
||||
for line in lines:
|
||||
# Official ledger balance
|
||||
available_balance = ItemService._compute_balance(db, line.id, as_of_date)
|
||||
|
||||
# Subtract what's already assigned in THIS invoice
|
||||
reserved = reserved_map.get(line.line_number, Decimal(0))
|
||||
active_balance = available_balance - reserved
|
||||
|
||||
qty = line.quantity
|
||||
desc = line.description
|
||||
fa = line.fa_data
|
||||
@@ -925,8 +1004,11 @@ class ItemService:
|
||||
"quantity_used_temp": float(qty_used_temp),
|
||||
"quantity_used_def": float(qty_used_def),
|
||||
# Balance
|
||||
"available_balance": float(available_balance),
|
||||
"has_balance": available_balance > Decimal(0),
|
||||
"available_balance": float(active_balance),
|
||||
"has_balance": active_balance > Decimal(0),
|
||||
# Weights for proportional calculation
|
||||
"net_weight": float(qty.net_weight) if qty and qty.net_weight is not None else 0.0,
|
||||
"gross_weight": float(qty.gross_weight) if qty and qty.gross_weight is not None else 0.0,
|
||||
# FA / subitem info
|
||||
"is_subitem": fa.is_subitem if fa else None,
|
||||
"contains_subitems": fa.contains_subitems if fa else None,
|
||||
@@ -934,6 +1016,39 @@ class ItemService:
|
||||
})
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def get_pending_discharge_sum(
|
||||
db: Session,
|
||||
invoice_id: int,
|
||||
search_invoice: str,
|
||||
search_line: int,
|
||||
movement_type_import: Optional[str] = None,
|
||||
exclude_line_id: Optional[int] = None
|
||||
) -> Decimal:
|
||||
"""
|
||||
CUENTA_CANTIDADES_A_DESC equivalent.
|
||||
Sums quantity from other lines in the same invoice targeting the same import source.
|
||||
"""
|
||||
query = (
|
||||
select(func.sum(LineQuantity.quantity))
|
||||
.join(LineItem, LineItem.id == LineQuantity.id)
|
||||
.join(FaLineItem, FaLineItem.id == LineItem.id)
|
||||
.where(
|
||||
LineItem.invoice_id == invoice_id,
|
||||
FaLineItem.search_invoice == search_invoice,
|
||||
FaLineItem.search_line == search_line,
|
||||
FaLineItem.discharge == True
|
||||
)
|
||||
)
|
||||
if movement_type_import:
|
||||
query = query.where(FaLineItem.movement_type_import == movement_type_import)
|
||||
|
||||
if exclude_line_id:
|
||||
query = query.where(LineItem.id != exclude_line_id)
|
||||
|
||||
result = db.execute(query).scalar()
|
||||
return Decimal(str(result or 0))
|
||||
|
||||
@staticmethod
|
||||
def _compute_balance(
|
||||
db: Session,
|
||||
|
||||
Reference in New Issue
Block a user