feat: MercadoLibre integration + inventory bulk publish + WhatsApp bridge fixes

- Add MercadoLibre OAuth, listings, orders, webhooks and category search
- New marketplace_external_bp.py, meli_service.py, marketplace_external_service.py
- New marketplace_external.html/js with ML management UI
- Inventory: bulk publish to ML with category autocomplete, listing type and shipping selectors
- Inventory: new .btn--meli styles, select/label CSS fixes
- WhatsApp bridge: rate limiting, 440/515/408 error handling, stale watchdog
- DB migration v3.4_meli_integration.sql for marketplace_listings, orders, sync_queue
- Add Celery tasks for ML sync and webhook processing
- Sidebar: MercadoLibre navigation link
This commit is contained in:
2026-05-26 04:24:07 +00:00
parent 50c0dbe7d4
commit a236187f3a
66 changed files with 7335 additions and 498 deletions

View File

@@ -104,7 +104,7 @@ const Customers = (() => {
const creditClass = usedPct >= 80 ? 'none' : usedPct >= 60 ? 'low' : '';
const num = String(c.id).padStart(5, '0');
const selClass = (currentCustomer && currentCustomer.id === c.id) ? 'selected' : '';
return '<tr class="' + selClass + '" onclick="selectCustomer(' + c.id + ')">' +
return '<tr class="' + selClass + '" onclick="Customers.selectCustomer(' + c.id + ')">' +
'<td class="cell-num">' + num + '</td>' +
'<td>' +
'<div class="cell-name">' + (c.name || '') + '</div>' +
@@ -263,6 +263,13 @@ const Customers = (() => {
bar.className = `progress-bar__fill ${pct < 40 ? 'low' : pct > 75 ? 'high' : ''}`;
}
// Discount
const discountEl = document.getElementById('detailMaxDiscount');
if (discountEl) discountEl.textContent = (c.max_discount_pct || 0) + '%';
// Re-wire action buttons after detail panel is visible
wireActionButtons();
// Purchase History
const hbody = document.getElementById('historyBody');
if (hbody) {
@@ -363,7 +370,7 @@ const Customers = (() => {
const btns = document.querySelectorAll('.quick-actions .action-btn');
// Order: Nueva Venta, Editar, Estado de Cuenta, Historial
if (btns.length >= 1) btns[0].onclick = () => {
if (currentCustomer) window.location.href = '/pos/?customer=' + currentCustomer.id;
if (currentCustomer) window.location.href = '/pos/sale?customer=' + currentCustomer.id;
};
if (btns.length >= 2) btns[1].onclick = () => editCurrent();
if (btns.length >= 3) btns[2].onclick = () => showStatement();
@@ -378,17 +385,19 @@ const Customers = (() => {
if (!modal) return;
document.getElementById('modalTitle').textContent = 'Nuevo Cliente';
document.getElementById('editId').value = '';
document.getElementById('fName').value = '';
document.getElementById('fRfc').value = '';
document.getElementById('fRazonSocial').value = '';
document.getElementById('fRegimenFiscal').value = '';
document.getElementById('fUsoCfdi').value = 'G03';
document.getElementById('fCp').value = '';
document.getElementById('fPhone').value = '';
document.getElementById('fEmail').value = '';
document.getElementById('fAddress').value = '';
document.getElementById('fPriceTier').value = '1';
document.getElementById('fCreditLimit').value = '0';
const safeSet = (id, v) => { const el = document.getElementById(id); if (el) el.value = v; };
safeSet('fName', '');
safeSet('fRfc', '');
safeSet('fRazonSocial', '');
safeSet('fRegimenFiscal', '');
safeSet('fUsoCfdi', 'G03');
safeSet('fCp', '');
safeSet('fPhone', '');
safeSet('fEmail', '');
safeSet('fAddress', '');
safeSet('fPriceTier', '1');
safeSet('fCreditLimit', '0');
safeSet('fMaxDiscountPct', '0');
modal.classList.add('active');
document.getElementById('fName').focus();
}
@@ -400,17 +409,19 @@ const Customers = (() => {
if (!modal) return;
document.getElementById('modalTitle').textContent = 'Editar Cliente';
document.getElementById('editId').value = c.id;
document.getElementById('fName').value = c.name || '';
document.getElementById('fRfc').value = c.rfc || '';
document.getElementById('fRazonSocial').value = c.razon_social || '';
document.getElementById('fRegimenFiscal').value = c.regimen_fiscal || '';
document.getElementById('fUsoCfdi').value = c.uso_cfdi || 'G03';
document.getElementById('fCp').value = c.cp || '';
document.getElementById('fPhone').value = c.phone || '';
document.getElementById('fEmail').value = c.email || '';
document.getElementById('fAddress').value = c.address || '';
document.getElementById('fPriceTier').value = c.price_tier || '1';
document.getElementById('fCreditLimit').value = c.credit_limit || 0;
const safeSet = (id, v) => { const el = document.getElementById(id); if (el) el.value = v; };
safeSet('fName', c.name || '');
safeSet('fRfc', c.rfc || '');
safeSet('fRazonSocial', c.razon_social || '');
safeSet('fRegimenFiscal', c.regimen_fiscal || '');
safeSet('fUsoCfdi', c.uso_cfdi || 'G03');
safeSet('fCp', c.cp || '');
safeSet('fPhone', c.phone || '');
safeSet('fEmail', c.email || '');
safeSet('fAddress', c.address || '');
safeSet('fPriceTier', c.price_tier || '1');
safeSet('fCreditLimit', c.credit_limit || 0);
safeSet('fMaxDiscountPct', c.max_discount_pct || 0);
modal.classList.add('active');
}
@@ -438,6 +449,7 @@ const Customers = (() => {
address: val('fAddress') || null,
price_tier: parseInt(val('fPriceTier')) || 1,
credit_limit: parseFloat(val('fCreditLimit')) || 0,
max_discount_pct: parseFloat(val('fMaxDiscountPct')) || 0,
};
const editId = val('editId');
@@ -586,7 +598,9 @@ const Customers = (() => {
function injectModals() {
// Customer Create/Edit Modal
if (!document.getElementById('customerModal')) {
// Always remove and re-inject to ensure latest fields are present
const existingModal = document.getElementById('customerModal');
if (existingModal) existingModal.remove();
const div = document.createElement('div');
div.innerHTML = `
<div id="customerModal" class="modal-overlay" style="display:none;">
@@ -646,6 +660,7 @@ const Customers = (() => {
</select>
</div>
<div class="form-group"><label>Limite de Credito</label><input type="number" id="fCreditLimit" class="form-input" value="0" min="0" step="1000" /></div>
<div class="form-group"><label>Descuento Max (%)</label><input type="number" id="fMaxDiscountPct" class="form-input" value="0" min="0" max="100" step="0.5" /></div>
</div>
</div>
<div class="modal-footer">
@@ -655,9 +670,10 @@ const Customers = (() => {
</div>
</div>`;
document.body.appendChild(div);
}
// Statement Modal
const existingStatement = document.getElementById('statementModal');
if (existingStatement) existingStatement.remove();
if (!document.getElementById('statementModal')) {
const div = document.createElement('div');
div.innerHTML = `
@@ -772,11 +788,15 @@ const Customers = (() => {
// Run init
init();
return {
const publicApi = {
search, goToPage, loadCustomers,
showDetail, selectCustomer, closeDetail,
showCreateModal, editCurrent, closeModal, save,
showStatement, closeStatement,
showPaymentModal, closePayment, recordPayment,
};
// Expose globally for inline HTML onclick handlers
window.Customers = publicApi;
return publicApi;
})();