feat: Fase 1-3 completas - precios proveedor, multi-sucursal, factura global
Fase 1: Lista de precios de proveedor - Tabla supplier_catalog_prices en master DB - Endpoints GET/POST/PUT/DELETE /supplier-catalog/prices - Upload CSV/Excel de precios de proveedor - Visualizacion de supplier_price en catalogo y POS Fase 2: Multi-sucursal completo - Migracion v4.0: inventory.branch_id=NULL, tabla inventory_stock - Campos fiscales en branches (RFC, regimen, CP, serie CFDI, certificados) - Trigger trg_update_inventory_stock para sincronizar stock por sucursal - Backend config_bp.py con CRUD de sucursales fiscales - Backend inventory_bp.py y pos_bp.py refactorizados para inventario compartido - Backend invoicing_bp.py usa datos fiscales de la sucursal de la venta - Frontend config.html/js con modal de sucursales expandido Fase 3: Factura global mensual - Migracion v4.1: tablas global_invoice_sales, sales.global_invoiced_at - build_global_invoice_xml() con InformacionGlobal SAT-compliant - Servicio global_invoice.py para agrupar ventas PUE <=000 - Endpoints POST/GET /global-invoice y /global-invoice/eligible-sales - Frontend invoicing.html/js con boton y modal de factura global
This commit is contained in:
@@ -46,6 +46,11 @@
|
||||
var checkoutBtn = document.getElementById('checkoutBtn');
|
||||
var cartFab = document.getElementById('cartFab');
|
||||
var cartCloseBtn = document.getElementById('cartCloseBtn');
|
||||
// Supplier prices upload
|
||||
var uploadPricesBtn = document.getElementById('uploadPricesBtn');
|
||||
var uploadPricesModal= document.getElementById('uploadPricesModal');
|
||||
var uploadPricesFile = document.getElementById('uploadPricesFile');
|
||||
var uploadPricesStatus=document.getElementById('uploadPricesStatus');
|
||||
|
||||
// ─── Navigation State ───
|
||||
var nav = {
|
||||
@@ -1053,6 +1058,7 @@
|
||||
'</div>' +
|
||||
'<div class="part-card__footer">' +
|
||||
(p.local_price ? '<span class="part-card__price">$' + fmt(p.local_price) + '</span>' : '<span class="part-card__price" style="color:var(--color-text-muted);">Sin precio</span>') +
|
||||
(p.supplier_price ? '<span class="part-card__price" style="color:#2d7d46;font-size:0.85em;">Prov: $' + fmt(p.supplier_price) + '</span>' : '') +
|
||||
stockBadge +
|
||||
'</div>' +
|
||||
'</article>';
|
||||
@@ -2105,6 +2111,53 @@
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Supplier prices upload ─────────────────────────────────────────────
|
||||
function openUploadPricesModal() {
|
||||
if (uploadPricesModal) uploadPricesModal.style.display = 'flex';
|
||||
if (uploadPricesStatus) uploadPricesStatus.innerHTML = '';
|
||||
if (uploadPricesFile) uploadPricesFile.value = '';
|
||||
}
|
||||
function closeUploadPricesModal() {
|
||||
if (uploadPricesModal) uploadPricesModal.style.display = 'none';
|
||||
}
|
||||
async function submitUploadPrices() {
|
||||
if (!uploadPricesFile || !uploadPricesFile.files || !uploadPricesFile.files[0]) {
|
||||
if (uploadPricesStatus) uploadPricesStatus.innerHTML = '<span style="color:var(--color-error);">Selecciona un archivo primero.</span>';
|
||||
return;
|
||||
}
|
||||
var form = new FormData();
|
||||
form.append('file', uploadPricesFile.files[0]);
|
||||
if (uploadPricesStatus) uploadPricesStatus.innerHTML = 'Subiendo...';
|
||||
try {
|
||||
var res = await fetch('/pos/api/supplier-catalog/prices/upload', {
|
||||
method: 'POST',
|
||||
headers: { 'Authorization': 'Bearer ' + token },
|
||||
body: form
|
||||
});
|
||||
var data = await res.json();
|
||||
if (res.ok && data.success) {
|
||||
if (uploadPricesStatus) uploadPricesStatus.innerHTML = '<span style="color:var(--color-success);">✓ Precios actualizados: ' + data.processed + ' (insertados: ' + data.inserted + ', actualizados: ' + data.updated + ')</span>';
|
||||
uploadPricesFile.value = '';
|
||||
} else {
|
||||
var msg = data.error || 'Error al subir precios';
|
||||
var details = (data.details || []).join('<br>');
|
||||
if (uploadPricesStatus) uploadPricesStatus.innerHTML = '<span style="color:var(--color-error);">' + esc(msg) + '</span>' + (details ? '<div style="margin-top:4px;font-size:0.9em;">' + details + '</div>' : '');
|
||||
}
|
||||
} catch (e) {
|
||||
if (uploadPricesStatus) uploadPricesStatus.innerHTML = '<span style="color:var(--color-error);">Error de red: ' + esc(e.message) + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
function shouldShowUploadPricesButton() {
|
||||
try {
|
||||
var user = JSON.parse(localStorage.getItem('pos_employee') || '{}');
|
||||
return user.role === 'owner' || user.role === 'admin';
|
||||
} catch (e) { return false; }
|
||||
}
|
||||
if (uploadPricesBtn && shouldShowUploadPricesButton()) {
|
||||
uploadPricesBtn.style.display = 'inline-flex';
|
||||
}
|
||||
|
||||
window.CatalogApp = {
|
||||
toggleCart: toggleCart,
|
||||
goToCheckout: goToCheckout,
|
||||
@@ -2124,6 +2177,9 @@
|
||||
togglePlate: togglePlate,
|
||||
lookupPlate: lookupPlate,
|
||||
setMode: setCatalogMode,
|
||||
openUploadPricesModal: openUploadPricesModal,
|
||||
closeUploadPricesModal: closeUploadPricesModal,
|
||||
submitUploadPrices: submitUploadPrices,
|
||||
};
|
||||
|
||||
// ─── INIT ───
|
||||
|
||||
@@ -161,7 +161,7 @@ const Config = (() => {
|
||||
|
||||
_branches.forEach(function(b, idx) {
|
||||
var statusBadge = b.is_active
|
||||
? '<span class="badge badge--ok" style="padding:0 4px;font-size:0.625rem;">' + (idx === 0 ? 'Principal' : 'Activa') + '</span>'
|
||||
? '<span class="badge badge--ok" style="padding:0 4px;font-size:0.625rem;">' + (b.is_main ? 'Principal' : 'Activa') + '</span>'
|
||||
: '<span class="badge badge--inactive" style="padding:0 4px;font-size:0.625rem;">Inactiva</span>';
|
||||
|
||||
html += '<div class="device-card">'
|
||||
@@ -170,14 +170,20 @@ const Config = (() => {
|
||||
+ '</div>'
|
||||
+ '<div class="device-card__body">'
|
||||
+ '<div class="device-card__name">' + escHtml(b.name) + '</div>'
|
||||
+ '<div class="device-card__detail">' + statusBadge + '</div>'
|
||||
+ '<div class="device-card__detail">' + statusBadge
|
||||
+ (b.rfc ? ' · RFC: ' + escHtml(b.rfc) : '')
|
||||
+ (b.codigo_postal ? ' · CP: ' + escHtml(b.codigo_postal) : '')
|
||||
+ '</div>'
|
||||
+ (b.address ? '<div class="device-card__detail">' + escHtml(b.address) + '</div>' : '')
|
||||
+ (b.phone ? '<div class="device-card__detail">' + escHtml(b.phone) + '</div>' : '')
|
||||
+ '</div>'
|
||||
+ '<div class="device-card__actions">'
|
||||
+ '<button class="btn btn--ghost btn--sm" onclick="Config.editBranch(' + b.id + ')">Editar</button>'
|
||||
+ '</div></div>';
|
||||
});
|
||||
|
||||
// "Agregar Sucursal" card
|
||||
html += '<div class="device-card" style="border-style:dashed;cursor:pointer;" onclick="Config.openModal(\'modal-branch\')">'
|
||||
html += '<div class="device-card" style="border-style:dashed;cursor:pointer;" onclick="Config.openBranchModal()">'
|
||||
+ '<div class="device-card__icon" style="background:transparent;border:2px dashed var(--color-border);">'
|
||||
+ '<svg viewBox="0 0 24 24" style="stroke:var(--color-text-muted);"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>'
|
||||
+ '</div>'
|
||||
@@ -203,9 +209,36 @@ const Config = (() => {
|
||||
});
|
||||
}
|
||||
|
||||
function openBranchModal(branch) {
|
||||
document.getElementById('branch-modal-title').textContent = branch ? 'Editar Sucursal' : 'Nueva Sucursal';
|
||||
document.getElementById('branch-id').value = branch ? branch.id : '';
|
||||
document.getElementById('branch-name').value = branch ? branch.name : '';
|
||||
document.getElementById('branch-rfc').value = branch ? (branch.rfc || '') : '';
|
||||
document.getElementById('branch-razon').value = branch ? (branch.razon_social || '') : '';
|
||||
document.getElementById('branch-regimen').value = branch ? (branch.regimen_fiscal || '') : '';
|
||||
document.getElementById('branch-cp').value = branch ? (branch.codigo_postal || '') : '';
|
||||
document.getElementById('branch-serie').value = branch ? (branch.serie_cfdi || '') : '';
|
||||
document.getElementById('branch-folio').value = branch ? (branch.folio_inicial || '') : '';
|
||||
document.getElementById('branch-licencia').value = branch ? (branch.licencia_fiscal || '') : '';
|
||||
document.getElementById('branch-address').value = branch ? (branch.address || '') : '';
|
||||
document.getElementById('branch-phone').value = branch ? (branch.phone || '') : '';
|
||||
document.getElementById('branch-main').checked = branch ? !!branch.is_main : false;
|
||||
document.getElementById('branch-cert').value = branch ? (branch.certificado_pem || '') : '';
|
||||
document.getElementById('branch-key').value = branch ? (branch.llave_pem || '') : '';
|
||||
openModal('modal-branch');
|
||||
}
|
||||
|
||||
function editBranch(branchId) {
|
||||
var b = _branches.find(function(x) { return x.id === branchId; });
|
||||
if (!b) { toast('Sucursal no encontrada', 'error'); return; }
|
||||
openBranchModal(b);
|
||||
}
|
||||
|
||||
async function saveBranch(data) {
|
||||
var res = await fetch(API + '/branches', {
|
||||
method: 'POST',
|
||||
var branchId = document.getElementById('branch-id').value;
|
||||
var url = API + '/branches' + (branchId ? '/' + branchId : '');
|
||||
var res = await fetch(url, {
|
||||
method: branchId ? 'PUT' : 'POST',
|
||||
headers: headers(),
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
@@ -429,14 +462,36 @@ const Config = (() => {
|
||||
try {
|
||||
await saveBranch({
|
||||
name: name,
|
||||
address: document.getElementById('branch-address').value.trim(),
|
||||
phone: document.getElementById('branch-phone').value.trim()
|
||||
rfc: document.getElementById('branch-rfc').value.trim() || null,
|
||||
razon_social: document.getElementById('branch-razon').value.trim() || null,
|
||||
regimen_fiscal: document.getElementById('branch-regimen').value.trim() || null,
|
||||
codigo_postal: document.getElementById('branch-cp').value.trim() || null,
|
||||
serie_cfdi: document.getElementById('branch-serie').value.trim() || null,
|
||||
folio_inicial: document.getElementById('branch-folio').value ? parseInt(document.getElementById('branch-folio').value, 10) : null,
|
||||
licencia_fiscal: document.getElementById('branch-licencia').value.trim() || null,
|
||||
address: document.getElementById('branch-address').value.trim() || null,
|
||||
phone: document.getElementById('branch-phone').value.trim() || null,
|
||||
is_main: document.getElementById('branch-main').checked,
|
||||
certificado_pem: document.getElementById('branch-cert').value.trim() || null,
|
||||
llave_pem: document.getElementById('branch-key').value.trim() || null,
|
||||
});
|
||||
toast('Sucursal creada');
|
||||
toast('Sucursal guardada');
|
||||
closeModal('modal-branch');
|
||||
// Reset form
|
||||
document.getElementById('branch-id').value = '';
|
||||
document.getElementById('branch-name').value = '';
|
||||
document.getElementById('branch-rfc').value = '';
|
||||
document.getElementById('branch-razon').value = '';
|
||||
document.getElementById('branch-regimen').value = '';
|
||||
document.getElementById('branch-cp').value = '';
|
||||
document.getElementById('branch-serie').value = '';
|
||||
document.getElementById('branch-folio').value = '';
|
||||
document.getElementById('branch-licencia').value = '';
|
||||
document.getElementById('branch-address').value = '';
|
||||
document.getElementById('branch-phone').value = '';
|
||||
document.getElementById('branch-main').checked = false;
|
||||
document.getElementById('branch-cert').value = '';
|
||||
document.getElementById('branch-key').value = '';
|
||||
await loadBranches();
|
||||
} catch (e) {
|
||||
toast(e.message, 'error');
|
||||
@@ -805,7 +860,7 @@ const Config = (() => {
|
||||
loadBusiness, saveBusiness, saveTaxParams,
|
||||
loadCurrency, saveCurrency,
|
||||
loadModules, saveModules,
|
||||
openModal, closeModal
|
||||
openModal, closeModal, openBranchModal, editBranch
|
||||
};
|
||||
// Register Cmd+K items
|
||||
if (typeof registerCmdKItem === "function") {
|
||||
|
||||
@@ -478,6 +478,51 @@ const Invoicing = (() => {
|
||||
alert('Nota de credito: proximamente');
|
||||
}
|
||||
|
||||
// ---- Global Invoice ----
|
||||
function openGlobalInvoiceModal() {
|
||||
const now = new Date();
|
||||
document.getElementById('global-year').value = now.getFullYear();
|
||||
document.getElementById('global-month').value = now.getMonth() + 1;
|
||||
document.getElementById('global-preview').innerHTML = 'Presiona "Vista previa" para ver ventas elegibles.';
|
||||
document.getElementById('modalGlobalInvoice').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function previewGlobalInvoice() {
|
||||
const year = document.getElementById('global-year').value;
|
||||
const month = document.getElementById('global-month').value;
|
||||
const preview = document.getElementById('global-preview');
|
||||
preview.innerHTML = 'Cargando...';
|
||||
try {
|
||||
const res = await api(`/global-invoice/eligible-sales?year=${year}&month=${month}`);
|
||||
preview.innerHTML = `<strong>${res.count} ventas elegibles</strong> — Total: $${fmt(res.total)}<br><small>${res.sales.map(s => '#' + s.id).join(', ')}</small>`;
|
||||
} catch (e) {
|
||||
preview.innerHTML = '<span style="color:var(--color-error);">Error: ' + e.message + '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
async function generateGlobalInvoice() {
|
||||
const year = parseInt(document.getElementById('global-year').value, 10);
|
||||
const month = parseInt(document.getElementById('global-month').value, 10);
|
||||
const btn = document.querySelector('#modalGlobalInvoice .btn--primary');
|
||||
const originalText = btn.textContent;
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Generando...';
|
||||
try {
|
||||
const res = await api('/global-invoice', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ year, month })
|
||||
});
|
||||
alert(`Factura global generada: ${res.provisional_folio} (${res.sales_count} ventas, $${fmt(res.total)})`);
|
||||
document.getElementById('modalGlobalInvoice').style.display = 'none';
|
||||
loadFacturas();
|
||||
} catch (e) {
|
||||
alert('Error: ' + e.message);
|
||||
} finally {
|
||||
btn.disabled = false;
|
||||
btn.textContent = originalText;
|
||||
}
|
||||
}
|
||||
|
||||
// Expose switchTab globally for onclick handlers in HTML
|
||||
window.switchTab = switchTab;
|
||||
window.showNewInvoiceModal = showNewInvoiceModal;
|
||||
@@ -489,6 +534,7 @@ const Invoicing = (() => {
|
||||
switchTab, loadFacturas, loadNotas, loadComplementos, loadCancelaciones,
|
||||
showDetail, showCancelModal, confirmCancel, processQueue,
|
||||
showNewInvoiceModal, closeNewInvoiceModal, submitNewInvoice, notaCreditoPlaceholder,
|
||||
openGlobalInvoiceModal, previewGlobalInvoice, generateGlobalInvoice,
|
||||
};
|
||||
// Register Cmd+K items
|
||||
if (typeof registerCmdKItem === "function") {
|
||||
|
||||
@@ -321,6 +321,21 @@ const POS = (() => {
|
||||
}
|
||||
}
|
||||
|
||||
function modifyPrice() {
|
||||
if (selectedRow < 0 || selectedRow >= cart.length) {
|
||||
showToast('Selecciona un articulo primero', 'warn');
|
||||
return;
|
||||
}
|
||||
const p = prompt('Nuevo precio unitario:', cart[selectedRow].unit_price);
|
||||
if (p !== null) {
|
||||
const n = parseFloat(p);
|
||||
if (n >= 0) {
|
||||
cart[selectedRow].unit_price = n;
|
||||
renderCart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wire confirm-cancel button
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
var btn = document.getElementById('btnConfirmCancel');
|
||||
@@ -1363,7 +1378,7 @@ const POS = (() => {
|
||||
connectThermal, thermalPrint,
|
||||
showOpenRegisterModal, closeOpenRegisterModal, openRegister,
|
||||
showCutZModal, closeCutZModal, loadCutX, confirmCutZ,
|
||||
openCancelModal, closeCancelModal, changeQuantity, applyDiscount,
|
||||
openCancelModal, closeCancelModal, changeQuantity, applyDiscount, modifyPrice,
|
||||
};
|
||||
// Register Cmd+K items
|
||||
if (typeof registerCmdKItem === "function") {
|
||||
|
||||
Reference in New Issue
Block a user