feat(crm): costo estimado por servicio adicional, folios visibles y sidebar por flujo
- Solicitud: al marcar un servicio adicional se habilita su costo estimado (columna JSON additional_service_costs). Al cotizar, cada servicio marcado se siembra como concepto de la cotización con ese costo de partida (costo=venta). - Folios visibles: se muestran en la tarjeta de Oportunidad del kanban y se aclara en el formulario que el folio se asigna al guardar (las listas ya lo mostraban). - Sidebar CRM reordenado por flujo comercial (captación → embudo → solicitud → cotización → catálogos de apoyo). - Migración c2d3e4f5a6b7 aditiva y reversible. Suite backend en verde (102). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
|||||||
|
"""Costo estimado por servicio adicional en la solicitud de servicio
|
||||||
|
|
||||||
|
Revision ID: c2d3e4f5a6b7
|
||||||
|
Revises: b1c2d3e4f5a6
|
||||||
|
Create Date: 2026-08-04 00:00:00.000000
|
||||||
|
|
||||||
|
Agrega crm.service_requests.additional_service_costs (JSON: {codigo_servicio: costo})
|
||||||
|
para capturar el costo estimado de cada servicio adicional marcado; ese costo se
|
||||||
|
usa como punto de partida al sembrar los conceptos de la cotización.
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from alembic import op
|
||||||
|
|
||||||
|
revision: str = "c2d3e4f5a6b7"
|
||||||
|
down_revision: Union[str, None] = "b1c2d3e4f5a6"
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
SCHEMA = "crm"
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
op.add_column(
|
||||||
|
"service_requests",
|
||||||
|
sa.Column("additional_service_costs", sa.JSON(), nullable=True),
|
||||||
|
schema=SCHEMA,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
op.drop_column("service_requests", "additional_service_costs", schema=SCHEMA)
|
||||||
@@ -6,6 +6,7 @@ from sqlalchemy import func
|
|||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from ..accounts.models import Account
|
from ..accounts.models import Account
|
||||||
|
from ..catalogs.models import CatalogItem
|
||||||
from ..common.folios import next_folio
|
from ..common.folios import next_folio
|
||||||
from ..service_requests.models import RateRequest, ServiceRequest
|
from ..service_requests.models import RateRequest, ServiceRequest
|
||||||
from ..suppliers.models import Supplier
|
from ..suppliers.models import Supplier
|
||||||
@@ -150,6 +151,14 @@ def create_quotes_from_service_request(
|
|||||||
)
|
)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
|
# Etiquetas legibles de los servicios adicionales (global + tenant) para los conceptos
|
||||||
|
service_labels = {
|
||||||
|
code: label
|
||||||
|
for code, label in db.query(CatalogItem.code, CatalogItem.label).filter(
|
||||||
|
CatalogItem.catalog == "servicio_adicional"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
service_costs = sr.additional_service_costs or {}
|
||||||
|
|
||||||
created: list[Quote] = []
|
created: list[Quote] = []
|
||||||
for variant in variants:
|
for variant in variants:
|
||||||
@@ -178,6 +187,15 @@ def create_quotes_from_service_request(
|
|||||||
unit_cost=amount, unit_sale=amount, currency=rr.currency,
|
unit_cost=amount, unit_sale=amount, currency=rr.currency,
|
||||||
tenant_id=tenant_id, company_id=company_id,
|
tenant_id=tenant_id, company_id=company_id,
|
||||||
))
|
))
|
||||||
|
# Servicios adicionales marcados en la solicitud → conceptos con su costo estimado
|
||||||
|
for code in (sr.additional_services or []):
|
||||||
|
amount = Decimal(str(service_costs.get(code) or 0))
|
||||||
|
db.add(QuoteItem(
|
||||||
|
quote_id=quote.id, concept=code[:60],
|
||||||
|
description=service_labels.get(code, "Servicio adicional"),
|
||||||
|
quantity=Decimal(1), unit_cost=amount, unit_sale=amount,
|
||||||
|
currency=sr.currency, tenant_id=tenant_id, company_id=company_id,
|
||||||
|
))
|
||||||
db.flush()
|
db.flush()
|
||||||
_recompute_totals(db, quote)
|
_recompute_totals(db, quote)
|
||||||
created.append(quote)
|
created.append(quote)
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ class ServiceRequestBase(BaseModel):
|
|||||||
volume_per_pallet: Decimal | None = Field(None, ge=0, max_digits=14, decimal_places=3)
|
volume_per_pallet: Decimal | None = Field(None, ge=0, max_digits=14, decimal_places=3)
|
||||||
# Servicios adicionales (códigos del catálogo servicio_adicional) y pago
|
# Servicios adicionales (códigos del catálogo servicio_adicional) y pago
|
||||||
additional_services: list[str] | None = None
|
additional_services: list[str] | None = None
|
||||||
|
additional_service_costs: dict[str, float] | None = None # {codigo: costo estimado}
|
||||||
payment_method: str | None = Field(None, max_length=20)
|
payment_method: str | None = Field(None, max_length=20)
|
||||||
destination_agent_id: int | None = None
|
destination_agent_id: int | None = None
|
||||||
requirements: str | None = None
|
requirements: str | None = None
|
||||||
@@ -146,6 +147,7 @@ class ServiceRequestUpdate(BaseModel):
|
|||||||
weight_per_pallet: Decimal | None = Field(None, ge=0, max_digits=14, decimal_places=3)
|
weight_per_pallet: Decimal | None = Field(None, ge=0, max_digits=14, decimal_places=3)
|
||||||
volume_per_pallet: Decimal | None = Field(None, ge=0, max_digits=14, decimal_places=3)
|
volume_per_pallet: Decimal | None = Field(None, ge=0, max_digits=14, decimal_places=3)
|
||||||
additional_services: list[str] | None = None
|
additional_services: list[str] | None = None
|
||||||
|
additional_service_costs: dict[str, float] | None = None
|
||||||
payment_method: str | None = Field(None, max_length=20)
|
payment_method: str | None = Field(None, max_length=20)
|
||||||
destination_agent_id: int | None = None
|
destination_agent_id: int | None = None
|
||||||
requirements: str | None = None
|
requirements: str | None = None
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ class ServiceRequest(Base, TenantScopedMixin, TimestampMixin):
|
|||||||
volume_per_pallet: Mapped[float | None] = mapped_column(Numeric(14, 3), nullable=True)
|
volume_per_pallet: Mapped[float | None] = mapped_column(Numeric(14, 3), nullable=True)
|
||||||
# Servicios adicionales (lista de códigos del catálogo servicio_adicional) y pago
|
# Servicios adicionales (lista de códigos del catálogo servicio_adicional) y pago
|
||||||
additional_services: Mapped[list | None] = mapped_column(JSON, nullable=True)
|
additional_services: Mapped[list | None] = mapped_column(JSON, nullable=True)
|
||||||
|
# Costo estimado por servicio adicional marcado: {codigo: costo}
|
||||||
|
additional_service_costs: Mapped[dict | None] = mapped_column(JSON, nullable=True)
|
||||||
payment_method: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
payment_method: Mapped[str | None] = mapped_column(String(20), nullable=True)
|
||||||
# Notas
|
# Notas
|
||||||
client_notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
client_notes: Mapped[str | None] = mapped_column(Text, nullable=True)
|
||||||
|
|||||||
@@ -106,6 +106,25 @@ def test_quote_from_service_request_ambas_genera_dos(db):
|
|||||||
assert len(service.get_quote_items(db, q.id, T, C)) == 2
|
assert len(service.get_quote_items(db, q.id, T, C)) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_quote_from_service_request_seeds_additional_services(db):
|
||||||
|
sr = sr_service.create_service_request(
|
||||||
|
db,
|
||||||
|
ServiceRequestCreate(
|
||||||
|
operation_type="importacion", load_type="FCL", currency="USD",
|
||||||
|
additional_services=["seguro", "despacho_aduanal"],
|
||||||
|
additional_service_costs={"seguro": 500, "despacho_aduanal": 300},
|
||||||
|
),
|
||||||
|
T, C,
|
||||||
|
)
|
||||||
|
quotes = service.create_quotes_from_service_request(db, sr.id, T, C)
|
||||||
|
items = service.get_quote_items(db, quotes[0].id, T, C)
|
||||||
|
costs = {i.concept: float(i.unit_cost) for i in items}
|
||||||
|
assert costs.get("seguro") == 500.0
|
||||||
|
assert costs.get("despacho_aduanal") == 300.0
|
||||||
|
# el costo estimado de la solicitud es el punto de partida (costo=venta)
|
||||||
|
assert float(quotes[0].total_sale) == 800.0
|
||||||
|
|
||||||
|
|
||||||
def test_quote_from_service_request_sets_issue_date_today(db):
|
def test_quote_from_service_request_sets_issue_date_today(db):
|
||||||
sr = _sr_with_rates(db)
|
sr = _sr_with_rates(db)
|
||||||
quotes = service.create_quotes_from_service_request(db, sr.id, T, C)
|
quotes = service.create_quotes_from_service_request(db, sr.id, T, C)
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ export interface ServiceRequest {
|
|||||||
weight_per_pallet: number | null;
|
weight_per_pallet: number | null;
|
||||||
volume_per_pallet: number | null;
|
volume_per_pallet: number | null;
|
||||||
additional_services: string[] | null;
|
additional_services: string[] | null;
|
||||||
|
additional_service_costs: Record<string, number> | null;
|
||||||
payment_method: string | null;
|
payment_method: string | null;
|
||||||
destination_agent_id: number | null;
|
destination_agent_id: number | null;
|
||||||
requirements: string | null;
|
requirements: string | null;
|
||||||
|
|||||||
@@ -34,6 +34,18 @@
|
|||||||
return [c.first_name, c.last_name].filter(Boolean).join(' ');
|
return [c.first_name, c.last_name].filter(Boolean).join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Costo estimado por servicio adicional (se traspasa a la cotización)
|
||||||
|
function serviceCost(code: string): number | undefined {
|
||||||
|
return form.additional_service_costs?.[code];
|
||||||
|
}
|
||||||
|
function setServiceCost(code: string, value: string) {
|
||||||
|
const map = { ...(form.additional_service_costs ?? {}) };
|
||||||
|
const n = value === '' ? NaN : Number(value);
|
||||||
|
if (Number.isNaN(n)) delete map[code];
|
||||||
|
else map[code] = n;
|
||||||
|
form.additional_service_costs = map;
|
||||||
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
void crmCatalogs.preload([
|
void crmCatalogs.preload([
|
||||||
'pais', 'moneda', 'prioridad', 'tipo_mercancia', 'unidad_medida',
|
'pais', 'moneda', 'prioridad', 'tipo_mercancia', 'unidad_medida',
|
||||||
@@ -41,12 +53,13 @@
|
|||||||
'puerto', 'aeropuerto'
|
'puerto', 'aeropuerto'
|
||||||
]);
|
]);
|
||||||
if (!form.additional_services) form.additional_services = [];
|
if (!form.additional_services) form.additional_services = [];
|
||||||
|
if (!form.additional_service_costs) form.additional_service_costs = {};
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if tab === 'datos'}
|
{#if tab === 'datos'}
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Folio</span><input class="{inputCls} bg-muted/40" bind:value={form.reference} readonly placeholder="Se genera automáticamente" /></label>
|
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Folio</span><input class="{inputCls} bg-muted/40" bind:value={form.reference} readonly placeholder="Se asigna automáticamente al guardar (ej. S2026-08-001-E)" /></label>
|
||||||
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Tipo de operación *</span><select class={inputCls} bind:value={form.operation_type}>{#each OPERATION_TYPES as o (o.value)}<option value={o.value}>{o.label}</option>{/each}</select></label>
|
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Tipo de operación *</span><select class={inputCls} bind:value={form.operation_type}>{#each OPERATION_TYPES as o (o.value)}<option value={o.value}>{o.label}</option>{/each}</select></label>
|
||||||
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Cliente</span><select class={inputCls} bind:value={form.account_id}><option value={undefined}>—</option>{#each accounts as a (a.id)}<option value={a.id}>{a.name}</option>{/each}</select></label>
|
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Cliente</span><select class={inputCls} bind:value={form.account_id}><option value={undefined}>—</option>{#each accounts as a (a.id)}<option value={a.id}>{a.name}</option>{/each}</select></label>
|
||||||
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Contacto</span><select class={inputCls} bind:value={form.contact_id}><option value={undefined}>—</option>{#each clientContacts as c (c.id)}<option value={c.id}>{contactName(c)}</option>{/each}</select></label>
|
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Contacto</span><select class={inputCls} bind:value={form.contact_id}><option value={undefined}>—</option>{#each clientContacts as c (c.id)}<option value={c.id}>{contactName(c)}</option>{/each}</select></label>
|
||||||
@@ -139,13 +152,23 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if tab === 'servicios'}
|
{:else if tab === 'servicios'}
|
||||||
<p class="mb-2 text-sm font-medium">Servicios adicionales</p>
|
<p class="mb-1 text-sm font-medium">Servicios adicionales</p>
|
||||||
<div class="grid grid-cols-2 gap-2 sm:grid-cols-3">
|
<p class="mb-3 text-xs text-muted-foreground">Marca los servicios requeridos e indica su costo estimado (opcional). Al cotizar, cada servicio marcado se agrega como concepto de la cotización con ese costo de partida.</p>
|
||||||
|
<div class="space-y-2">
|
||||||
{#each crmCatalogs.options('servicio_adicional') as s (s.value)}
|
{#each crmCatalogs.options('servicio_adicional') as s (s.value)}
|
||||||
<label class="flex items-center gap-2 text-sm"><input type="checkbox" class="h-4 w-4 rounded border" value={s.value} bind:group={form.additional_services} /><span>{s.label}</span></label>
|
{@const checked = (form.additional_services ?? []).includes(s.value)}
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<label class="flex w-60 items-center gap-2 text-sm"><input type="checkbox" class="h-4 w-4 rounded border" value={s.value} bind:group={form.additional_services} /><span>{s.label}</span></label>
|
||||||
|
{#if checked}
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<input type="number" min="0" step="0.01" class="{inputCls} w-40" placeholder="Costo estimado" value={serviceCost(s.value) ?? ''} oninput={(e) => setServiceCost(s.value, e.currentTarget.value)} />
|
||||||
|
<span class="text-xs text-muted-foreground">{form.currency ?? ''}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
<label class="mt-4 flex max-w-md flex-col gap-1 text-sm"><span class="font-medium">Forma de pago</span><select class={inputCls} bind:value={form.payment_method}><option value={undefined}>—</option>{#each crmCatalogs.options('forma_pago') as f (f.value)}<option value={f.value}>{f.value} — {f.label}</option>{/each}</select></label>
|
<label class="mt-5 flex max-w-md flex-col gap-1 text-sm"><span class="font-medium">Forma de pago</span><select class={inputCls} bind:value={form.payment_method}><option value={undefined}>—</option>{#each crmCatalogs.options('forma_pago') as f (f.value)}<option value={f.value}>{f.value} — {f.label}</option>{/each}</select></label>
|
||||||
{:else if tab === 'notas'}
|
{:else if tab === 'notas'}
|
||||||
<div class="grid gap-4 sm:grid-cols-2">
|
<div class="grid gap-4 sm:grid-cols-2">
|
||||||
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Notas del cliente</span><textarea rows="4" class={inputCls} bind:value={form.client_notes}></textarea></label>
|
<label class="flex flex-col gap-1 text-sm"><span class="font-medium">Notas del cliente</span><textarea rows="4" class={inputCls} bind:value={form.client_notes}></textarea></label>
|
||||||
|
|||||||
@@ -42,17 +42,18 @@ export function getNavMain(): NavMainItem[] {
|
|||||||
title: 'CRM',
|
title: 'CRM',
|
||||||
url: '/dashboard/crm',
|
url: '/dashboard/crm',
|
||||||
icon: Briefcase,
|
icon: Briefcase,
|
||||||
|
// Orden por flujo comercial: captación → embudo → solicitud → cotización → apoyo
|
||||||
items: [
|
items: [
|
||||||
{ title: 'Panel', url: '/dashboard/crm' },
|
{ title: 'Panel', url: '/dashboard/crm' },
|
||||||
{ title: 'Clientes / Prospectos', url: '/dashboard/crm/cuentas' },
|
{ title: 'Clientes / Prospectos', url: '/dashboard/crm/cuentas' },
|
||||||
{ title: 'Proveedores', url: '/dashboard/crm/proveedores' },
|
|
||||||
{ title: 'Contactos', url: '/dashboard/crm/contactos' },
|
{ title: 'Contactos', url: '/dashboard/crm/contactos' },
|
||||||
|
{ title: 'Prospectos (embudo)', url: '/dashboard/crm/prospectos' },
|
||||||
|
{ title: 'Oportunidades', url: '/dashboard/crm/oportunidades' },
|
||||||
{ title: 'Solicitudes', url: '/dashboard/crm/solicitudes' },
|
{ title: 'Solicitudes', url: '/dashboard/crm/solicitudes' },
|
||||||
{ title: 'Cotizaciones', url: '/dashboard/crm/cotizaciones' },
|
{ title: 'Cotizaciones', url: '/dashboard/crm/cotizaciones' },
|
||||||
{ title: 'Tarifarios', url: '/dashboard/crm/tarifarios' },
|
{ title: 'Tarifarios', url: '/dashboard/crm/tarifarios' },
|
||||||
{ title: 'Cotizador', url: '/dashboard/crm/cotizador' },
|
{ title: 'Cotizador', url: '/dashboard/crm/cotizador' },
|
||||||
{ title: 'Prospectos (embudo)', url: '/dashboard/crm/prospectos' },
|
{ title: 'Proveedores', url: '/dashboard/crm/proveedores' },
|
||||||
{ title: 'Oportunidades', url: '/dashboard/crm/oportunidades' },
|
|
||||||
{ title: 'Actividades', url: '/dashboard/crm/actividades' },
|
{ title: 'Actividades', url: '/dashboard/crm/actividades' },
|
||||||
{ title: 'Catálogos', url: '/dashboard/crm/catalogos' },
|
{ title: 'Catálogos', url: '/dashboard/crm/catalogos' },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -276,6 +276,9 @@
|
|||||||
ondragstart={(e) => onDragStart(e, opp.id)}
|
ondragstart={(e) => onDragStart(e, opp.id)}
|
||||||
>
|
>
|
||||||
<p class="text-sm font-medium">{opp.name}</p>
|
<p class="text-sm font-medium">{opp.name}</p>
|
||||||
|
{#if opp.reference}
|
||||||
|
<p class="font-mono text-[11px] text-muted-foreground">{opp.reference}</p>
|
||||||
|
{/if}
|
||||||
{#if accountName(opp.account_id)}
|
{#if accountName(opp.account_id)}
|
||||||
<p class="text-xs text-muted-foreground">{accountName(opp.account_id)}</p>
|
<p class="text-xs text-muted-foreground">{accountName(opp.account_id)}</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
contactsAPI.list(cid),
|
contactsAPI.list(cid),
|
||||||
rateRequestsAPI.list(cid, id)
|
rateRequestsAPI.list(cid, id)
|
||||||
]);
|
]);
|
||||||
form = { ...sr, additional_services: sr.additional_services ?? [] };
|
form = { ...sr, additional_services: sr.additional_services ?? [], additional_service_costs: sr.additional_service_costs ?? {} };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(e instanceof Error ? e.message : 'No se pudo cargar la solicitud');
|
toast.error(e instanceof Error ? e.message : 'No se pudo cargar la solicitud');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -67,7 +67,7 @@
|
|||||||
saving = true;
|
saving = true;
|
||||||
try {
|
try {
|
||||||
sr = await serviceRequestsAPI.update(sr.id, form, companyId);
|
sr = await serviceRequestsAPI.update(sr.id, form, companyId);
|
||||||
form = { ...sr, additional_services: sr.additional_services ?? [] };
|
form = { ...sr, additional_services: sr.additional_services ?? [], additional_service_costs: sr.additional_service_costs ?? {} };
|
||||||
toast.success('Cambios guardados');
|
toast.success('Cambios guardados');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(e instanceof Error ? e.message : 'No se pudo guardar');
|
toast.error(e instanceof Error ? e.message : 'No se pudo guardar');
|
||||||
@@ -82,7 +82,7 @@
|
|||||||
busy = true;
|
busy = true;
|
||||||
try {
|
try {
|
||||||
sr = await serviceRequestsAPI.registerContact(sr.id, companyId, notes);
|
sr = await serviceRequestsAPI.registerContact(sr.id, companyId, notes);
|
||||||
form = { ...sr, additional_services: sr.additional_services ?? [] };
|
form = { ...sr, additional_services: sr.additional_services ?? [], additional_service_costs: sr.additional_service_costs ?? {} };
|
||||||
toast.success('Contacto registrado');
|
toast.success('Contacto registrado');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(e instanceof Error ? e.message : 'No se pudo registrar el contacto');
|
toast.error(e instanceof Error ? e.message : 'No se pudo registrar el contacto');
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
busy = true;
|
busy = true;
|
||||||
try {
|
try {
|
||||||
sr = await serviceRequestsAPI.requote(sr.id, companyId);
|
sr = await serviceRequestsAPI.requote(sr.id, companyId);
|
||||||
form = { ...sr, additional_services: sr.additional_services ?? [] };
|
form = { ...sr, additional_services: sr.additional_services ?? [], additional_service_costs: sr.additional_service_costs ?? {} };
|
||||||
toast.success('Solicitud reabierta para re-cotizar');
|
toast.success('Solicitud reabierta para re-cotizar');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast.error(e instanceof Error ? e.message : 'No se pudo reabrir');
|
toast.error(e instanceof Error ? e.message : 'No se pudo reabrir');
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
import ServiceRequestFields from '$lib/components/crm/ServiceRequestFields.svelte';
|
import ServiceRequestFields from '$lib/components/crm/ServiceRequestFields.svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
let form = $state<ServiceRequestInput>({ operation_type: 'exportacion', status: 'nueva', additional_services: [] });
|
let form = $state<ServiceRequestInput>({ operation_type: 'exportacion', status: 'nueva', additional_services: [], additional_service_costs: {} });
|
||||||
let accounts = $state<Account[]>([]);
|
let accounts = $state<Account[]>([]);
|
||||||
let suppliers = $state<Supplier[]>([]);
|
let suppliers = $state<Supplier[]>([]);
|
||||||
let contacts = $state<Contact[]>([]);
|
let contacts = $state<Contact[]>([]);
|
||||||
|
|||||||
Reference in New Issue
Block a user