+import { ref, computed } from 'vue';
+import { formatCurrency, formatDate } from '@/utils/formatters';
+
+import Modal from '@Holos/Modal.vue';
+import GoogleIcon from '@Shared/GoogleIcon.vue';
+
+/** Props */
+const props = defineProps({
+ request: {
+ type: Object,
+ required: true
+ }
+});
+
+/** Emits */
+const emit = defineEmits(['close', 'refresh']);
+
+/** Estado */
+const processing = ref(false);
+
+/** Computed */
+const hasSales = computed(() => props.request.sales && props.request.sales.length > 0);
+const latestSale = computed(() => hasSales.value ? props.request.sales[0] : null);
+
+/** Métodos */
+const closeModal = () => {
+ emit('close');
+};
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Datos de Facturación
+
+
+ Última venta: {{ latestSale.invoice_number || `#${String(latestSale.id).padStart(6, '0')}` }}
+
+
+ Cliente con datos fiscales registrados
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Datos del Cliente
+
+
+
+
+
+
+ {{ request.name }}
+
+
+
+
+
+
+ {{ request.email }}
+
+
+
+
+
+
+ {{ request.phone || 'No especificado' }}
+
+
+
+
+
+ {{ request.address || 'No especificada' }}
+
+
+
+
+
+
+
+
+
+
+ Datos Fiscales
+
+
+
+
+
+
+
+ {{ request.rfc }}
+
+
+
+
+
+
+ {{ request.razon_social || 'No especificada' }}
+
+
+
+
+
+ {{ request.regimen_fiscal || 'No especificado' }}
+
+
+
+
+
+ {{ request.cp_fiscal || 'No especificado' }}
+
+
+
+
+
+ {{ request.uso_cfdi || 'No especificado' }}
+
+
+
+
+
+
+
+
+
+
+ Última Venta Registrada
+
+
+
+
+
+
+ {{ latestSale.invoice_number || `#${String(latestSale.id).padStart(6, '0')}` }}
+
+
+
+
+
+ {{ formatCurrency(latestSale.total || 0) }}
+
+
+
+
+
+ {{ formatDate(latestSale.created_at) }}
+
+
+
+
+
+ Este cliente tiene {{ request.sales.length }} venta(s) registrada(s)
+
+
+
+
+
+
+
+
+
+
+ {{ formatDate(request.created_at) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/POS/Clients/BillingRequests.vue b/src/pages/POS/Clients/BillingRequests.vue
new file mode 100644
index 0000000..c5c2c97
--- /dev/null
+++ b/src/pages/POS/Clients/BillingRequests.vue
@@ -0,0 +1,160 @@
+
+
+
+
+
searcher.search(x)"
+ >
+
+
+
+
searcher.pagination(page)"
+ >
+
+ | CLIENTE |
+ RFC |
+ RAZÓN SOCIAL |
+ RÉGIMEN FISCAL |
+ FECHA REGISTRO |
+ ACCIONES |
+
+
+
+
+ |
+
+ {{ request.name }}
+
+
+ {{ request.email }}
+
+
+ {{ request.phone || 'N/A' }}
+
+ |
+
+
+
+ {{ request.rfc || 'N/A' }}
+
+ |
+
+
+
+ {{ request.razon_social || 'N/A' }}
+
+ |
+
+
+
+ {{ request.regimen_fiscal || 'N/A' }}
+
+ |
+
+
+
+ {{ formatDate(request.created_at) }}
+
+ |
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+ No hay solicitudes de facturación
+
+
+ Las solicitudes de facturación aparecerán aquí
+
+
+ |
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/POS/Factura/Index.vue b/src/pages/POS/Factura/Index.vue
new file mode 100644
index 0000000..c2d9c0a
--- /dev/null
+++ b/src/pages/POS/Factura/Index.vue
@@ -0,0 +1,319 @@
+
+
+
+
+
+
+
+
+
+
+
+ Solicitud de Factura
+
+
+ Complete sus datos fiscales para generar su factura
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ No se puede procesar
+
+
+ {{ error }}
+
+
+
+
+
+
+
+
+
+ Solicitud Enviada
+
+
+ Sus datos han sido recibidos correctamente. Recibirá su factura en el correo electrónico proporcionado.
+
+
+ Folio: {{ invoiceNumber }}
+
+
+
+
+
+
+
+
+
+ Datos de la Venta
+
+
+
+ Folio:
+ {{ saleData.invoice_number }}
+
+
+ Fecha:
+
+ {{ new Date(saleData.created_at).toLocaleDateString('es-MX') }}
+
+
+
+ Total:
+
+ {{ formatCurrency(saleData.total) }}
+
+
+
+
+
+
+
+
+
+
+
+
¿Tiene dudas? Contáctenos
+
+
+
+
diff --git a/src/pages/POS/Factura/Module.js b/src/pages/POS/Factura/Module.js
new file mode 100644
index 0000000..8f52795
--- /dev/null
+++ b/src/pages/POS/Factura/Module.js
@@ -0,0 +1,16 @@
+import { hasPermission } from '@Plugins/RolePermission.js';
+
+// Ruta API
+const apiTo = (name, params = {}) => route(`factura.${name}`, params)
+
+// Ruta visual
+const viewTo = ({ name = '', params = {}, query = {} }) => ({ name: `pos.factura.${name}`, params, query })
+
+// Determina si un usuario puede hacer algo en base a los permisos
+const can = (permission) => hasPermission(`factura.${permission}`)
+
+export {
+ can,
+ viewTo,
+ apiTo
+}
diff --git a/src/pages/POS/Inventory/Index.vue b/src/pages/POS/Inventory/Index.vue
index db57517..be47741 100644
--- a/src/pages/POS/Inventory/Index.vue
+++ b/src/pages/POS/Inventory/Index.vue
@@ -2,6 +2,7 @@
import { onMounted, ref } from 'vue';
import { useSearcher, apiURL } from '@Services/Api';
import { formatCurrency } from '@/utils/formatters';
+import { can } from './Module.js';
import SearcherHead from '@Holos/Searcher.vue';
import Table from '@Holos/Table.vue';
@@ -112,6 +113,7 @@ onMounted(() => {
@search="(x) => searcher.search(x)"
>
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/pages/POS/Inventory/Module.js b/src/pages/POS/Inventory/Module.js
new file mode 100644
index 0000000..641d1c6
--- /dev/null
+++ b/src/pages/POS/Inventory/Module.js
@@ -0,0 +1,16 @@
+import { hasPermission } from '@Plugins/RolePermission.js';
+
+// Ruta API
+const apiTo = (name, params = {}) => route(`inventario.${name}`, params)
+
+// Ruta visual
+const viewTo = ({ name = '', params = {}, query = {} }) => ({ name: `pos.inventory.${name}`, params, query })
+
+// Determina si un usuario puede hacer algo en base a los permisos
+const can = (permission) => hasPermission(`inventario.${permission}`)
+
+export {
+ can,
+ viewTo,
+ apiTo
+}
diff --git a/src/pages/POS/Sales/Index.vue b/src/pages/POS/Sales/Index.vue
index 3e064a4..be012fd 100644
--- a/src/pages/POS/Sales/Index.vue
+++ b/src/pages/POS/Sales/Index.vue
@@ -142,7 +142,7 @@ onMounted(() => {
>
- #{{ String(model.id).padStart(6, '0') }}
+ #{{ model.invoice_number }}
|
diff --git a/src/router/Index.js b/src/router/Index.js
index d996309..e7b08d2 100644
--- a/src/router/Index.js
+++ b/src/router/Index.js
@@ -39,6 +39,7 @@ const router = createRouter({
{
path: 'inventory',
name: 'pos.inventory.index',
+ beforeEnter: (to, from, next) => can(next, 'inventario.index'),
component: () => import('@Pages/POS/Inventory/Index.vue')
},
{
@@ -75,6 +76,11 @@ const router = createRouter({
path: 'clients',
name: 'pos.clients.index',
component: () => import('@Pages/POS/Clients/Index.vue')
+ },
+ {
+ path: 'billing-requests',
+ name: 'pos.billingRequests.index',
+ component: () => import('@Pages/POS/Clients/BillingRequests.vue')
}
]
},
@@ -185,6 +191,11 @@ const router = createRouter({
}
]
},
+ {
+ path: '/facturacion/:invoiceNumber',
+ name: 'facturacion.index',
+ component: () => import('@Pages/POS/Factura/Index.vue')
+ },
{
path: '/auth',
component: () => import('@Holos/Layout/Auth.vue'),
diff --git a/src/services/ticketService.js b/src/services/ticketService.js
index 6bc7c0a..80f2c56 100644
--- a/src/services/ticketService.js
+++ b/src/services/ticketService.js
@@ -1,4 +1,5 @@
import jsPDF from 'jspdf';
+import QRCode from 'qrcode';
import { formatMoney, PAYMENT_METHODS } from '@/utils/formatters';
/**
@@ -86,7 +87,7 @@ const ticketService = {
doc.setTextColor(...darkGrayColor);
// Folio
- const folio = saleData.invoice_number || `INV-${String(saleData.id || '').padStart(12, '0')}`;
+ const folio = saleData.invoice_number || `INV-${String(saleData.id || '').padStart(7, '0')}`;
doc.text(`Folio: ${folio}`, leftMargin, yPosition);
yPosition += 4;
@@ -274,6 +275,53 @@ const ticketService = {
doc.text(`Impreso: ${currentDate}`, centerX, yPosition, { align: 'center' });
yPosition += 3;
doc.text(`ID de venta: ${saleData.id || 'N/A'}`, centerX, yPosition, { align: 'center' });
+ yPosition += 8;
+
+ // ===== QR =====
+ const invoiceNumber = saleData.invoice_number || saleData.id;
+ const facturacionUrl = `${window.location.origin}/#/facturacion/${invoiceNumber}`;
+
+ try {
+ // Generar QR como data URL
+ const qrDataUrl = await QRCode.toDataURL(facturacionUrl, {
+ width: 150,
+ margin: 1,
+ color: {
+ dark: '#000000',
+ light: '#ffffff'
+ }
+ });
+
+ // Línea separadora antes del QR
+ doc.setDrawColor(...darkGrayColor);
+ doc.setLineWidth(0.2);
+ doc.line(leftMargin, yPosition, rightMargin, yPosition);
+ yPosition += 5;
+
+ // Texto invitación a facturar
+ doc.setFontSize(8);
+ doc.setFont('helvetica', 'bold');
+ doc.setTextColor(...blackColor);
+ doc.text('¿Necesitas factura?', centerX, yPosition, { align: 'center' });
+ yPosition += 4;
+
+ doc.setFontSize(7);
+ doc.setFont('helvetica', 'normal');
+ doc.setTextColor(...darkGrayColor);
+ doc.text('Escanea el código QR para', centerX, yPosition, { align: 'center' });
+ yPosition += 3;
+ doc.text('enviar tus datos de facturación', centerX, yPosition, { align: 'center' });
+ yPosition += 5;
+
+ // Agregar imagen QR centrada
+ const qrSize = 25;
+ const qrX = centerX - (qrSize / 2);
+ doc.addImage(qrDataUrl, 'PNG', qrX, yPosition, qrSize, qrSize);
+ yPosition += qrSize + 3;
+
+ } catch (error) {
+ console.error('Error generando QR:', error);
+ }
// Guardar o retornar el PDF
if (autoDownload) {
|