feat: agregar secciones de vida útil y depreciación a la gestión de activos
This commit is contained in:
parent
0b1001b8d1
commit
817e6c76c4
@ -0,0 +1,114 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue';
|
||||
import Card from 'primevue/card';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import Select from 'primevue/select';
|
||||
import type { FixedAssetFormData, FiscalDepreciationRateOption } from '../../types/fixedAsset';
|
||||
import fixedAssetsService from '../../services/fixedAssetsService';
|
||||
|
||||
interface Props {
|
||||
form: FixedAssetFormData;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const fiscalRates = ref<FiscalDepreciationRateOption[]>([]);
|
||||
const loadingRates = ref(false);
|
||||
|
||||
const depreciationOptions = [
|
||||
{ label: 'Línea Recta', value: 'straight_line' },
|
||||
{ label: 'Saldo Decreciente', value: 'declining_balance' },
|
||||
{ label: 'Doble Saldo Decreciente', value: 'double_declining_balance' },
|
||||
];
|
||||
|
||||
const selectedRate = computed<FiscalDepreciationRateOption | null>(
|
||||
() => fiscalRates.value.find(r => r.id === props.form.fiscal_depreciation_rate_id) ?? null
|
||||
);
|
||||
|
||||
const implicitLife = computed(() => {
|
||||
if (!selectedRate.value) return null;
|
||||
const rate = parseFloat(selectedRate.value.depreciation_rate);
|
||||
if (!rate || rate <= 0) return null;
|
||||
const years = 100 / rate;
|
||||
const months = Math.round(years * 12);
|
||||
return { years: Math.round(years * 10) / 10, months };
|
||||
});
|
||||
|
||||
const fetchRates = async () => {
|
||||
loadingRates.value = true;
|
||||
try {
|
||||
fiscalRates.value = await fixedAssetsService.getFiscalDepreciationRates();
|
||||
} finally {
|
||||
loadingRates.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(fetchRates);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card class="shadow-sm">
|
||||
<template #title>
|
||||
<div class="flex items-center gap-2 text-xl">
|
||||
<i class="pi pi-chart-line text-primary"></i>
|
||||
<span>Depreciación</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
|
||||
Método de Depreciación
|
||||
</label>
|
||||
<Select
|
||||
v-model="form.depreciation_method"
|
||||
:options="depreciationOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
|
||||
Tasa de Depreciación Fiscal (SAT)
|
||||
</label>
|
||||
<Select
|
||||
v-model="form.fiscal_depreciation_rate_id"
|
||||
:options="fiscalRates"
|
||||
optionLabel="name"
|
||||
optionValue="id"
|
||||
placeholder="Seleccionar tasa SAT (opcional)"
|
||||
:loading="loadingRates"
|
||||
showClear
|
||||
filter
|
||||
class="w-full"
|
||||
/>
|
||||
<div
|
||||
v-if="implicitLife"
|
||||
class="rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-700 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-300"
|
||||
>
|
||||
<i class="pi pi-info-circle mr-1"></i>
|
||||
Vida útil implícita:
|
||||
<span class="font-semibold">{{ implicitLife.years }} años / {{ implicitLife.months }} meses</span>
|
||||
<span class="ml-1 text-amber-500 dark:text-amber-400">({{ selectedRate!.depreciation_rate }}% anual)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
|
||||
Valor Residual
|
||||
</label>
|
||||
<InputNumber
|
||||
v-model="form.residual_value"
|
||||
mode="currency"
|
||||
currency="MXN"
|
||||
locale="es-MX"
|
||||
:min="0"
|
||||
class="w-full"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
@ -6,7 +6,8 @@ import Toast from 'primevue/toast';
|
||||
import Button from 'primevue/button';
|
||||
import Card from 'primevue/card';
|
||||
import FixedAssetGeneralInfoSection from './FixedAssetGeneralInfoSection.vue';
|
||||
import FixedAssetAcquisitionSection from './FixedAssetAcquisitionSection.vue';
|
||||
import FixedAssetUsefulLifeSection from './FixedAssetUsefulLifeSection.vue';
|
||||
import FixedAssetDepreciationSection from './FixedAssetDepreciationSection.vue';
|
||||
import FixedAssetAssignmentSection from './FixedAssetAssignmentSection.vue';
|
||||
import fixedAssetsService from '../../services/fixedAssetsService';
|
||||
import type { FixedAssetFormData } from '../../types/fixedAsset';
|
||||
@ -23,6 +24,8 @@ const currentProductName = ref('');
|
||||
|
||||
const form = ref<FixedAssetFormData>({
|
||||
inventory_warehouse_id: null,
|
||||
useful_life_catalog_id: null,
|
||||
fiscal_depreciation_rate_id: null,
|
||||
estimated_useful_life: null,
|
||||
depreciation_method: 'straight_line',
|
||||
residual_value: null,
|
||||
@ -39,6 +42,8 @@ const loadAsset = async () => {
|
||||
const asset = response.data.data;
|
||||
form.value = {
|
||||
inventory_warehouse_id: asset.inventory_warehouse?.id ?? null,
|
||||
useful_life_catalog_id: asset.useful_life_catalog_id ?? null,
|
||||
fiscal_depreciation_rate_id: asset.fiscal_depreciation_rate_id ?? null,
|
||||
estimated_useful_life: asset.estimated_useful_life,
|
||||
depreciation_method: asset.depreciation_method ?? 'straight_line',
|
||||
residual_value: asset.residual_value ? parseFloat(asset.residual_value) : null,
|
||||
@ -67,8 +72,8 @@ const saveAsset = async () => {
|
||||
toast.add({ severity: 'warn', summary: 'Campo requerido', detail: 'Selecciona un producto del almacén.', life: 3000 });
|
||||
return;
|
||||
}
|
||||
if (!form.value.estimated_useful_life) {
|
||||
toast.add({ severity: 'warn', summary: 'Campo requerido', detail: 'Indica la vida útil estimada.', life: 3000 });
|
||||
if (!form.value.estimated_useful_life && !form.value.useful_life_catalog_id) {
|
||||
toast.add({ severity: 'warn', summary: 'Campo requerido', detail: 'Indica la vida útil estimada o selecciona un catálogo.', life: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
@ -80,6 +85,8 @@ const saveAsset = async () => {
|
||||
depreciation_method: form.value.depreciation_method || 'straight_line',
|
||||
};
|
||||
|
||||
if (form.value.useful_life_catalog_id != null) payload.useful_life_catalog_id = form.value.useful_life_catalog_id;
|
||||
if (form.value.fiscal_depreciation_rate_id != null) payload.fiscal_depreciation_rate_id = form.value.fiscal_depreciation_rate_id;
|
||||
if (form.value.residual_value != null) payload.residual_value = form.value.residual_value;
|
||||
if (form.value.asset_tag) payload.asset_tag = form.value.asset_tag;
|
||||
if (form.value.warranty_days != null) payload.warranty_days = form.value.warranty_days;
|
||||
@ -139,8 +146,9 @@ const saveAsset = async () => {
|
||||
</Card>
|
||||
|
||||
<div class="grid grid-cols-1 gap-5 xl:grid-cols-2">
|
||||
<FixedAssetAcquisitionSection :form="form" />
|
||||
<FixedAssetAssignmentSection :form="form" />
|
||||
<FixedAssetUsefulLifeSection :form="form" />
|
||||
<FixedAssetDepreciationSection :form="form" />
|
||||
<FixedAssetAssignmentSection :form="form" class="xl:col-span-2" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap items-center justify-end gap-3">
|
||||
|
||||
@ -0,0 +1,106 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue';
|
||||
import Card from 'primevue/card';
|
||||
import InputNumber from 'primevue/inputnumber';
|
||||
import Select from 'primevue/select';
|
||||
import type { FixedAssetFormData, UsefulLifeCatalogOption } from '../../types/fixedAsset';
|
||||
import fixedAssetsService from '../../services/fixedAssetsService';
|
||||
|
||||
interface Props {
|
||||
form: FixedAssetFormData;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const catalogs = ref<UsefulLifeCatalogOption[]>([]);
|
||||
const loadingCatalogs = ref(false);
|
||||
const autoCalculated = ref(false);
|
||||
|
||||
const selectedCatalog = computed<UsefulLifeCatalogOption | null>(
|
||||
() => catalogs.value.find(c => c.id === props.form.useful_life_catalog_id) ?? null
|
||||
);
|
||||
|
||||
const fetchCatalogs = async () => {
|
||||
loadingCatalogs.value = true;
|
||||
try {
|
||||
catalogs.value = await fixedAssetsService.getUsefulLifeCatalog();
|
||||
} finally {
|
||||
loadingCatalogs.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
watch(() => props.form.useful_life_catalog_id, async (id) => {
|
||||
if (!id) {
|
||||
autoCalculated.value = false;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const result = await fixedAssetsService.calculateUsefulLife({ useful_life_catalog_id: id });
|
||||
if (result.from_useful_life_catalog) {
|
||||
props.form.estimated_useful_life = result.from_useful_life_catalog.months;
|
||||
autoCalculated.value = true;
|
||||
}
|
||||
} catch {
|
||||
autoCalculated.value = false;
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(fetchCatalogs);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Card class="shadow-sm">
|
||||
<template #title>
|
||||
<div class="flex items-center gap-2 text-xl">
|
||||
<i class="pi pi-calendar text-primary"></i>
|
||||
<span>Vida Útil</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
|
||||
Catálogo de Vida Útil
|
||||
</label>
|
||||
<Select
|
||||
v-model="form.useful_life_catalog_id"
|
||||
:options="catalogs"
|
||||
optionLabel="name"
|
||||
optionValue="id"
|
||||
placeholder="Seleccionar catálogo (opcional)"
|
||||
:loading="loadingCatalogs"
|
||||
showClear
|
||||
filter
|
||||
class="w-full"
|
||||
/>
|
||||
<div
|
||||
v-if="selectedCatalog"
|
||||
class="rounded-lg border border-primary-200 bg-primary-50 px-3 py-2 text-sm text-primary-700 dark:border-primary-800 dark:bg-primary-950 dark:text-primary-300"
|
||||
>
|
||||
<span class="font-semibold">{{ selectedCatalog.min_years }}–{{ selectedCatalog.max_years }} años</span>
|
||||
<span v-if="selectedCatalog.practical_comment" class="ml-2 text-primary-600 dark:text-primary-400">
|
||||
— {{ selectedCatalog.practical_comment }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<label class="text-sm font-semibold text-surface-800 dark:text-surface-100">
|
||||
Vida Útil Estimada (meses) *
|
||||
</label>
|
||||
<InputNumber
|
||||
v-model="form.estimated_useful_life"
|
||||
:min="1"
|
||||
suffix=" meses"
|
||||
class="w-full"
|
||||
placeholder="Ej: 60"
|
||||
/>
|
||||
<p v-if="autoCalculated" class="flex items-center gap-1 text-xs text-green-600 dark:text-green-400">
|
||||
<i class="pi pi-check-circle"></i>
|
||||
Calculado automáticamente desde el catálogo. Puedes ajustarlo.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</template>
|
||||
@ -1,4 +1,5 @@
|
||||
import api from '../../../services/api';
|
||||
import type { UsefulLifeCatalogOption, FiscalDepreciationRateOption, UsefulLifeCalculationResult } from '../types/fixedAsset';
|
||||
|
||||
export interface Asset {
|
||||
id: number;
|
||||
@ -12,6 +13,10 @@ export interface Asset {
|
||||
book_value: string;
|
||||
warranty_days: number | null;
|
||||
warranty_end_date: string | null;
|
||||
fiscal_depreciation_rate_id: number | null;
|
||||
useful_life_catalog_id: number | null;
|
||||
fiscal_depreciation_rate: { id: number; name: string; depreciation_rate: string } | null;
|
||||
useful_life_catalog: { id: number; name: string; min_years: number; max_years: number } | null;
|
||||
inventory_warehouse: {
|
||||
id: number;
|
||||
product: {
|
||||
@ -207,6 +212,21 @@ class FixedAssetsService {
|
||||
}));
|
||||
}
|
||||
|
||||
async getUsefulLifeCatalog(): Promise<UsefulLifeCatalogOption[]> {
|
||||
const response = await api.get('/api/catalogs/useful-life-catalog', { params: { paginate: false } });
|
||||
return response.data.data.data;
|
||||
}
|
||||
|
||||
async getFiscalDepreciationRates(): Promise<FiscalDepreciationRateOption[]> {
|
||||
const response = await api.get('/api/catalogs/fiscal-depreciation-rates', { params: { paginate: false } });
|
||||
return response.data.data.data;
|
||||
}
|
||||
|
||||
async calculateUsefulLife(params: { useful_life_catalog_id?: number }): Promise<UsefulLifeCalculationResult> {
|
||||
const response = await api.get('/api/assets/options/calculate-useful-life', { params });
|
||||
return response.data.data;
|
||||
}
|
||||
|
||||
async downloadResguardo(assetId: number, assignmentId: number): Promise<void> {
|
||||
const response = await api.get(
|
||||
`/api/assets/${assetId}/assignments/${assignmentId}/resguardo`,
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
export interface FixedAssetFormData {
|
||||
inventory_warehouse_id: number | null;
|
||||
useful_life_catalog_id: number | null;
|
||||
fiscal_depreciation_rate_id: number | null;
|
||||
estimated_useful_life: number | null;
|
||||
depreciation_method: string;
|
||||
residual_value: number | null;
|
||||
@ -8,6 +10,27 @@ export interface FixedAssetFormData {
|
||||
warranty_end_date: string;
|
||||
}
|
||||
|
||||
export interface UsefulLifeCatalogOption {
|
||||
id: number;
|
||||
name: string;
|
||||
min_years: number;
|
||||
max_years: number;
|
||||
practical_comment: string | null;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
export interface FiscalDepreciationRateOption {
|
||||
id: number;
|
||||
name: string;
|
||||
depreciation_rate: string;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
export interface UsefulLifeCalculationResult {
|
||||
from_useful_life_catalog?: { months: number; label: string };
|
||||
from_fiscal_rate?: { months: number; label: string };
|
||||
}
|
||||
|
||||
export interface InventoryWarehouseOption {
|
||||
id: number;
|
||||
serial_number: string | null;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user