feature/clases-formulario-faltante

This commit is contained in:
2026-04-14 10:20:07 -06:00
parent 36c2b174ad
commit 41d5511323
2 changed files with 133 additions and 91 deletions

View File

@@ -1,38 +0,0 @@
# CSV Ingestion Global Rollout
## Goal
- Apply one CSV ingestion policy across all upload flows.
- Detect regressions early in scan/commit execution.
## Rollout Stages
- Stage 1: Enable common `CsvReadPlan` path for semiconsolidated layouts (`classes`, `parts`, `pedmientos`).
- Stage 2: Enable common deduped-header iterator for legacy transportation layouts (`drivers`, `trailers`).
- Stage 3: Keep `facturas` business parsing while enforcing common encoding + dialect detection.
- Stage 4: Reuse common encoding detection in non-layout ingestion (`carta_porte_codes` seed).
## Metrics To Track
- `csv_scan_failed_total{layout}`: scan failures by layout.
- `csv_commit_failed_total{layout}`: commit failures by layout.
- `csv_decode_fallback_total{layout,encoding}`: resolved encoding different from UTF-8.
- `csv_headerless_detected_total{layout}`: files treated as headerless.
- `csv_rows_processed_total{layout,stage}`: processed rows in scan/commit.
## Alerting Rules
- Alert when `csv_scan_failed_total{layout}` spikes > 2x baseline for 15 minutes.
- Alert when `csv_commit_failed_total{layout}` spikes > 2x baseline for 15 minutes.
- Alert when `csv_decode_fallback_total` changes abruptly after deployment.
- Alert when median `csv_rows_processed_total` drops sharply for active layouts.
## Logging Requirements
- Log resolved encoding and selected dialect once per job.
- Log `header_mode` (`header`, `headerless`, `auto`) once per job.
- Keep `job_id`, `layout`, `tenant_id`, `company_id` in structured logs.
## Verification Checklist
- Validate one UTF-8 and one CP1252 file per layout in staging.
- Validate comma and semicolon delimiters in staging.
- Validate scan and commit consistency for line numbers and stored values.
## Rollback Strategy
- Revert to previous release if scan/commit failures exceed threshold for 30 minutes.
- Keep compatibility wrappers in `csv_reader` to reduce rollback blast radius.

View File

