Merge pull request 'feature/clases-formulario-faltante' (#276) from feature/table-classes-lost-columns into development
Reviewed-on: ADUANASOFT/anexo76#276
This commit is contained in:
@@ -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.
|
||||
@@ -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">Sí</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}>
|
||||
|
||||
Reference in New Issue
Block a user