feature/reportes
This commit is contained in:
@@ -519,6 +519,12 @@ async function fetchBlob(endpoint: string, options: RequestInit = {}): Promise<B
|
||||
export const api = {
|
||||
get: <T = any>(endpoint: string) => fetchApi<T>(endpoint, { method: 'GET' }),
|
||||
getBlob: (endpoint: string) => fetchBlob(endpoint, { method: 'GET' }),
|
||||
postBlob: (endpoint: string, body: any) =>
|
||||
fetchBlob(endpoint, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body)
|
||||
}),
|
||||
|
||||
post: <T = any>(endpoint: string, body: any, options: RequestInit = {}) =>
|
||||
fetchApi<T>(endpoint, {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import { api } from '$lib/api';
|
||||
|
||||
export interface DownloadedPartsReportSection {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface DownloadedPartsReportBootstrap {
|
||||
report_key: string;
|
||||
title: string;
|
||||
description: string;
|
||||
company_id: number;
|
||||
tenant_id: number;
|
||||
status: string;
|
||||
available_filters: string[];
|
||||
next_steps: string[];
|
||||
sections: DownloadedPartsReportSection[];
|
||||
}
|
||||
|
||||
export interface DownloadedPartsReportRequest {
|
||||
date_from: string;
|
||||
date_to: string;
|
||||
class_from?: string;
|
||||
class_to?: string;
|
||||
print_class_mode: 'exported' | 'downloaded';
|
||||
exchange_rate_mode: 'invoice' | 'pedimento_payment';
|
||||
currency_mode: 'dollars' | 'pesos' | 'both';
|
||||
temporality_mode: 'temporales' | 'definitivos' | 'ambos';
|
||||
weight_type_mode: 'kilos' | 'libras' | 'ambos';
|
||||
operation_mode: 'importacion' | 'exportacion';
|
||||
material_type?: string;
|
||||
invoice_type?: string;
|
||||
parts?: string[];
|
||||
pedimento_key?: string;
|
||||
provider_id?: number;
|
||||
sold_to_id?: number;
|
||||
shipped_to_id?: number;
|
||||
destination_customs?: string;
|
||||
include_series: boolean;
|
||||
print_class_total: boolean;
|
||||
include_totals_by_fraction: boolean;
|
||||
julian_date: boolean;
|
||||
show_item_description: boolean;
|
||||
include_exempt_fraction: boolean;
|
||||
show_export_fraction: boolean;
|
||||
include_rule_octava: boolean;
|
||||
include_american_fraction_and_country: boolean;
|
||||
respect_import_invoice_value_in_pesos: boolean;
|
||||
show_all_temporary_balances: boolean;
|
||||
}
|
||||
|
||||
export const downloadedPartsReportsApi = {
|
||||
getBootstrap: (companyId: number) =>
|
||||
api.get<DownloadedPartsReportBootstrap>(
|
||||
`/v1/a76/reports/exportacion/partes-descargadas/bootstrap?company_id=${companyId}`
|
||||
),
|
||||
|
||||
generate: async (
|
||||
companyId: number,
|
||||
params: DownloadedPartsReportRequest
|
||||
): Promise<void> => {
|
||||
const blob = await api.postBlob(
|
||||
`/v1/a76/reports/exportacion/partes-descargadas/generate?company_id=${companyId}`,
|
||||
params
|
||||
);
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `partes_descargadas_${params.date_from}_${params.date_to}.csv`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
a.remove();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
};
|
||||
@@ -3,13 +3,15 @@
|
||||
import * as Dialog from '$lib/components/ui/dialog';
|
||||
import { Input } from '$lib/components/ui/input';
|
||||
import { Label } from '$lib/components/ui/label';
|
||||
import { FolderSearch, Scale } from 'lucide-svelte';
|
||||
import UnitMeasureSelectorDialog from '$lib/components/dashboard/goods/modales/unit-measure-dialog.svelte';
|
||||
import { companyStore } from '$lib/stores/company.svelte';
|
||||
import { Scale } from 'lucide-svelte';
|
||||
import {
|
||||
createEquivalencyItem,
|
||||
updateEquivalencyItem,
|
||||
type EquivalencyItem
|
||||
} from '$lib/api/dashboard/a76/general_catalogs/equivalencies';
|
||||
import type { UnitOfMeasure } from '$lib/api/dashboard/a76/general_catalogs/units-of-measure';
|
||||
|
||||
let {
|
||||
open = $bindable(false),
|
||||
@@ -37,25 +39,33 @@
|
||||
|
||||
let loading = $state(false);
|
||||
let error = $state<string | null>(null);
|
||||
let showOriginalModal = $state(false);
|
||||
let showExternalModal = $state(false);
|
||||
|
||||
$effect(() => {
|
||||
if (!open) return;
|
||||
|
||||
if (item) {
|
||||
formData = {
|
||||
original_field: item.original_field || '',
|
||||
external_field: item.external_field || ''
|
||||
};
|
||||
formData.original_field = item.original_field || '';
|
||||
formData.external_field = item.external_field || '';
|
||||
} else {
|
||||
formData = {
|
||||
original_field: defaultOriginalField ?? '',
|
||||
external_field: ''
|
||||
};
|
||||
formData.original_field = defaultOriginalField ?? '';
|
||||
formData.external_field = '';
|
||||
}
|
||||
|
||||
error = null;
|
||||
});
|
||||
|
||||
function handleSelectOriginal(unit: UnitOfMeasure) {
|
||||
formData.original_field = unit.code;
|
||||
showOriginalModal = false;
|
||||
}
|
||||
|
||||
function handleSelectExternal(unit: UnitOfMeasure) {
|
||||
formData.external_field = unit.code;
|
||||
showExternalModal = false;
|
||||
}
|
||||
|
||||
async function handleSubmit() {
|
||||
const companyId = companyStore.activeCompany?.id;
|
||||
if (!companyId) {
|
||||
@@ -113,40 +123,64 @@
|
||||
|
||||
<div class="grid gap-4 py-4">
|
||||
<div class="grid gap-2">
|
||||
<Label for="from_unit_code">
|
||||
<Label for="original_field">
|
||||
Campo Original <span class="text-destructive">*</span>
|
||||
</Label>
|
||||
<div class="flex gap-2">
|
||||
<div class="relative w-full">
|
||||
<Scale class="absolute top-2.5 left-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="from_unit_code"
|
||||
bind:value={formData.original_field}
|
||||
class="pl-9 font-mono"
|
||||
placeholder="Ej: PZA, KGM..."
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
id="original_field"
|
||||
bind:value={formData.original_field}
|
||||
readonly
|
||||
onclick={() => (showOriginalModal = true)}
|
||||
class="cursor-pointer pl-9 font-mono"
|
||||
placeholder="Seleccione..."
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
type="button"
|
||||
onclick={() => (showOriginalModal = true)}
|
||||
disabled={loading}
|
||||
class="shrink-0"
|
||||
>
|
||||
<FolderSearch class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="to_unit_code">
|
||||
<Label for="external_field">
|
||||
Campo Exterior <span class="text-destructive">*</span>
|
||||
</Label>
|
||||
<div class="flex gap-2">
|
||||
<div class="relative w-full">
|
||||
<Scale class="absolute top-2.5 left-3 h-4 w-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="to_unit_code"
|
||||
bind:value={formData.external_field}
|
||||
class="pl-9 font-mono"
|
||||
placeholder="Ej: PIEZAS, KGS..."
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
<Input
|
||||
id="external_field"
|
||||
bind:value={formData.external_field}
|
||||
readonly
|
||||
onclick={() => (showExternalModal = true)}
|
||||
class="cursor-pointer pl-9 font-mono"
|
||||
placeholder="Seleccione..."
|
||||
disabled={loading}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
type="button"
|
||||
onclick={() => (showExternalModal = true)}
|
||||
disabled={loading}
|
||||
class="shrink-0"
|
||||
>
|
||||
<FolderSearch class="h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,3 +194,6 @@
|
||||
</form>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<UnitMeasureSelectorDialog bind:open={showOriginalModal} onSelect={handleSelectOriginal} />
|
||||
<UnitMeasureSelectorDialog bind:open={showExternalModal} onSelect={handleSelectExternal} />
|
||||
|
||||
@@ -33,13 +33,9 @@
|
||||
operation_type: operationType,
|
||||
invoice_number: searchTerm || undefined
|
||||
};
|
||||
if (operationType === 'imp' && regimen) {
|
||||
if (regimen === 'Temporal' || regimen === 'TEMPORAL SCAF') {
|
||||
filters.invoice_type = 'TEM';
|
||||
} else if (regimen === 'Definitiva' || regimen === 'DEFINITIVO SCAF') {
|
||||
filters.invoice_type = 'DEF';
|
||||
}
|
||||
}
|
||||
// Do NOT filter by invoice_type here: restricting to TEM or DEF based on
|
||||
// the current movement_type_import value would hide valid invoices of the
|
||||
// other type. Let the user search freely and pick the right one.
|
||||
|
||||
const res = await invoicesApi.list(companyStore.activeCompany.id, 1, 50, filters);
|
||||
if (res.data) {
|
||||
@@ -134,7 +130,8 @@
|
||||
{invoice.invoice_number}
|
||||
</td>
|
||||
<td class="max-w-[400px] truncate p-3 text-muted-foreground italic">
|
||||
{invoice.compliance_mx?.pedimento_r1 ||
|
||||
{invoice.compliance_mx?.pedimento?.pedimento_number ||
|
||||
invoice.compliance_mx?.pedimento_r1 ||
|
||||
invoice.compliance_mx?.pedimento_id ||
|
||||
'-'}
|
||||
</td>
|
||||
|
||||
@@ -164,10 +164,12 @@
|
||||
if (showLinkToImportBlock && num && !selectedImportInvoiceId && !loadingImportLines) {
|
||||
(async () => {
|
||||
try {
|
||||
const res = await invoicesApi.list(companyStore.activeCompany!.id, 1, 5, {
|
||||
// Do NOT filter by invoice_type: if movement_type_import is null or
|
||||
// mismatched the invoice won't be found, leaving the line picker
|
||||
// permanently disabled. Exact match is enforced by .find() below.
|
||||
const res = await invoicesApi.list(companyStore.activeCompany!.id, 1, 50, {
|
||||
operation_type: 'imp',
|
||||
invoice_number: num,
|
||||
invoice_type: movementType === 'DEF' ? 'DEF' : 'TEM'
|
||||
invoice_number: num
|
||||
});
|
||||
const items = res.data?.items ?? [];
|
||||
const inv = items.find((i: Invoice) => i.invoice_number === num);
|
||||
@@ -183,7 +185,7 @@
|
||||
if (showRepairBlock && num && !selectedExportInvoiceId && !loadingExportLines) {
|
||||
(async () => {
|
||||
try {
|
||||
const res = await invoicesApi.list(companyStore.activeCompany!.id, 1, 5, {
|
||||
const res = await invoicesApi.list(companyStore.activeCompany!.id, 1, 50, {
|
||||
operation_type: 'exp',
|
||||
invoice_number: num
|
||||
});
|
||||
@@ -735,7 +737,7 @@
|
||||
<Dialog.Header>
|
||||
<Dialog.Title class="text-sm">Seleccionar línea de importación</Dialog.Title>
|
||||
<p class="text-xs text-muted-foreground mt-0.5">
|
||||
Solo se muestran líneas con saldo disponible
|
||||
Líneas sin saldo disponible se muestran en gris.
|
||||
</p>
|
||||
</Dialog.Header>
|
||||
<!-- overflow-x on a wrapper that does NOT also do overflow-y.
|
||||
@@ -743,9 +745,9 @@
|
||||
independently from the horizontal scrollbar. -->
|
||||
<div class="overflow-x-auto">
|
||||
<div class="overflow-y-auto max-h-[500px]">
|
||||
{#if importInvoiceLines.every(l => !l.has_balance)}
|
||||
{#if importInvoiceLines.length === 0}
|
||||
<p class="px-3 py-8 text-xs text-muted-foreground text-center">
|
||||
No hay líneas con saldo disponible en esta factura.
|
||||
No hay líneas en esta factura.
|
||||
</p>
|
||||
{:else}
|
||||
<table class="text-xs border-collapse" style="min-width: max-content; width: 100%;">
|
||||
@@ -768,9 +770,8 @@
|
||||
</thead>
|
||||
<tbody class="divide-y divide-border">
|
||||
{#each importInvoiceLines as lineItem}
|
||||
{#if lineItem.has_balance}
|
||||
<tr
|
||||
class="hover:bg-muted/50 cursor-pointer transition-colors group"
|
||||
class="{lineItem.has_balance ? 'hover:bg-muted/50 cursor-pointer' : 'opacity-50 cursor-default'} transition-colors group"
|
||||
onclick={() => {
|
||||
editingItem.fa_data = editingItem.fa_data || {};
|
||||
editingItem.fa_data.search_line = lineItem.line_number;
|
||||
@@ -830,7 +831,6 @@
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -507,6 +507,10 @@ export function getSidebarData(): SidebarData {
|
||||
title: "Facturas Impo/Expo",
|
||||
url: "/dashboard/reports/invoices",
|
||||
},
|
||||
{
|
||||
title: "Partes descargadas",
|
||||
url: "/dashboard/reports/partes-descargadas",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user