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 Button from 'primevue/button';
|
||||||
import Card from 'primevue/card';
|
import Card from 'primevue/card';
|
||||||
import FixedAssetGeneralInfoSection from './FixedAssetGeneralInfoSection.vue';
|
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 FixedAssetAssignmentSection from './FixedAssetAssignmentSection.vue';
|
||||||
import fixedAssetsService from '../../services/fixedAssetsService';
|
import fixedAssetsService from '../../services/fixedAssetsService';
|
||||||
import type { FixedAssetFormData } from '../../types/fixedAsset';
|
import type { FixedAssetFormData } from '../../types/fixedAsset';
|
||||||
@ -23,6 +24,8 @@ const currentProductName = ref('');
|
|||||||
|
|
||||||
const form = ref<FixedAssetFormData>({
|
const form = ref<FixedAssetFormData>({
|
||||||
inventory_warehouse_id: null,
|
inventory_warehouse_id: null,
|
||||||
|
useful_life_catalog_id: null,
|
||||||
|
fiscal_depreciation_rate_id: null,
|
||||||
estimated_useful_life: null,
|
estimated_useful_life: null,
|
||||||
depreciation_method: 'straight_line',
|
depreciation_method: 'straight_line',
|
||||||
residual_value: null,
|
residual_value: null,
|
||||||
@ -39,6 +42,8 @@ const loadAsset = async () => {
|
|||||||
const asset = response.data.data;
|
const asset = response.data.data;
|
||||||
form.value = {
|
form.value = {
|
||||||
inventory_warehouse_id: asset.inventory_warehouse?.id ?? null,
|
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,
|
estimated_useful_life: asset.estimated_useful_life,
|
||||||
depreciation_method: asset.depreciation_method ?? 'straight_line',
|
depreciation_method: asset.depreciation_method ?? 'straight_line',
|
||||||
residual_value: asset.residual_value ? parseFloat(asset.residual_value) : null,
|
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 });
|
toast.add({ severity: 'warn', summary: 'Campo requerido', detail: 'Selecciona un producto del almacén.', life: 3000 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!form.value.estimated_useful_life) {
|
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.', life: 3000 });
|
toast.add({ severity: 'warn', summary: 'Campo requerido', detail: 'Indica la vida útil estimada o selecciona un catálogo.', life: 3000 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,6 +85,8 @@ const saveAsset = async () => {
|
|||||||
depreciation_method: form.value.depreciation_method || 'straight_line',
|
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.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.asset_tag) payload.asset_tag = form.value.asset_tag;
|
||||||
if (form.value.warranty_days != null) payload.warranty_days = form.value.warranty_days;
|
if (form.value.warranty_days != null) payload.warranty_days = form.value.warranty_days;
|
||||||
@ -139,8 +146,9 @@ const saveAsset = async () => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
<div class="grid grid-cols-1 gap-5 xl:grid-cols-2">
|
<div class="grid grid-cols-1 gap-5 xl:grid-cols-2">
|
||||||
<FixedAssetAcquisitionSection :form="form" />
|
<FixedAssetUsefulLifeSection :form="form" />
|
||||||
<FixedAssetAssignmentSection :form="form" />
|
<FixedAssetDepreciationSection :form="form" />
|
||||||
|
<FixedAssetAssignmentSection :form="form" class="xl:col-span-2" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex flex-wrap items-center justify-end gap-3">
|
<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 api from '../../../services/api';
|
||||||
|
import type { UsefulLifeCatalogOption, FiscalDepreciationRateOption, UsefulLifeCalculationResult } from '../types/fixedAsset';
|
||||||
|
|
||||||
export interface Asset {
|
export interface Asset {
|
||||||
id: number;
|
id: number;
|
||||||
@ -12,6 +13,10 @@ export interface Asset {
|
|||||||
book_value: string;
|
book_value: string;
|
||||||
warranty_days: number | null;
|
warranty_days: number | null;
|
||||||
warranty_end_date: string | 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: {
|
inventory_warehouse: {
|
||||||
id: number;
|
id: number;
|
||||||
product: {
|
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> {
|
async downloadResguardo(assetId: number, assignmentId: number): Promise<void> {
|
||||||
const response = await api.get(
|
const response = await api.get(
|
||||||
`/api/assets/${assetId}/assignments/${assignmentId}/resguardo`,
|
`/api/assets/${assetId}/assignments/${assignmentId}/resguardo`,
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
export interface FixedAssetFormData {
|
export interface FixedAssetFormData {
|
||||||
inventory_warehouse_id: number | null;
|
inventory_warehouse_id: number | null;
|
||||||
|
useful_life_catalog_id: number | null;
|
||||||
|
fiscal_depreciation_rate_id: number | null;
|
||||||
estimated_useful_life: number | null;
|
estimated_useful_life: number | null;
|
||||||
depreciation_method: string;
|
depreciation_method: string;
|
||||||
residual_value: number | null;
|
residual_value: number | null;
|
||||||
@ -8,6 +10,27 @@ export interface FixedAssetFormData {
|
|||||||
warranty_end_date: string;
|
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 {
|
export interface InventoryWarehouseOption {
|
||||||
id: number;
|
id: number;
|
||||||
serial_number: string | null;
|
serial_number: string | null;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user