- |
+ |
{
-
-
-
-
-
- Eliminar Número de Serie
-
-
-
-
-
-
- ¿Estás seguro de eliminar el número de serie
-
- {{ deletingSerial?.serial_number }}
- ?
-
-
- Esta acción no se puede deshacer y reducirá el stock del producto.
-
-
-
-
-
-
-
-
-
diff --git a/src/pages/POS/Movements/Edit.vue b/src/pages/POS/Movements/Edit.vue
index b12b472..e645e1b 100644
--- a/src/pages/POS/Movements/Edit.vue
+++ b/src/pages/POS/Movements/Edit.vue
@@ -7,6 +7,7 @@ import Modal from '@Holos/Modal.vue';
import FormInput from '@Holos/Form/Input.vue';
import FormError from '@Holos/Form/Elements/Error.vue';
import GoogleIcon from '@Shared/GoogleIcon.vue';
+import SerialInputList from '@Components/POS/SerialInputList.vue';
/** Eventos */
const emit = defineEmits(['close', 'updated']);
@@ -21,6 +22,8 @@ const props = defineProps({
const warehouses = ref([]);
const suppliers = ref([]);
const loading = ref(false);
+const serialsText = ref('');
+const serialNumbers = ref([]);
const api = useApi();
/** Formulario */
@@ -37,7 +40,7 @@ const form = useForm({
});
/** Estado para manejo de seriales */
-const serialsText = ref(''); // Texto editable (uno por línea)
+const serialsList = ref([]); // Array de { serial_number, locked }
/** Computed */
const movementTypeInfo = computed(() => {
@@ -80,9 +83,8 @@ const hasSerials = computed(() => {
});
const serialsArray = computed(() => {
- return serialsText.value
- .split('\n')
- .map(s => s.trim())
+ return serialsList.value
+ .map(s => s.serial_number.trim())
.filter(s => s.length > 0);
});
@@ -174,19 +176,9 @@ const updateMovement = () => {
data.warehouse_to_id = form.destination_warehouse_id;
}
- console.log('📤 Datos a enviar al backend:', {
- movement_id: props.movement.id,
- data: data,
- serial_numbers_count: data.serial_numbers?.length || 0,
- hasSerials: hasSerials.value
- });
-
api.put(apiURL(`movimientos/${props.movement.id}`), {
data,
onSuccess: (response) => {
- console.log('✅ Respuesta del backend (completa):', JSON.stringify(response, null, 2));
- console.log('✅ Tipo de respuesta:', typeof response);
- console.log('✅ Keys:', Object.keys(response || {}));
window.Notify.success('Movimiento actualizado correctamente');
emit('updated');
closeModal();
@@ -219,18 +211,18 @@ watch(() => props.show, (isShown) => {
form.notes = props.movement.notes || '';
// Cargar números de serie si existen
- let serialNumbers = [];
-
- if (props.movement.serial_numbers && props.movement.serial_numbers.length > 0) {
- serialNumbers = props.movement.serial_numbers;
- } else if (props.movement.serials && props.movement.serials.length > 0) {
- serialNumbers = props.movement.serials.map(s => s.serial_number);
- }
-
- if (serialNumbers.length > 0) {
- serialsText.value = serialNumbers.join('\n');
+ if (props.movement.serials && props.movement.serials.length > 0) {
+ serialsList.value = props.movement.serials.map(s => ({
+ serial_number: s.serial_number,
+ locked: s.status === 'vendido'
+ }));
+ } else if (props.movement.serial_numbers && props.movement.serial_numbers.length > 0) {
+ serialsList.value = props.movement.serial_numbers.map(sn => ({
+ serial_number: sn,
+ locked: false
+ }));
} else {
- serialsText.value = '';
+ serialsList.value = [{ serial_number: '', locked: false }];
}
// Almacenes según tipo
@@ -242,15 +234,6 @@ watch(() => props.show, (isShown) => {
form.origin_warehouse_id = props.movement.origin_warehouse_id || '';
form.destination_warehouse_id = props.movement.destination_warehouse_id || '';
}
- console.log('🔍 Debug Movement:', {
- movement: props.movement,
- has_inventory: !!props.movement.inventory,
- track_serials: props.movement?.inventory?.track_serials,
- serials_relation: props.movement.serials,
- hasSerials_computed: hasSerials.value,
- serialNumbers_loaded: serialNumbers,
- serialsText_final: serialsText.value
- });
}
} else {
// Limpiar seriales al cerrar
@@ -338,26 +321,15 @@ watch(() => props.show, (isShown) => {
NÚMEROS DE SERIE
-
- Ingresa un número de serie por línea. Debe coincidir con la cantidad ({{ form.quantity }}).
-
-
+
-
+
{{ serialsArray.length }} de {{ form.quantity }} seriales
{{ serialsValidation.message }}
diff --git a/src/pages/POS/Movements/EntryModal.vue b/src/pages/POS/Movements/EntryModal.vue
index 0c2a260..19d636c 100644
--- a/src/pages/POS/Movements/EntryModal.vue
+++ b/src/pages/POS/Movements/EntryModal.vue
@@ -7,6 +7,7 @@ import Modal from '@Holos/Modal.vue';
import FormInput from '@Holos/Form/Input.vue';
import FormError from '@Holos/Form/Elements/Error.vue';
import GoogleIcon from '@Shared/GoogleIcon.vue';
+import SerialInputList from '@Components/POS/SerialInputList.vue';
/** Eventos */
const emit = defineEmits(['close', 'created']);
@@ -101,7 +102,7 @@ const addProduct = () => {
track_serials: false,
unit_of_measure: null,
allows_decimals: false,
- serial_numbers_text: '', // Texto con seriales separados por líneas
+ serial_numbers_list: [{ serial_number: '', locked: false }], // Inputs individuales de seriales
serial_validation_error: ''
});
};
@@ -116,7 +117,7 @@ const onProductInput = (index) => {
productNotFound.value = false;
const searchValue = productSearch.value?.trim();
- if (!searchValue || searchValue.length < 2) {
+ if (!searchValue || searchValue.length < 1) {
productSuggestions.value = [];
showProductSuggestions.value = false;
return;
@@ -180,7 +181,7 @@ const selectProduct = (product) => {
// Limpiar seriales si la unidad permite decimales
if (product.unit_of_measure?.allows_decimals) {
- selectedProducts.value[currentSearchIndex.value].serial_numbers_text = '';
+ selectedProducts.value[currentSearchIndex.value].serial_numbers_list = [{ serial_number: '', locked: false }];
}
}
@@ -195,30 +196,22 @@ const selectProduct = (product) => {
// Contar seriales ingresados (solo para mostrar feedback visual)
const countSerials = (item) => {
- if (!item.serial_numbers_text) return 0;
- const serials = item.serial_numbers_text
- .split('\n')
- .map(s => s.trim())
- .filter(s => s.length > 0)
- .filter((s, index, self) => self.indexOf(s) === index); // Eliminar duplicados (igual que createEntry)
- return serials.length;
+ if (!item.serial_numbers_list) return 0;
+ return item.serial_numbers_list.filter(s => s.serial_number.trim()).length;
};
-// Actualizar cantidad según seriales ingresados
+// Actualizar cantidad según seriales ingresados (llamado por el watcher del v-model)
const updateQuantityFromSerials = (item) => {
// Solo actualizar cantidad automáticamente si el producto requiere seriales y puede usarlos
if (!item.track_serials || !canUseSerials(item)) {
return;
}
- // Contar seriales válidos (sin líneas vacías)
const serialCount = countSerials(item);
- // Actualizar cantidad siempre basado en el conteo actual
if (serialCount > 0) {
item.quantity = serialCount;
} else {
- // Si no hay seriales, resetear a 1
item.quantity = 1;
}
};
@@ -233,14 +226,12 @@ const createEntry = () => {
serial_numbers: [] // Inicializar siempre como array vacío
};
- // Agregar seriales solo si la unidad lo permite y hay texto ingresado
- if (canUseSerials(item) && item.serial_numbers_text && item.serial_numbers_text.trim()) {
- // Limpiar y filtrar seriales - eliminar líneas vacías, espacios, y duplicados
- const serials = item.serial_numbers_text
- .split('\n')
- .map(s => s.trim())
+ // Agregar seriales solo si la unidad lo permite y hay seriales ingresados
+ if (canUseSerials(item) && item.serial_numbers_list) {
+ const serials = item.serial_numbers_list
+ .map(s => s.serial_number.trim())
.filter(s => s.length > 0)
- .filter((s, index, self) => self.indexOf(s) === index); // Eliminar duplicados locales
+ .filter((s, index, self) => self.indexOf(s) === index);
if (serials.length > 0) {
productData.serial_numbers = serials;
@@ -410,7 +401,7 @@ watch(() => form.warehouse_id, (newWarehouseId, oldWarehouseId) => {
:class="{
'border-red-500 focus:ring-red-500 focus:border-red-500': productNotFound && currentSearchIndex === index
}"
- :disabled="searchingProduct"
+ :readonly="searchingProduct"
/>
@@ -507,7 +498,6 @@ watch(() => form.warehouse_id, (newWarehouseId, oldWarehouseId) => {
Números de Serie
*
(opcional)
- - uno por línea, debe coincidir con la cantidad
@@ -520,18 +510,11 @@ watch(() => form.warehouse_id, (newWarehouseId, oldWarehouseId) => {
-
-
-
- {{ countSerials(item) }} serial(es) ingresado(s)
-
+ @update:model-value="updateQuantityFromSerials(item)"
+ />
diff --git a/src/pages/POS/Point.vue b/src/pages/POS/Point.vue
index 06599fc..e6ea2f0 100644
--- a/src/pages/POS/Point.vue
+++ b/src/pages/POS/Point.vue
@@ -17,6 +17,7 @@ import CheckoutModal from '@Components/POS/CheckoutModal.vue';
import ClientModal from '@Components/POS/ClientModal.vue';
import QRscan from '@Components/POS/QRscan.vue';
import SerialSelector from '@Components/POS/SerialSelector.vue';
+import BundleSerialSelector from '@Components/POS/BundleSerialSelector.vue';
/** i18n */
const { t } = useI18n();
@@ -41,6 +42,10 @@ const lastSaleData = ref(null);
const showSerialSelector = ref(false);
const serialSelectorProduct = ref(null);
+// Estado para selector de seriales de bundles
+const showBundleSerialSelector = ref(false);
+const bundleSerialSelectorBundle = ref(null);
+
/** Buscador de productos */
const searcher = useSearcher({
url: apiURL('inventario'),
@@ -110,7 +115,25 @@ const addBundleToCart = async (bundle) => {
return;
}
- // Agregar bundle/kit al carrito
+ // Verificar si el bundle tiene componentes con seriales
+ try {
+ const components = await serialService.getBundleComponents(bundle.id);
+ const hasSerialComponents = components.some(item => {
+ const inv = item.inventory || item.product || {};
+ return inv.track_serials || item.track_serials;
+ });
+
+ if (hasSerialComponents) {
+ // Abrir selector de seriales para el bundle
+ bundleSerialSelectorBundle.value = bundle;
+ showBundleSerialSelector.value = true;
+ return;
+ }
+ } catch {
+ // Si falla la consulta de componentes, agregar sin seriales
+ }
+
+ // Agregar bundle/kit al carrito sin seriales
cart.addBundle(bundle);
window.Notify.success(`${bundle.name} agregado al carrito`);
} catch (error) {
@@ -160,6 +183,19 @@ const handleSerialConfirm = (serialConfig) => {
closeSerialSelector();
};
+const closeBundleSerialSelector = () => {
+ showBundleSerialSelector.value = false;
+ bundleSerialSelectorBundle.value = null;
+};
+
+const handleBundleSerialConfirm = (serialConfig) => {
+ if (!bundleSerialSelectorBundle.value) return;
+
+ cart.addBundle(bundleSerialSelectorBundle.value, serialConfig);
+ window.Notify.success(`${bundleSerialSelectorBundle.value.name} agregado al carrito`);
+ closeBundleSerialSelector();
+};
+
const handleClearCart = () => {
if (confirm(t('cart.clearConfirm'))) {
cart.clear();
@@ -328,11 +364,15 @@ const handleConfirmSale = async (paymentData) => {
payment_method: paymentData.paymentMethod,
items: cart.items.map(item => {
if (item.is_bundle) {
- return {
+ const bundleItem = {
type: 'bundle',
bundle_id: item.bundle_id,
quantity: item.quantity,
};
+ if (item.track_serials && item.serial_numbers && Object.keys(item.serial_numbers).length > 0) {
+ bundleItem.serial_numbers = item.serial_numbers;
+ }
+ return bundleItem;
}
return {
type: 'product',
@@ -801,5 +841,15 @@ watch(activeTab, (newTab) => {
@close="closeSerialSelector"
@confirm="handleSerialConfirm"
/>
+
+
+
diff --git a/src/pages/POS/Returns/ReturnDetail.vue b/src/pages/POS/Returns/ReturnDetail.vue
index 70755cb..1419bbf 100644
--- a/src/pages/POS/Returns/ReturnDetail.vue
+++ b/src/pages/POS/Returns/ReturnDetail.vue
@@ -91,21 +91,6 @@ const handleClose = () => {
emit('close');
};
-const handleCancelReturn = async () => {
- if (!confirm('¿Cancelar esta devolución? Los productos volverán a estado vendido y el stock se revertirá.')) {
- return;
- }
-
- try {
- const response = await returnsService.cancelReturn(props.returnData.id);
- window.Notify.success(response.message || 'Devolución cancelada exitosamente');
- emit('cancelled');
- emit('close');
- } catch (error) {
- console.error('Error al cancelar devolución:', error);
- window.Notify.error('Error al cancelar la devolución');
- }
-};
@@ -295,16 +280,7 @@ const handleCancelReturn = async () => {
-
-
+
|