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:
2026-06-11 08:59:56 +00:00
parent ea29cc31c0
commit 2b73c2c6db
23 changed files with 1665 additions and 230 deletions

View File

@@ -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 ───

View File

@@ -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") {

View File

@@ -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") {

View File

@@ -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") {