feat: Enhance invoice validation and calculations
- Added new validations for pedimento regimes and compliance checks in common_validators.py. - Implemented missing date checks for consolidated pedimentos. - Introduced validation for shipped_by_id and manifest_number in invoice compliance. - Refactored create_validators.py by removing the file as it was no longer needed. - Updated update validators for exports and imports to use invoice_data instead of invoice. - Modified schemas.py to set default values for financial fields to avoid None values. - Created calculations.py to handle invoice calculations, including total increments and regime changes. - Improved layout responsiveness in various Svelte components for better user experience.
This commit is contained in:
37
backend/api/v1/modules/a76/invoices/common/calculations.py
Normal file
37
backend/api/v1/modules/a76/invoices/common/calculations.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from sqlalchemy.orm import Session
|
||||
from api.v1.modules.a76.invoices.models import InvoiceFinancials, InvoiceHeader, InvoiceLogistics
|
||||
from core.exceptions import ErrorCollector
|
||||
|
||||
from .. import schemas
|
||||
|
||||
|
||||
|
||||
def apply_calculations(
|
||||
invoice: schemas.InvoiceHeaderUpdate,
|
||||
):
|
||||
if invoice.invoice_type == "CR":
|
||||
invoice.compliance_mx.is_regime_change = True
|
||||
else:
|
||||
invoice.compliance_mx.is_regime_change = False
|
||||
|
||||
increments_me = (invoice.financials.freight or 0) + (invoice.financials.insurance or 0) + (invoice.financials.packaging or 0) + (invoice.financials.other_increments or 0)
|
||||
if invoice.financials.currency == "foreign":
|
||||
invoice.financials.total_increments_me = increments_me
|
||||
invoice.financials.total_increments_mn = invoice.financials.total_increments_me * invoice.financials.exchange_rate
|
||||
invoice.financials.currency_type = "USD"
|
||||
elif invoice.financials.currency == "local":
|
||||
invoice.financials.total_increments_mn = increments_me
|
||||
invoice.financials.total_increments_me = invoice.financials.total_increments_mn / invoice.financials.exchange_rate
|
||||
invoice.financials.currency_type = "MXN"
|
||||
elif invoice.financials.currency == "manual":
|
||||
invoice.financials.total_increments_me = (increments_me)/invoice.financials.exchange_rate
|
||||
invoice.financials.total_increments_mn = invoice.financials.total_increments_me * invoice.financials.exchange_rate
|
||||
|
||||
|
||||
invoice.compliance_mx.is_pedimento_pending = False
|
||||
if not invoice.compliance_mx.pedimento_id:
|
||||
invoice.compliance_mx.is_pedimento_pending = True
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,11 +6,14 @@ from .. import models
|
||||
from .. import schemas
|
||||
from ..models import InvoiceComplianceMx
|
||||
from ..models import TransportType, Currency, WeightUnit
|
||||
from api.v1.modules.a76.invoices.common.calculations import apply_calculations
|
||||
from api.v1.modules.a76.items.models import LineItem
|
||||
from api.v1.modules.a76.pedmientos.models.pedimentos import Pedimentos
|
||||
from api.v1.modules.a76.general_catalogs.exchange_rate.models import ExchangeRate
|
||||
from api.v1.modules.a76.clients_and_providers.models import ClientProvider
|
||||
from api.v1.modules.a76.customs_brokers.models import CustomsBroker
|
||||
from api.v1.modules.a76.transportation.transporters.models import Transporter
|
||||
from api.v1.modules.a76.manifests.manifest.models import Manifest
|
||||
from api.v1.modules.public.reference_data.incoterms.models import Incoterm
|
||||
from api.v1.modules.public.reference_data.currency_types.models import CurrencyType
|
||||
from api.v1.modules.public.reference_data.customs_sections.models import CustomsSection
|
||||
@@ -208,13 +211,20 @@ def validate_common(
|
||||
value=pedimento.operation_type,
|
||||
)
|
||||
else:
|
||||
# Validar regímenes incompatibles
|
||||
only_regimes = ["EXD", "ETE", "ETR"]
|
||||
# Validar regímenes incompatibles
|
||||
only_regimes = ["EXD", "ETE", "ETR"]
|
||||
is_valid = True
|
||||
if invoice.operation_type == "imp" and pedimento.regime in only_regimes:
|
||||
is_valid = False
|
||||
oposite_operacion = "Exportación"
|
||||
elif invoice.operation_type == "exp" and pedimento.regime not in only_regimes:
|
||||
is_valid = False
|
||||
oposite_operacion = "Importación"
|
||||
|
||||
if pedimento.regime in only_regimes:
|
||||
if not is_valid:
|
||||
errors.add_error(
|
||||
field="compliance_mx.pedimento_id",
|
||||
message=f"El Pedimento seleccionado corresponde a una Exportación, no a una {operacion}.",
|
||||
message=f"El Pedimento seleccionado no corresponde a una {oposite_operacion}, no a una {operacion}.",
|
||||
solution=[f"Selecciona un Pedimento de {operacion}"],
|
||||
code="INVALID_REGIME",
|
||||
value=pedimento.regime,
|
||||
@@ -272,24 +282,33 @@ def validate_common(
|
||||
)
|
||||
|
||||
if pedimento.pedimento_type == "consolidated":
|
||||
# Convertir invoice_date a date si es datetime para poder comparar
|
||||
invoice_date = (
|
||||
invoice.invoice_date.date()
|
||||
if hasattr(invoice.invoice_date, "date")
|
||||
else invoice.invoice_date
|
||||
)
|
||||
entry_date = (
|
||||
pedimento.pedimento_dates.entry_date.date()
|
||||
if hasattr(pedimento.pedimento_dates.entry_date, "date")
|
||||
else pedimento.pedimento_dates.entry_date
|
||||
)
|
||||
end_date = (
|
||||
pedimento.pedimento_dates.end_date.date()
|
||||
if hasattr(pedimento.pedimento_dates.end_date, "date")
|
||||
else pedimento.pedimento_dates.end_date
|
||||
)
|
||||
if not pedimento.pedimento_dates:
|
||||
errors.add_error(
|
||||
field="compliance_mx.pedimento_id",
|
||||
message="El Pedimento seleccionado no tiene fechas registradas.",
|
||||
solution=["Verifica las fechas del Pedimento en el catálogo"],
|
||||
code="MISSING_PEDIMENTO_DATES",
|
||||
value=invoice.compliance_mx.pedimento_id,
|
||||
)
|
||||
else:
|
||||
# Convertir invoice_date a date si es datetime para poder comparar
|
||||
invoice_date = (
|
||||
invoice.invoice_date.date()
|
||||
if hasattr(invoice.invoice_date, "date")
|
||||
else invoice.invoice_date
|
||||
)
|
||||
entry_date = (
|
||||
pedimento.pedimento_dates.entry_date.date()
|
||||
if hasattr(pedimento.pedimento_dates.entry_date, "date")
|
||||
else pedimento.pedimento_dates.entry_date
|
||||
)
|
||||
end_date = (
|
||||
pedimento.pedimento_dates.end_date.date()
|
||||
if hasattr(pedimento.pedimento_dates.end_date, "date")
|
||||
else pedimento.pedimento_dates.end_date
|
||||
)
|
||||
|
||||
if invoice_date < entry_date or invoice_date > end_date:
|
||||
if pedimento.pedimento_dates and (invoice_date < entry_date or invoice_date > end_date):
|
||||
errors.add_error(
|
||||
field="invoice_date",
|
||||
message=f"La Fecha de la Factura {invoice.invoice_date} no está dentro del rango de fechas del Pedimento {pedimento.customs_office}-{pedimento.license}-{pedimento.pedimento_number}.",
|
||||
@@ -444,6 +463,25 @@ def validate_common(
|
||||
code="NOT_FOUND",
|
||||
value=invoice.compliance_mx.shipped_to_id,
|
||||
)
|
||||
|
||||
if invoice.compliance_mx.shipped_by_id:
|
||||
shipped_by_exists = (
|
||||
db.query(ClientProvider)
|
||||
.filter(
|
||||
ClientProvider.id == invoice.compliance_mx.shipped_by_id,
|
||||
ClientProvider.tenant_id == tenant_id,
|
||||
ClientProvider.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not shipped_by_exists:
|
||||
errors.add_error(
|
||||
field="compliance_mx.shipped_by_id",
|
||||
message="El Remitente no existe en el Catálogo de Clientes y Proveedores.",
|
||||
solution=["Verifica el ID del Remitente", "Revisa el catálogo"],
|
||||
code="NOT_FOUND",
|
||||
value=invoice.compliance_mx.shipped_by_id,
|
||||
)
|
||||
|
||||
# Validar agente aduanal solo si se proporciona
|
||||
if invoice.compliance_mx.customs_broker_id:
|
||||
@@ -463,12 +501,27 @@ def validate_common(
|
||||
solution=["Verifica el ID del Agente Aduanal", "Revisa el catálogo"],
|
||||
code="NOT_FOUND",
|
||||
value=invoice.compliance_mx.customs_broker_id,
|
||||
)
|
||||
)
|
||||
|
||||
if invoice.logistics:
|
||||
if invoice.logistics.transport_num and not invoice.logistics.transport_num:
|
||||
# logic ...
|
||||
pass
|
||||
if invoice.logistics.carrier_id:
|
||||
carrier_exists = (
|
||||
db.query(Transporter)
|
||||
.filter(
|
||||
Transporter.id == invoice.logistics.carrier_id,
|
||||
Transporter.tenant_id == tenant_id,
|
||||
Transporter.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not carrier_exists:
|
||||
errors.add_error(
|
||||
field="logistics.carrier_id",
|
||||
message="El Transportista no existe en el Catálogo de Clientes y Proveedores.",
|
||||
solution=["Verifica el ID del Transportista", "Revisa el catálogo"],
|
||||
code="NOT_FOUND",
|
||||
value=invoice.logistics.carrier_id,
|
||||
)
|
||||
|
||||
if invoice.logistics.transport_type not in [t.value for t in TransportType]:
|
||||
errors.add_error(
|
||||
@@ -614,4 +667,25 @@ def validate_common(
|
||||
solution=["Verifica el código de la Aduana", "Revisa el catálogo"],
|
||||
code="NOT_FOUND",
|
||||
value=invoice.compliance_mx.aduana,
|
||||
)
|
||||
)
|
||||
|
||||
if invoice.compliance_mx.manifest_number:
|
||||
manifest_exists = (
|
||||
db.query(Manifest)
|
||||
.filter(
|
||||
Manifest.manifest_number == invoice.compliance_mx.manifest_number,
|
||||
Manifest.tenant_id == tenant_id,
|
||||
Manifest.company_id == company_id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if not manifest_exists:
|
||||
errors.add_error(
|
||||
field="compliance_mx.manifest_number",
|
||||
message="El Número de Manifiesto no existe en el sistema.",
|
||||
solution=["Verifica el Número de Manifiesto", "Revisa el catálogo"],
|
||||
code="NOT_FOUND",
|
||||
value=invoice.compliance_mx.manifest_number,
|
||||
)
|
||||
|
||||
apply_calculations(invoice)
|
||||
@@ -49,7 +49,7 @@ def validate_update(
|
||||
}
|
||||
|
||||
validate_required_fields_by_operation(
|
||||
invoice=invoice_dict,
|
||||
invoice_data=invoice_dict,
|
||||
operation_type=invoice.operation_type or (existing_invoice.operation_type or 'imp'),
|
||||
errors=errors
|
||||
)
|
||||
@@ -211,14 +211,21 @@ def validate_update(
|
||||
elif hasattr(invoice.logistics, 'weight_type'):
|
||||
invoice.logistics.weight_type = clean_str(invoice.logistics.weight_type).upper()
|
||||
|
||||
# Columna Z: E-Document (Opcional)
|
||||
# Columna Z: Número de Manifiesto (Opcional)
|
||||
if invoice.compliance_mx.manifest_number:
|
||||
if not invoice.compliance_mx.manifest_number:
|
||||
invoice.compliance_mx.manifest_number = existing_invoice.compliance_mx.manifest_number if existing_invoice.compliance_mx else None
|
||||
else:
|
||||
invoice.compliance_mx.manifest_number = clean_str(invoice.compliance_mx.manifest_number)
|
||||
|
||||
# Columna AA: E-Document (Opcional)
|
||||
if invoice.compliance_mx:
|
||||
if not invoice.compliance_mx.edocument:
|
||||
invoice.compliance_mx.edocument = existing_invoice.compliance_mx.edocument if existing_invoice.compliance_mx else None
|
||||
else:
|
||||
invoice.compliance_mx.edocument = clean_str(invoice.compliance_mx.edocument)
|
||||
|
||||
# Columna AA: Num. Operación (Opcional)
|
||||
# Columna AB: Num. Operación (Opcional)
|
||||
if invoice.compliance_mx:
|
||||
if not invoice.compliance_mx.vucem_operation_num:
|
||||
invoice.compliance_mx.vucem_operation_num = existing_invoice.compliance_mx.vucem_operation_num if existing_invoice.compliance_mx else None
|
||||
@@ -232,11 +239,17 @@ def validate_update(
|
||||
else:
|
||||
invoice.compliance_mx.aduana = clean_str(invoice.compliance_mx.aduana)
|
||||
|
||||
# Validar que aduana sea obligatorio (excepto para MEX)
|
||||
if existing_invoice.invoice_type != "MEX":
|
||||
current_aduana = invoice.compliance_mx.aduana if invoice.compliance_mx else (existing_invoice.compliance_mx.aduana if existing_invoice.compliance_mx else None)
|
||||
if not current_aduana:
|
||||
errors.add_required_error("aduana")
|
||||
# Columna AC: Enviado Por (Obligatorio)
|
||||
if invoice.compliance_mx:
|
||||
if not invoice.compliance_mx.shipped_by_id:
|
||||
invoice.compliance_mx.shipped_by_id = existing_invoice.compliance_mx.shipped_by_id if existing_invoice.compliance_mx else None
|
||||
else:
|
||||
invoice.compliance_mx.shipped_by_id = clean_str(invoice.compliance_mx.shipped_by_id)
|
||||
|
||||
# Columna AD: Aduana_Cruce (Obligatorio)
|
||||
current_aduana = invoice.compliance_mx.aduana if invoice.compliance_mx else (existing_invoice.compliance_mx.aduana if existing_invoice.compliance_mx else None)
|
||||
if not current_aduana:
|
||||
errors.add_required_error("aduana")
|
||||
|
||||
# Columna AC: Sección de Despacho / Puerto de Entrada (Opcional)
|
||||
if invoice.compliance_mx:
|
||||
@@ -245,15 +258,27 @@ def validate_update(
|
||||
else:
|
||||
invoice.compliance_mx.port_of_entry = clean_str(invoice.compliance_mx.port_of_entry)
|
||||
|
||||
# Columna AD: Observación en Español (Opcional)
|
||||
# Columna AE: Observación en Español (Opcional)
|
||||
if not invoice.observation_es:
|
||||
invoice.observation_es = existing_invoice.observation_es
|
||||
else:
|
||||
invoice.observation_es = clean_str(invoice.observation_es)
|
||||
|
||||
# Columna AD: Observación en Inglés (Opcional)
|
||||
# Columna AF: Observación en Inglés (Opcional)
|
||||
if not invoice.observation_en:
|
||||
invoice.observation_en = existing_invoice.observation_en
|
||||
else:
|
||||
invoice.observation_en = clean_str(invoice.observation_en)
|
||||
|
||||
# Columna AG: cfdi_uuid (Opcional)
|
||||
if not invoice.cfdi_uuid:
|
||||
invoice.cfdi_uuid = existing_invoice.cfdi_uuid if existing_invoice.compliance_mx else None
|
||||
else:
|
||||
invoice.cfdi_uuid = clean_str(invoice.cfdi_uuid)
|
||||
|
||||
# Columna AH: Localizacion (Opcional)
|
||||
if invoice.compliance_mx.location:
|
||||
if not invoice.compliance_mx.location:
|
||||
invoice.compliance_mx.location = existing_invoice.compliance_mx.location if existing_invoice.compliance_mx.location else None
|
||||
else:
|
||||
invoice.compliance_mx.location = clean_str(invoice.compliance_mx.location)
|
||||
|
||||
@@ -49,7 +49,7 @@ def validate_update(
|
||||
}
|
||||
|
||||
validate_required_fields_by_operation(
|
||||
invoice=invoice_dict,
|
||||
invoice_data=invoice_dict,
|
||||
operation_type=invoice.operation_type or (existing_invoice.operation_type or 'imp'),
|
||||
errors=errors
|
||||
)
|
||||
|
||||
@@ -210,75 +210,75 @@ class InvoiceFinancialsBase(BaseModel):
|
||||
currency_type: Optional[str] = Field("USD", description="Currency type")
|
||||
exchange_rate: Optional[Decimal] = Field(0.00, description="Exchange rate")
|
||||
exchange_rate_mm: Optional[Decimal] = Field(
|
||||
None, description="Exchange rate currency to currency"
|
||||
0.00, description="Exchange rate currency to currency"
|
||||
)
|
||||
value_mn: Optional[Decimal] = Field(None, description="Value in MXN")
|
||||
value_me: Optional[Decimal] = Field(None, description="Value in foreign currency")
|
||||
value_mc: Optional[Decimal] = Field(None, description="Value in third currency")
|
||||
value_mn: Optional[Decimal] = Field(0.00, description="Value in MXN")
|
||||
value_me: Optional[Decimal] = Field(0.00, description="Value in foreign currency")
|
||||
value_mc: Optional[Decimal] = Field(0.00, description="Value in third currency")
|
||||
customs_value_mn: Optional[Decimal] = Field(
|
||||
None, description="Customs value in MXN"
|
||||
0.00, description="Customs value in MXN"
|
||||
)
|
||||
customs_value_me: Optional[Decimal] = Field(
|
||||
None, description="Customs value in foreign currency"
|
||||
0.00, description="Customs value in foreign currency"
|
||||
)
|
||||
raw_material_value_mn: Optional[Decimal] = Field(
|
||||
None, description="Raw material value in MXN"
|
||||
0.00, description="Raw material value in MXN"
|
||||
)
|
||||
raw_material_value_me: Optional[Decimal] = Field(
|
||||
None, description="Raw material value in foreign currency"
|
||||
0.00, description="Raw material value in foreign currency"
|
||||
)
|
||||
aggregate_value_mn: Optional[Decimal] = Field(
|
||||
None, description="Aggregate value in MXN"
|
||||
0.00, description="Aggregate value in MXN"
|
||||
)
|
||||
aggregate_value_me: Optional[Decimal] = Field(
|
||||
None, description="Aggregate value in foreign currency"
|
||||
0.00, description="Aggregate value in foreign currency"
|
||||
)
|
||||
aggregate_value_mc: Optional[Decimal] = Field(
|
||||
None, description="Aggregate value in third currency"
|
||||
0.00, description="Aggregate value in third currency"
|
||||
)
|
||||
mexican_value_mn: Optional[Decimal] = Field(
|
||||
None, description="Mexican merchandise value in MXN"
|
||||
0.00, description="Mexican merchandise value in MXN"
|
||||
)
|
||||
mexican_value_me: Optional[Decimal] = Field(
|
||||
None, description="Mexican merchandise value in foreign currency"
|
||||
0.00, description="Mexican merchandise value in foreign currency"
|
||||
)
|
||||
mexican_value_mc: Optional[Decimal] = Field(
|
||||
None, description="Mexican merchandise value in third currency"
|
||||
0.00, description="Mexican merchandise value in third currency"
|
||||
)
|
||||
national_packaging_mn: Optional[Decimal] = Field(
|
||||
None, description="National packaging in MXN"
|
||||
0.00, description="National packaging in MXN"
|
||||
)
|
||||
national_packaging_me: Optional[Decimal] = Field(
|
||||
None, description="National packaging in foreign currency"
|
||||
0.00, description="National packaging in foreign currency"
|
||||
)
|
||||
national_packaging_mc: Optional[Decimal] = Field(
|
||||
None, description="National packaging in third currency"
|
||||
0.00, description="National packaging in third currency"
|
||||
)
|
||||
freight: Optional[Decimal] = Field(None, description="Freight cost")
|
||||
insurance: Optional[Decimal] = Field(None, description="Insurance cost")
|
||||
insurance_value: Optional[Decimal] = Field(None, description="Insurance value")
|
||||
packaging: Optional[Decimal] = Field(None, description="Packaging")
|
||||
other_increments: Optional[Decimal] = Field(None, description="Other increments")
|
||||
other_deductibles: Optional[Decimal] = Field(None, description="Other deductibles")
|
||||
freight: Optional[Decimal] = Field(0.00, description="Freight cost")
|
||||
insurance: Optional[Decimal] = Field(0.00, description="Insurance cost")
|
||||
insurance_value: Optional[Decimal] = Field(0.00, description="Insurance value")
|
||||
packaging: Optional[Decimal] = Field(0.00, description="Packaging")
|
||||
other_increments: Optional[Decimal] = Field(0.00, description="Other increments")
|
||||
other_deductibles: Optional[Decimal] = Field(0.00, description="Other deductibles")
|
||||
total_increments_mn: Optional[Decimal] = Field(
|
||||
None, description="Total increments in MXN"
|
||||
0.00, description="Total increments in MXN"
|
||||
)
|
||||
total_increments_me: Optional[Decimal] = Field(
|
||||
None, description="Total increments in foreign currency"
|
||||
0.00, description="Total increments in foreign currency"
|
||||
)
|
||||
iva_mn: Optional[Decimal] = Field(None, description="IVA in MXN")
|
||||
iva_me: Optional[Decimal] = Field(None, description="IVA in foreign currency")
|
||||
iva_mc: Optional[Decimal] = Field(None, description="IVA in third currency")
|
||||
iva_factor: Optional[Decimal] = Field(None, description="IVA factor")
|
||||
iva_mn: Optional[Decimal] = Field(0.00, description="IVA in MXN")
|
||||
iva_me: Optional[Decimal] = Field(0.00, description="IVA in foreign currency")
|
||||
iva_mc: Optional[Decimal] = Field(0.00, description="IVA in third currency")
|
||||
iva_factor: Optional[Decimal] = Field(0.00, description="IVA factor")
|
||||
tax_value_me: Optional[Decimal] = Field(
|
||||
None, description="Tax value in foreign currency"
|
||||
0.00, description="Tax value in foreign currency"
|
||||
)
|
||||
seal_value_2500: Optional[bool] = Field(None, description="Seal value 2500")
|
||||
total_quantity: Optional[Decimal] = Field(None, description="Total quantity")
|
||||
gross_weight: Optional[Decimal] = Field(None, description="Gross weight")
|
||||
net_weight: Optional[Decimal] = Field(None, description="Net weight")
|
||||
bundle_count: Optional[int] = Field(None, description="Bundle count")
|
||||
weight_factor: Optional[Decimal] = Field(None, description="Weight factor")
|
||||
total_quantity: Optional[Decimal] = Field(0.00, description="Total quantity")
|
||||
gross_weight: Optional[Decimal] = Field(0.00, description="Gross weight")
|
||||
net_weight: Optional[Decimal] = Field(0.00, description="Net weight")
|
||||
bundle_count: Optional[int] = Field(0, description="Bundle count")
|
||||
weight_factor: Optional[Decimal] = Field(0.00, description="Weight factor")
|
||||
|
||||
|
||||
class InvoiceLogisticsBase(BaseModel):
|
||||
@@ -485,7 +485,7 @@ class InvoiceHeaderUpdate(InvoiceHeaderBase):
|
||||
operation_type: Optional[OperationType] = None
|
||||
|
||||
compliance_mx: Optional[InvoiceComplianceMxUpdate] = None
|
||||
financials: Optional[InvoiceFinancialsUpdate] = None
|
||||
financials: Optional[InvoiceFinancialsUpdate]
|
||||
logistics: Optional[InvoiceLogisticsUpdate] = None
|
||||
details: Optional[List[InvoiceSalesDetailsUpdate]] = None
|
||||
collections: Optional[List[InvoiceCollectionsUpdate]] = None
|
||||
|
||||
@@ -277,9 +277,9 @@ class InvoiceService:
|
||||
)
|
||||
|
||||
if invoice_data.operation_type == "exp":
|
||||
validate_update_export(invoice_data, invoice, errors)
|
||||
validate_update_export(db, invoice_data, invoice, tenant_id, company_id, errors)
|
||||
else:
|
||||
validate_update_import(invoice_data, invoice, errors)
|
||||
validate_update_import(db, invoice_data, invoice, tenant_id, company_id, errors)
|
||||
|
||||
# Si hay errores, lanzar excepción ANTES de actualizar
|
||||
errors.raise_if_errors("Error al actualizar la factura")
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
</script>
|
||||
|
||||
<!-- Layout de 2 columnas compacto -->
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<!-- Columna Izquierda -->
|
||||
<div class="space-y-3 rounded-md border p-3">
|
||||
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Información General</h4>
|
||||
@@ -148,7 +148,7 @@
|
||||
</div>
|
||||
|
||||
<!-- DESTINO/ORIGEN Y ES MIXTO -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="destino_origen" class="text-xs">Destino/Origen:</Label>
|
||||
<Input
|
||||
@@ -165,7 +165,7 @@
|
||||
<RadioGroup
|
||||
value={String(formData.is_mixed)}
|
||||
onValueChange={(v: string | undefined) => (formData.is_mixed = v === 'true')}
|
||||
class="flex gap-4"
|
||||
class="flex flex-wrap gap-4"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<RadioGroupItem value="true" id="mixed_si" />
|
||||
@@ -205,7 +205,7 @@
|
||||
<div class="space-y-3 pt-1">
|
||||
<div class="space-y-1.5">
|
||||
<Label class="text-xs">Razón de exportación:</Label>
|
||||
<RadioGroup bind:value={formData.reason_export} class="flex gap-4">
|
||||
<RadioGroup bind:value={formData.reason_export} class="flex flex-wrap gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<RadioGroupItem value="1" id="reason_vendido" />
|
||||
<Label for="reason_vendido" class="text-xs text-muted-foreground">Vendido</Label>
|
||||
@@ -245,7 +245,7 @@
|
||||
{/if}
|
||||
|
||||
<!-- CHECKBOXES -->
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
|
||||
{#if operationType !== 1 && invoiceType !== 'CR'}
|
||||
<div class="flex items-center gap-2">
|
||||
<Checkbox id="fue_revisado" bind:checked={formData.fue_revisado_equipo} />
|
||||
|
||||
@@ -302,12 +302,12 @@
|
||||
</script>
|
||||
|
||||
<!-- Layout de 2 columnas compacto -->
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Columna Izquierda: Clientes - Proveedores - Agente Aduanal -->
|
||||
<div class="space-y-3 rounded-md border p-3">
|
||||
{#if invoiceType !== 'MEX'}
|
||||
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Datos del pedimento</h4>
|
||||
<div class="grid grid-cols-4 gap-3 text-xs">
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 text-xs">
|
||||
<div>
|
||||
<span class="text-muted-foreground">Fecha del:</span>
|
||||
<p class="font-medium">{formData.fecha_pedimento_del || '-'}</p>
|
||||
@@ -330,7 +330,8 @@
|
||||
<h4 class="text-xs font-semibold text-muted-foreground uppercase">
|
||||
Clientes - Proveedores - Agente Aduanal
|
||||
</h4>
|
||||
<div class="grid grid-cols-4 gap-3 space-y-1.5">
|
||||
<div class="grid grid-cols-[1fr_4fr_4fr] items-center gap-x-2 gap-y-2 min-w-0">
|
||||
<!-- Row 1: Proveedor / Exportador -->
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.provider_header || providerHeaderOptions[0]?.value || ''}
|
||||
@@ -338,7 +339,7 @@
|
||||
formData.provider_header = v ?? '';
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="provider_header" class="h-7 max-w-[250px] min-w-[125px] text-xs">
|
||||
<Select.Trigger id="provider_header" class="h-7 w-full text-xs">
|
||||
<span class="truncate">
|
||||
{providerHeaderOptions.find(
|
||||
(o) => o.value === (formData.provider_header || providerHeaderOptions[0]?.value)
|
||||
@@ -360,7 +361,7 @@
|
||||
formData.provider_id = v ? parseInt(v) : null;
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="provider_id" class="h-7 max-w-[250px] min-w-[120px] text-xs">
|
||||
<Select.Trigger id="provider_id" class="h-7 w-full text-xs">
|
||||
<span class="truncate">
|
||||
{#if formData.provider_id}
|
||||
{providers.find((p) => p.id === formData.provider_id)?.name || 'Selecciona...'}
|
||||
@@ -377,10 +378,9 @@
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<span class="text-red-500">*</span>
|
||||
</div>
|
||||
<span class="text-red-500 font-bold">*</span>
|
||||
|
||||
<div class="grid grid-cols-4 gap-3 space-y-1.5">
|
||||
<!-- Row 2: Consignado a / Vendido a / Importador -->
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.sold_to_header || soldToHeaderOptions[0]?.value || ''}
|
||||
@@ -388,7 +388,7 @@
|
||||
formData.sold_to_header = v ?? '';
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="sold_to_header" class="h-7 max-w-[250px] min-w-[125px] text-xs">
|
||||
<Select.Trigger id="sold_to_header" class="h-7 w-full text-xs">
|
||||
<span class="truncate">
|
||||
{soldToHeaderOptions.find(
|
||||
(o) => o.value === (formData.sold_to_header || soldToHeaderOptions[0]?.value)
|
||||
@@ -410,7 +410,7 @@
|
||||
formData.sold_to_id = v ? parseInt(v) : null;
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="sold_to_id" class="h-7 max-w-[250px] min-w-[120px] text-xs">
|
||||
<Select.Trigger id="sold_to_id" class="h-7 w-full text-xs">
|
||||
<span class="truncate">
|
||||
{#if formData.sold_to_id}
|
||||
{clients.find((c) => c.id === formData.sold_to_id)?.name || 'Selecciona...'}
|
||||
@@ -427,10 +427,9 @@
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<span class="text-red-500">*</span>
|
||||
</div>
|
||||
<span class="text-red-500 font-bold">*</span>
|
||||
|
||||
<div class="grid grid-cols-4 gap-3 space-y-1.5">
|
||||
<!-- Row 3: Enviado a -->
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.shipped_to_header || shippedToHeaderOptions[0]?.value || ''}
|
||||
@@ -438,7 +437,7 @@
|
||||
formData.shipped_to_header = v ?? '';
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="shipped_to_header" class="h-7 text-xs min-w-[125px] max-w-[250px]">
|
||||
<Select.Trigger id="shipped_to_header" class="h-7 w-full text-xs">
|
||||
<span class="truncate">
|
||||
{shippedToHeaderOptions.find(
|
||||
(o) => o.value === (formData.shipped_to_header || shippedToHeaderOptions[0]?.value)
|
||||
@@ -460,7 +459,7 @@
|
||||
formData.shipped_to_id = v ? parseInt(v) : null;
|
||||
}}
|
||||
>
|
||||
<Select.Trigger id="shipped_to_id" class="h-7 text-xs min-w-[120px] max-w-[250px]">
|
||||
<Select.Trigger id="shipped_to_id" class="h-7 w-full text-xs">
|
||||
<span class="truncate">
|
||||
{#if formData.shipped_to_id}
|
||||
{allClientsProviders.find((cp) => cp.id === formData.shipped_to_id)?.name ||
|
||||
@@ -478,7 +477,7 @@
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
<span class="text-red-500">*</span>
|
||||
<span class="text-red-500 font-bold">*</span>
|
||||
</div>
|
||||
|
||||
<div class="space-y-1.5">
|
||||
@@ -542,7 +541,7 @@
|
||||
<div class="space-y-3">
|
||||
<!-- Tipo de Moneda - Pesos Netos y Brutos -->
|
||||
<div class="border rounded-md p-3 space-y-2">
|
||||
<div class="flex justify-between">
|
||||
<div class="flex flex-wrap justify-between gap-1">
|
||||
<h4 class="text-xs font-semibold text-muted-foreground uppercase">
|
||||
Tipo de Moneda - Pesos Netos y Brutos
|
||||
</h4>
|
||||
@@ -562,7 +561,7 @@
|
||||
|
||||
<!-- Radio buttons para tipo de moneda -->
|
||||
<div class="space-y-1.5">
|
||||
<RadioGroup.Root bind:value={formData.currency} class="flex gap-4">
|
||||
<RadioGroup.Root bind:value={formData.currency} class="flex flex-wrap gap-x-4 gap-y-2">
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="foreign" id="currency-foreign" class="h-4 w-4" />
|
||||
<Label for="currency-foreign" class="text-xs font-normal cursor-pointer"
|
||||
@@ -608,7 +607,7 @@
|
||||
</Select.Root>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="weight_type" class="text-xs">Tipo Peso:</Label>
|
||||
<Select.Root
|
||||
@@ -675,7 +674,7 @@
|
||||
<div class="border rounded-md p-3 space-y-2">
|
||||
<h4 class="text-xs font-semibold text-muted-foreground uppercase">Transportista</h4>
|
||||
|
||||
<div class="grid grid-cols-3 gap-2">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-3 gap-2">
|
||||
{#if invoiceType !== 'MEX'}
|
||||
<div class="space-y-1.5">
|
||||
<Label for="carrier_id" class="text-xs">Transportista:</Label>
|
||||
@@ -773,7 +772,7 @@
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="transport_type" class="text-xs">Tipo Transporte:</Label>
|
||||
<Select.Root
|
||||
@@ -799,7 +798,7 @@
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
<div class="col-span-3 space-y-1.5">
|
||||
<div class="col-span-1 sm:col-span-3 space-y-1.5">
|
||||
<Label for="transport_num" class="text-xs">Placas:</Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
|
||||
@@ -124,8 +124,8 @@
|
||||
</script>
|
||||
|
||||
<!-- Datos Principales en una fila compacta (reusable across tabs) -->
|
||||
<div class="grid grid-cols-12 items-end gap-3 pb-3">
|
||||
<div class="col-span-1 space-y-1">
|
||||
<div class="flex flex-wrap items-end gap-3 pb-3">
|
||||
<div class="min-w-[100px] flex-1 space-y-1">
|
||||
<Label for="operation_type" class="text-xs"
|
||||
>Tipo de Operación<span class="text-red-500">*</span></Label
|
||||
>
|
||||
@@ -147,7 +147,7 @@
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
<div class="col-span-1 space-y-1">
|
||||
<div class="min-w-[100px] flex-1 space-y-1">
|
||||
<Label for="operation_type" class="text-xs"
|
||||
>Tipo de factura <span class="text-red-500">*</span></Label
|
||||
>
|
||||
@@ -174,21 +174,31 @@
|
||||
</div>
|
||||
|
||||
{#if invoiceType !== 'MEX'}
|
||||
<div class="col-span-1 flex flex-col items-center space-y-1 pb-1">
|
||||
<div class="flex flex-col items-center space-y-1 pb-1">
|
||||
<Label for="is_pedimento_pending" class="text-xs">Pedimento Pendiente?</Label>
|
||||
<Switch
|
||||
id="is_pedimento_pending"
|
||||
checked={formData.is_pedimento_pending}
|
||||
onCheckedChange={(checked) => {
|
||||
formData.is_pedimento_pending = checked;
|
||||
if (checked) {
|
||||
formData.pedimento_id = null;
|
||||
formData.pedimento = '';
|
||||
formData.remesa = '';
|
||||
formData.fecha_pedimento_del = '';
|
||||
formData.fecha_pedimento_al = '';
|
||||
formData.clave_pedimento = '';
|
||||
formData.regimen_pedimento = '';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="col-span-2 space-y-1">
|
||||
<div class="min-w-[75px] flex-[2] space-y-1">
|
||||
<Label for="pedimento" class="text-xs">Pedimento</Label>
|
||||
<Select.Root
|
||||
<Select.Root
|
||||
type="single"
|
||||
value={formData.pedimento_id ? String(formData.pedimento_id) : ''}
|
||||
disabled={formData.is_pedimento_pending}
|
||||
onValueChange={(v) => {
|
||||
formData.pedimento_id = v ? parseInt(v) : null;
|
||||
if (v) {
|
||||
@@ -214,13 +224,13 @@
|
||||
</Select.Root>
|
||||
</div>
|
||||
|
||||
<div class="col-span-1 space-y-1">
|
||||
<div class="min-w-[80px] flex-1 space-y-1">
|
||||
<Label for="remesa" class="text-xs">Remesa</Label>
|
||||
<Input id="remesa" bind:value={formData.remesa} class="h-8 text-sm" />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="col-span-2 space-y-1">
|
||||
<div class="min-w-[140px] flex-[2] space-y-1">
|
||||
<Label for="invoice_number" class="text-xs"
|
||||
>Núm. Factura <span class="text-red-500">*</span></Label
|
||||
>
|
||||
@@ -232,7 +242,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 space-y-1">
|
||||
<div class="min-w-[140px] flex-[2] space-y-1">
|
||||
<Label for="invoice_date" class="text-xs">
|
||||
{formData.operation_type === 'exp' || invoiceType === 'CR'
|
||||
? 'Fecha'
|
||||
@@ -244,17 +254,17 @@
|
||||
<Input id="invoice_date" type="date" bind:value={formData.invoice_date} class="h-8 text-sm" />
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 space-y-1">
|
||||
<div class="min-w-[140px] flex-[2] space-y-1">
|
||||
<Label for="emission_date" class="text-xs">Fecha Emisión</Label>
|
||||
<Input id="emission_date" type="date" bind:value={formData.emission_date} class="h-8 text-sm" />
|
||||
</div>
|
||||
|
||||
{#if invoiceType === 'MEX'}
|
||||
<div class="col-span-1 space-y-1">
|
||||
<div class="min-w-[90px] flex-1 space-y-1">
|
||||
<Label for="iva_factor" class="text-xs">Factor IVA</Label>
|
||||
<Input id="iva_factor" bind:value={formData.iva_factor} class="h-8 text-sm" />
|
||||
</div>
|
||||
<div class="col-span-2 space-y-1">
|
||||
<div class="min-w-[140px] flex-[2] space-y-1">
|
||||
<Label for="alternate_invoice" class="text-xs">Factura Alterna</Label>
|
||||
<Input id="alternate_invoice" bind:value={formData.alternate_invoice} class="h-8 text-sm" />
|
||||
</div>
|
||||
|
||||
@@ -96,7 +96,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<!-- Left Column: Observations and Catalog Info -->
|
||||
<div class="space-y-4">
|
||||
<!-- Observations Section -->
|
||||
@@ -249,7 +249,7 @@
|
||||
|
||||
{#if operationType === 1 || invoiceType === 'CR'}
|
||||
<!-- Precintos & Tipo Mov Group (No Title) -->
|
||||
<div class="grid grid-cols-2 gap-4 rounded-md border bg-muted/20 p-3">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4 rounded-md border bg-muted/20 p-3">
|
||||
<div class="space-y-1">
|
||||
<Label for="num_seals_exp" class="text-xs font-semibold">Número de Precinto:</Label>
|
||||
<Select.Root
|
||||
@@ -290,7 +290,7 @@
|
||||
<h4 class="mb-1 border-b pb-2 text-xs font-bold text-muted-foreground uppercase">
|
||||
Factura Alterna & Flags
|
||||
</h4>
|
||||
<div class="grid grid-cols-2 gap-x-6 gap-y-4">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-4">
|
||||
<div class="space-y-1.5">
|
||||
<Label for="alternate_invoice_exp" class="text-xs font-semibold">Factura Alterna:</Label
|
||||
>
|
||||
@@ -520,8 +520,8 @@
|
||||
|
||||
<!-- Extra Bottom Row for Import Mode specific fields (Original Layout preserved for Import) -->
|
||||
{#if operationType !== 1 && invoiceType !== 'MEX' && invoiceType !== 'CR'}
|
||||
<div class="col-span-2 space-y-3 rounded-md border p-3">
|
||||
<div class="grid grid-cols-4 gap-3">
|
||||
<div class="col-span-1 md:col-span-2 space-y-3 rounded-md border p-3">
|
||||
<div class="grid grid-cols-2 sm:grid-cols-4 gap-3">
|
||||
<div class="space-y-1">
|
||||
<Label for="num_seals" class="text-xs">Num Precintos:</Label>
|
||||
<Select.Root
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
</script>
|
||||
|
||||
{#if invoiceType !== 'MEX'}
|
||||
<div class="grid grid-cols-3 grid-rows-1 gap-3">
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
<div class="space-y-3 rounded-md border p-3">
|
||||
<!-- Modo de Transporte -->
|
||||
<div class="space-y-2">
|
||||
@@ -152,7 +152,7 @@
|
||||
<RadioGroup.Root
|
||||
value={String(formData.is_mixed)}
|
||||
onValueChange={(v: string | undefined) => (formData.is_mixed = v === 'true')}
|
||||
class="flex gap-4"
|
||||
class="flex flex-wrap gap-4"
|
||||
>
|
||||
<div class="flex items-center space-x-2">
|
||||
<RadioGroup.Item value="true" id="mixed-si" class="h-4 w-4" />
|
||||
@@ -248,7 +248,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-2 space-y-3 rounded-md border p-3">
|
||||
<div class="col-span-1 md:col-span-2 space-y-3 rounded-md border p-3">
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
<!-- ID Relación Docs -->
|
||||
<div class="space-y-2">
|
||||
|
||||
@@ -1002,7 +1002,7 @@
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
class="fixed right-0 bottom-0 left-0 z-[5] ml-[calc(var(--sidebar-width))] border-t bg-background/95 shadow-lg backdrop-blur group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] supports-[backdrop-filter]:bg-background/80"
|
||||
class="fixed right-0 bottom-0 left-0 z-[5] md:ml-[calc(var(--sidebar-width))] border-t bg-background/95 shadow-lg backdrop-blur md:group-has-data-[state=collapsed]/sidebar-wrapper:ml-[calc(var(--sidebar-width-icon))] supports-[backdrop-filter]:bg-background/80"
|
||||
>
|
||||
<div class="mx-auto max-w-[1400px] space-y-4 px-4 py-4">
|
||||
<!-- Tabs Navigation -->
|
||||
|
||||
Reference in New Issue
Block a user