@@ -27,6 +27,7 @@
type FDACatalog
} from '$lib/api/dashboard/a76/general_catalogs/fda-catalog';
import { companyStore } from '$lib/stores/company.svelte';
import * as RadioGroup from '$lib/components/ui/radio-group';
interface Props {
onSave?: (data: any) => void;
@@ -64,7 +65,7 @@
annual_depreciation_rate: initialData?.annual_depreciation_rate || '',
fda_key: initialData?.fda_key || '',
eccn_code: initialData?.eccn_code || '',
iva_exempt_fraction: initialData?.iva_exempt_fraction || false,
iva_exempt_fraction: '',
physical_review: initialData?.physical_review || false,
carta_porte_code: initialData?.carta_porte_code || '',
// Campos de tarifas de importación/exportación
@@ -82,6 +83,16 @@
us_fraction_fixed_rate: ''
});
/** Sí / No (legacy); el valor persistido es `iva_exempt_fraction` (string máx. 4) */
let ivaExemptChoice = $state<'no' | 'yes'>('no');
function setIvaExemptChoice(choice: 'no' | 'yes') {
ivaExemptChoice = choice;
if (choice === 'no') {
formData.iva_exempt_fraction = '';
}
}
// Reactively update formData when initialData changes
$effect(() => {
if (initialData) {
@@ -102,13 +113,23 @@
// Mapear fda_code a fda_key si existe
formData.fda_key = snap.fda_code ?? snap.fda_key ?? '';
formData.eccn_code = snap.eccn_code ?? '';
formData.iva_exempt_fraction = snap.iva_exempt_fraction ?? false;
formData.physical_review = snap.physical_review ?? false;
const ivaRaw = snap.iva_exempt_fraction;
const ivaStr =
ivaRaw !== undefined && ivaRaw !== null && ivaRaw !== false && ivaRaw !== true
? String(ivaRaw).trim().slice(0, 4)
: '';
formData.iva_exempt_fraction = ivaStr;
ivaExemptChoice = ivaStr.length > 0 ? 'yes' : 'no';
formData.physical_review =
snap.physical_review === 1 ||
snap.physical_review === true ||
snap.physical_review === '1';
formData.carta_porte_code = snap.carta_porte_code ?? '';
formData.import_tariff_code = snap.import_tariff_code ?? '';
formData.import_tariff_type = snap.import_tariff_type ?? '';
formData.export_tariff_code = snap.export_tariff_code ?? '';
formData.export_tariff_type = snap.export_tariff_type ?? '';
formData.fraction_umt = snap.import_tariff_type ?? '';
// Reiniciar banderas de error al cargar datos
showErrors = false;
@@ -125,13 +146,19 @@
formData.annual_depreciation_rate = '';
formData.fda_key = '';
formData.eccn_code = '';
formData.iva_exempt_fraction = false;
formData.iva_exempt_fraction = '';
ivaExemptChoice = 'no';
formData.physical_review = false;
formData.carta_porte_code = '';
formData.import_tariff_code = '';
formData.import_tariff_type = '';
formData.export_tariff_code = '';
formData.export_tariff_type = '';
formData.unit_measure_key = '';
formData.fraction_umt = '';
formData.fraction_uma_key = '';
formData.us_fraction_ad_valorem = '';
formData.us_fraction_fixed_rate = '';
// Reiniciar banderas de error al limpiar formulario
showErrors = false;
@@ -708,12 +735,12 @@
<Label for="unit_of_measure" class="font-bold">
U.M. Comercial: <span class="text-red-500">*</span>
</Label>
<div class="flex items-center gap-2">
<div class="flex flex-wrap items-center gap-2">
<Input
id="unit_of_measure"
bind:value={formData.unit_of_measure}
placeholder="UM"
class="flex-1 uppercase {validationErrors.unit_of_measure
class="min-w-[5rem] flex-1 uppercase {validationErrors.unit_of_measure
? 'border-red-500 focus-visible:ring-red-500'
: ''}"
maxlength={5}
@@ -725,6 +752,10 @@
<span class="cursor-pointer text-sm text-blue-600 hover:underline">
{formData.unit_of_measure_description || ''}
</span>
<span class="text-sm text-muted-foreground">
Clave U.M.A:
<span class="font-mono text-blue-600">{formData.unit_measure_key || '—'}</span>
</span>
</div>
{#if validationErrors.unit_of_measure}
<p class="mt-1 text-sm text-red-500">{validationErrors.unit_of_measure}</p>
@@ -735,12 +766,12 @@
<Label for="fraction" class="font-bold">
Fracción Arancelaria: <span class="text-red-500">*</span>
</Label>
<div class="flex items-center gap-2">
<div class="flex flex-wrap items-center gap-2">
<Input
id="fraction"
bind:value={formData.fraction}
placeholder="Fracción"
class="flex-1 {validationErrors.fraction
class="min-w-[8rem] flex-1 {validationErrors.fraction
? 'border-red-500 focus-visible:ring-red-500'
: ''}"
maxlength={10}
@@ -750,31 +781,47 @@
<Folder class="h-4 w-4" />
</Button>
</div>
<div class="flex flex-wrap gap-4 text-sm">
<span class="text-muted-foreground"
>U.M.T: <span class="font-mono text-blue-600">{formData.fraction_umt || '—'}</span></span
>
<span class="text-muted-foreground"
>Clave U.M.A:
<span class="font-mono text-blue-600">{formData.fraction_uma_key || '—'}</span></span
>
</div>
{#if validationErrors.fraction}
<p class="mt-1 text-sm text-red-500">{validationErrors.fraction}</p>
{/if}
</div>
</div>
<!-- Fracción Americana y Tasa Depreciación -->
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="space-y-2">
<Label for="us_fraction">Fracción Americana:</Label>
<div class="flex items-center gap-2">
<Input
id="us_fraction"
bind:value={formData.us_fraction}
placeholder="0000.00.00.00"
class="flex-1 font-mono tracking-wider"
maxlength={13}
oninput={formatUSFraction}
/>
<Button type="button" variant="outline" size="icon" onclick={openUSFractionSearch}>
<Folder class="h-4 w-4" />
</Button>
</div>
<!-- Fracción Americana (con AdValorem / Tasa fija como en legacy) -->
<div class="space-y-2">
<Label for="us_fraction">Fracción Americana:</Label>
<div class="flex flex-wrap items-center gap-2">
<Input
id="us_fraction"
bind:value={formData.us_fraction}
placeholder="0000.00.00.00"
class="min-w-[10rem] flex-1 font-mono tracking-wider"
maxlength={13}
oninput={formatUSFraction}
/>
<Button type="button" variant="outline" size="icon" onclick={openUSFractionSearch}>
<Folder class="h-4 w-4" />
</Button>
<span class="text-sm text-blue-600"
>AdValorem: {formData.us_fraction_ad_valorem || '0.00'}</span
>
<span class="text-sm text-blue-600"
>Tasa Fija: {formData.us_fraction_fixed_rate || '0.00000000'}</span
>
</div>
</div>
<!-- Tasa depreciación y ECCN (misma fila que en pantalla legacy) -->
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="space-y-2">
<Label for="annual_depreciation_rate">Tasa Anual de Depreciación:</Label>
<div class="flex items-center gap-2">
@@ -792,46 +839,79 @@
</Button>
</div>
</div>
</div>
<!-- FDA, Carta Porte y ECCN -->
<div class="grid grid-cols-1 gap-4 md:grid-cols-3">
<div class="space-y-2">
<Label for="fda_key">Clave FDA:</Label>
<div class="flex items-center gap-2">
<Input id="fda_key" bind:value={formData.fda_key} placeholder="Clave FDA" class="flex-1" />
<Button type="button" variant="outline" size="icon" onclick={openFDASearch}>
<Folder class="h-4 w-4" />
</Button>
</div>
</div>
<div class="space-y-2">
<Label for="carta_porte_code">Carta Porte:</Label>
<div class="flex items-center gap-2">
<Input
id="carta_porte_code"
bind:value={formData.carta_porte_code}
placeholder="Código"
class="flex-1"
/>
<Button type="button" variant="outline" size="icon" onclick={openCartaPorteSearch}>
<Folder class="h-4 w-4" />
</Button>
</div>
</div>
<div class="space-y-2">
<Label for="eccn_code">ECCN:</Label>
<Input
id="eccn_code"
bind:value={formData.eccn_code}
placeholder="Código ECCN"
class="flex-1 uppercase"
class="w-full uppercase"
maxlength={20}
/>
</div>
</div>
<!-- Clave FDA -->
<div class="space-y-2">
<Label for="fda_key">Clave FDA:</Label>
<div class="flex items-center gap-2">
<Input id="fda_key" bind:value={formData.fda_key} placeholder="Clave FDA" class="flex-1" />
<Button type="button" variant="outline" size="icon" onclick={openFDASearch}>
<Folder class="h-4 w-4" />
</Button>
</div>
</div>
<!-- Fracción exenta de IVA (Sí / No + código opcional máx. 4) -->
<div class="space-y-2">
<Label class="font-bold">Fracción exenta de IVA</Label>
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:gap-6">
<RadioGroup.Root
value={ivaExemptChoice}
onValueChange={(v) => setIvaExemptChoice(v as 'no' | 'yes')}
class="flex flex-wrap gap-6"
>
<div class="flex cursor-pointer items-center gap-2">
<RadioGroup.Item value="no" id="iva-no" class="h-4 w-4 border-primary" />
<Label for="iva-no" class="cursor-pointer font-normal">No</Label>
</div>
<div class="flex cursor-pointer items-center gap-2">
<RadioGroup.Item value="yes" id="iva-yes" class="h-4 w-4 border-primary" />
<Label for="iva-yes" class="cursor-pointer font-normal"></Label>
</div>
</RadioGroup.Root>
{#if ivaExemptChoice === 'yes'}
<div class="flex max-w-xs flex-1 items-center gap-2">
<Label for="iva_exempt_code" class="text-muted-foreground shrink-0 text-sm">Código</Label>
<Input
id="iva_exempt_code"
bind:value={formData.iva_exempt_fraction}
placeholder="Máx. 4"
class="font-mono uppercase"
maxlength={4}
/>
</div>
{/if}
</div>
</div>
<!-- Código Carta Porte -->
<div class="space-y-2">
<Label for="carta_porte_code">Código Carta Porte:</Label>
<div class="flex items-center gap-2">
<Input
id="carta_porte_code"
bind:value={formData.carta_porte_code}
placeholder="Código"
class="flex-1"
/>
<Button type="button" variant="outline" size="icon" onclick={openCartaPorteSearch}>
<Folder class="h-4 w-4" />
</Button>
</div>
</div>
<!-- Diálogos -->
<!-- Dialog: Material Types -->
<Dialog.Root bind:open={showMaterialDialog}>