Autosave: 20260211-073630

This commit is contained in:
Flatlogic Bot 2026-02-11 07:36:31 +00:00
parent 48923270af
commit a30bc16ccc
19 changed files with 1007 additions and 174 deletions

View File

@ -250,16 +250,55 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<script> <script>
function downloadPDF() { function getCompactStyle() {
const style = document.createElement('style');
style.id = 'temp-pdf-style';
style.textContent = `
/* Cairo loaded in base.html */
#invoice-card {
font-family: 'Cairo', sans-serif !important;
padding: 20px !important;
direction: rtl;
}
#invoice-card * {
font-family: 'Cairo', sans-serif !important;
}
/* Compact overrides */
.p-5 { padding: 10px !important; }
.mb-5 { margin-bottom: 10px !important; }
.mx-5 { margin-left: 0 !important; margin-right: 0 !important; }
.h1, h1 { font-size: 22px !important; margin-bottom: 5px !important; }
.h3, h3 { font-size: 18px !important; }
.h5, h5 { font-size: 14px !important; }
.table th, .table td { padding: 4px 8px !important; font-size: 11px !important; }
.small { font-size: 10px !important; }
img { max-height: 50px !important; margin-bottom: 10px !important; }
.badge { padding: 2px 8px !important; font-size: 10px !important; }
`;
return style;
}
async function downloadPDF() {
const element = document.getElementById('invoice-card'); const element = document.getElementById('invoice-card');
const style = getCompactStyle();
document.head.appendChild(style);
// Wait for fonts
await document.fonts.ready;
await new Promise(resolve => setTimeout(resolve, 500));
const opt = { const opt = {
margin: 0, margin: [10, 10, 10, 10],
filename: 'Invoice_{{ sale.invoice_number|default:sale.id }}.pdf', filename: 'Invoice_{{ sale.invoice_number|default:sale.id }}.pdf',
image: { type: 'jpeg', quality: 0.98 }, image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, letterRendering: true }, html2canvas: { scale: 2, useCORS: true, letterRendering: false },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' } jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
}; };
html2pdf().set(opt).from(element).save();
html2pdf().set(opt).from(element).save().then(() => {
style.remove();
});
} }
async function sendWhatsAppDirect() { async function sendWhatsAppDirect() {
@ -275,13 +314,20 @@ async function sendWhatsAppDirect() {
spinner.classList.remove('d-none'); spinner.classList.remove('d-none');
btn.disabled = true; btn.disabled = true;
try {
const element = document.getElementById('invoice-card'); const element = document.getElementById('invoice-card');
const style = getCompactStyle();
document.head.appendChild(style);
// Wait for fonts
await document.fonts.ready;
await new Promise(resolve => setTimeout(resolve, 500));
try {
const opt = { const opt = {
margin: 0, margin: [10, 10, 10, 10],
filename: 'Invoice_{{ sale.invoice_number|default:sale.id }}.pdf', filename: 'Invoice_{{ sale.invoice_number|default:sale.id }}.pdf',
image: { type: 'jpeg', quality: 0.98 }, image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, letterRendering: true }, html2canvas: { scale: 2, useCORS: true, letterRendering: false },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' } jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
}; };
@ -306,7 +352,7 @@ async function sendWhatsAppDirect() {
if (data.success) { if (data.success) {
alert(data.message || "{% trans 'Invoice sent successfully!' %}"); alert(data.message || "{% trans 'Invoice sent successfully!' %}");
} else { } else {
alert(data.error || data.message || "{% trans 'Failed to send invoice.' %}"); alert((data.error || data.message || "{% trans 'Failed to send invoice.' %}") + '\n\n{% trans "Please check your System Settings > WhatsApp Gateway configuration." %}');
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -314,21 +360,10 @@ async function sendWhatsAppDirect() {
} finally { } finally {
spinner.classList.add('d-none'); spinner.classList.add('d-none');
btn.disabled = false; btn.disabled = false;
style.remove();
} }
} }
// Auto-trigger if 'created=true' is in URL
document.addEventListener("DOMContentLoaded", function() {
const urlParams = new URLSearchParams(window.location.search);
if (urlParams.get("created") === "true") {
const phone = document.getElementById('whatsappPhoneDirect').value;
if (phone) {
sendWhatsAppDirect();
}
}
});
</script>
<style> <style>
@media print { @media print {
@page { @page {

View File

@ -240,6 +240,7 @@
</div> </div>
</div> </div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<script> <script>
function openWhatsAppModal(saleId, phone, invoiceNum) { function openWhatsAppModal(saleId, phone, invoiceNum) {
document.getElementById('waSaleId').value = saleId; document.getElementById('waSaleId').value = saleId;
@ -262,30 +263,125 @@ async function sendWhatsAppFromList() {
btn.disabled = true; btn.disabled = true;
spinner.classList.remove('d-none'); spinner.classList.remove('d-none');
// Create a temporary container for the invoice
const tempContainer = document.createElement('div');
tempContainer.style.position = 'absolute';
tempContainer.style.left = '-9999px';
tempContainer.style.top = '0';
tempContainer.style.width = '790px'; // Slightly less than A4 width to prevent overflow
document.body.appendChild(tempContainer);
try { try {
// 1. Fetch the invoice detail page
const invoiceResponse = await fetch(`/invoices/${saleId}/`);
const invoiceHtml = await invoiceResponse.text();
// 2. Parse and extract the invoice card and styles
const parser = new DOMParser();
const doc = parser.parseFromString(invoiceHtml, 'text/html');
const invoiceCard = doc.getElementById('invoice-card');
const styles = doc.querySelectorAll('style');
if (!invoiceCard) {
throw new Error("Could not load invoice template");
}
// Append styles to head temporarily
const addedStyles = [];
// Force Cairo Font for Arabic support AND compact styles for single page
const customStyle = document.createElement('style');
customStyle.textContent = `
/* Cairo is already loaded in base.html, just apply it */
#invoice-card {
font-family: 'Cairo', sans-serif !important;
padding: 20px !important;
background: white !important;
direction: rtl; /* Force RTL for better Arabic support in canvas */
}
#invoice-card * {
font-family: 'Cairo', sans-serif !important;
}
/* Compact overrides */
.p-5 { padding: 10px !important; }
.mb-5 { margin-bottom: 10px !important; }
.mx-5 { margin-left: 0 !important; margin-right: 0 !important; }
.h1, h1 { font-size: 22px !important; margin-bottom: 5px !important; }
.h3, h3 { font-size: 18px !important; }
.h5, h5 { font-size: 14px !important; }
.table td, .table th { padding: 4px 8px !important; font-size: 11px !important; }
.small { font-size: 10px !important; }
img { max-height: 50px !important; margin-bottom: 10px !important; }
.badge { padding: 2px 8px !important; font-size: 10px !important; }
`;
document.head.appendChild(customStyle);
addedStyles.push(customStyle);
styles.forEach(style => {
const newStyle = document.createElement('style');
newStyle.textContent = style.textContent;
document.head.appendChild(newStyle);
addedStyles.push(newStyle);
});
// Clean up for PDF (remove buttons/non-printable if any leaked)
const noPrint = invoiceCard.querySelectorAll('.d-print-none');
noPrint.forEach(el => el.remove());
tempContainer.appendChild(invoiceCard);
// Wait for fonts and layout
await document.fonts.ready;
await new Promise(resolve => setTimeout(resolve, 800)); // Give browser time to render ligatures
// 3. Generate PDF
const opt = {
margin: [10, 10, 10, 10], // Top, Left, Bottom, Right margins in mm
filename: `Invoice_${saleId}.pdf`,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, letterRendering: false }, // letterRendering: false fixes Arabic
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
};
const pdfBlob = await html2pdf().set(opt).from(invoiceCard).outputPdf('datauristring');
// Remove temporary styles
addedStyles.forEach(style => style.remove());
// 4. Send to Backend
const response = await fetch("{% url 'send_invoice_whatsapp' %}", { const response = await fetch("{% url 'send_invoice_whatsapp' %}", {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}' 'X-CSRFToken': '{{ csrf_token }}'
}, },
body: JSON.stringify({ sale_id: saleId, phone: phone }) body: JSON.stringify({
sale_id: saleId,
phone: phone,
pdf_data: pdfBlob
})
}); });
const data = await response.json(); const data = await response.json();
if (data.success) { if (data.success) {
alert(data.message || "{% trans 'Message sent successfully!' %}"); alert('{% trans "Invoice sent via WhatsApp successfully!" %}');
bootstrap.Modal.getInstance(document.getElementById('whatsappModal')).hide(); const waModalEl = document.getElementById('whatsappModal');
const modalInstance = bootstrap.Modal.getInstance(waModalEl);
if (modalInstance) {
modalInstance.hide();
}
} else { } else {
alert(data.error || "{% trans 'Failed to send message.' %}"); alert('{% trans "WhatsApp Error:" %} ' + data.error + '\n\n{% trans "Please check your System Settings > WhatsApp Gateway configuration." %}');
} }
} catch (e) { } catch (e) {
alert("{% trans 'An error occurred.' %}"); alert("{% trans 'An error occurred while generating or sending the invoice.' %}");
console.error(e); console.error(e);
} finally { } finally {
btn.disabled = false; btn.disabled = false;
spinner.classList.add('d-none'); spinner.classList.add('d-none');
document.body.removeChild(tempContainer);
} }
} }
</script> </script>

View File

@ -169,16 +169,54 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<script> <script>
function downloadPDF() { function getCompactStyle() {
const style = document.createElement('style');
style.id = 'temp-pdf-style';
style.textContent = `
/* Cairo loaded in base.html */
#order-card {
font-family: 'Cairo', sans-serif !important;
padding: 20px !important;
direction: rtl;
}
#order-card * {
font-family: 'Cairo', sans-serif !important;
}
/* Compact overrides */
.p-5 { padding: 10px !important; }
.mb-5 { margin-bottom: 10px !important; }
.mx-5 { margin-left: 0 !important; margin-right: 0 !important; }
.h1, h1 { font-size: 22px !important; margin-bottom: 5px !important; }
.h3, h3 { font-size: 18px !important; }
.h5, h5 { font-size: 14px !important; }
.table th, .table td { padding: 4px 8px !important; font-size: 11px !important; }
.small { font-size: 10px !important; }
img { max-height: 50px !important; margin-bottom: 10px !important; }
.badge { padding: 2px 8px !important; font-size: 10px !important; }
`;
return style;
}
async function downloadPDF() {
const element = document.getElementById('order-card'); const element = document.getElementById('order-card');
const style = getCompactStyle();
document.head.appendChild(style);
// Wait for fonts
await document.fonts.ready;
await new Promise(resolve => setTimeout(resolve, 500));
const opt = { const opt = {
margin: 0, margin: [10, 10, 10, 10],
filename: 'LPO_{{ order.lpo_number|default:order.id }}.pdf', filename: 'LPO_{{ order.lpo_number|default:order.id }}.pdf',
image: { type: 'jpeg', quality: 0.98 }, image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, letterRendering: true }, html2canvas: { scale: 2, useCORS: true, letterRendering: false },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' } jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
}; };
html2pdf().set(opt).from(element).save(); html2pdf().set(opt).from(element).save().then(() => {
style.remove();
});
} }
</script> </script>

View File

@ -69,6 +69,11 @@
<a href="{% url 'lpo_detail' order.id %}" class="btn btn-sm btn-white border" title="{% trans 'View & Print' %}"> <a href="{% url 'lpo_detail' order.id %}" class="btn btn-sm btn-white border" title="{% trans 'View & Print' %}">
<i class="bi bi-printer"></i> <i class="bi bi-printer"></i>
</a> </a>
<button type="button" class="btn btn-sm btn-white border text-success"
onclick="openWhatsAppModal('{{ order.id }}', '{{ order.supplier.phone|default:'' }}', '{{ order.lpo_number|default:order.id }}')"
title="{% trans 'Send via WhatsApp' %}">
<i class="bi bi-whatsapp"></i>
</button>
{% if order.status != 'converted' %} {% if order.status != 'converted' %}
<form action="{% url 'convert_lpo_to_purchase' order.id %}" method="POST" style="display: inline;" onsubmit="return confirm('{% trans 'Convert this LPO to a Purchase Invoice? This will update stock.' %}');"> <form action="{% url 'convert_lpo_to_purchase' order.id %}" method="POST" style="display: inline;" onsubmit="return confirm('{% trans 'Convert this LPO to a Purchase Invoice? This will update stock.' %}');">
{% csrf_token %} {% csrf_token %}
@ -119,5 +124,185 @@
{% endif %} {% endif %}
</div> </div>
</div> </div>
{% endif %}
</div> </div>
</div>
<!-- WhatsApp Modal -->
<div class="modal fade" id="whatsappModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0 shadow rounded-4">
<div class="modal-header border-0">
<h5 class="fw-bold">{% trans "Send LPO via WhatsApp" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<input type="hidden" id="waLpoId">
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "LPO #" %}</label>
<input type="text" id="waLpoNum" class="form-control-plaintext fw-bold" readonly>
</div>
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Phone Number" %}</label>
<input type="text" id="waPhone" class="form-control rounded-3" placeholder="e.g. 628123456789">
<div class="form-text">{% trans "Enter number with country code (e.g., 62...)" %}</div>
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-light rounded-3" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="button" class="btn btn-success rounded-3 px-4" onclick="sendWhatsAppFromList()">
<span id="waSpinner" class="spinner-border spinner-border-sm d-none me-2"></span>
{% trans "Send Message" %}
</button>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<script>
function openWhatsAppModal(lpoId, phone, lpoNum) {
document.getElementById('waLpoId').value = lpoId;
document.getElementById('waPhone').value = phone;
document.getElementById('waLpoNum').value = lpoNum;
new bootstrap.Modal(document.getElementById('whatsappModal')).show();
}
async function sendWhatsAppFromList() {
const lpoId = document.getElementById('waLpoId').value;
const phone = document.getElementById('waPhone').value;
const btn = document.querySelector('#whatsappModal .btn-success');
const spinner = document.getElementById('waSpinner');
if (!phone) {
alert("{% trans 'Please enter a phone number.' %}");
return;
}
btn.disabled = true;
spinner.classList.remove('d-none');
// Create a temporary container
const tempContainer = document.createElement('div');
tempContainer.style.position = 'absolute';
tempContainer.style.left = '-9999px';
tempContainer.style.top = '0';
tempContainer.style.width = '790px'; // Compact width
document.body.appendChild(tempContainer);
try {
// 1. Fetch detail page
const response = await fetch(`/lpo/${lpoId}/`);
const html = await response.text();
// 2. Parse
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
const card = doc.getElementById('order-card'); // ID in lpo_detail.html
const styles = doc.querySelectorAll('style');
if (!card) {
throw new Error("Could not load LPO template");
}
// Append styles
const addedStyles = [];
// Force Cairo Font and Compact Styles
const customStyle = document.createElement('style');
customStyle.textContent = `
/* Cairo loaded in base.html */
#order-card {
font-family: 'Cairo', sans-serif !important;
padding: 20px !important;
background: white !important;
direction: rtl;
}
#order-card * {
font-family: 'Cairo', sans-serif !important;
}
/* Compact overrides */
.p-5 { padding: 10px !important; }
.mb-5 { margin-bottom: 10px !important; }
.mx-5 { margin-left: 0 !important; margin-right: 0 !important; }
.h1, h1 { font-size: 22px !important; margin-bottom: 5px !important; }
.h3, h3 { font-size: 18px !important; }
.h5, h5 { font-size: 14px !important; }
.table td, .table th { padding: 4px 8px !important; font-size: 11px !important; }
.small { font-size: 10px !important; }
img { max-height: 50px !important; margin-bottom: 10px !important; }
.badge { padding: 2px 8px !important; font-size: 10px !important; }
`;
document.head.appendChild(customStyle);
addedStyles.push(customStyle);
styles.forEach(style => {
const newStyle = document.createElement('style');
newStyle.textContent = style.textContent;
document.head.appendChild(newStyle);
addedStyles.push(newStyle);
});
// Clean up
const noPrint = card.querySelectorAll('.d-print-none');
noPrint.forEach(el => el.remove());
tempContainer.appendChild(card);
// Wait for fonts and layout
await document.fonts.ready;
await new Promise(resolve => setTimeout(resolve, 800));
// 3. Generate PDF
const opt = {
margin: [10, 10, 10, 10],
filename: `LPO_${lpoId}.pdf`,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, letterRendering: false },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
};
const pdfBlob = await html2pdf().set(opt).from(card).outputPdf('datauristring');
// Remove styles
addedStyles.forEach(style => style.remove());
// 4. Send to Backend
const apiResponse = await fetch("{% url 'send_lpo_whatsapp' %}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
body: JSON.stringify({
lpo_id: lpoId,
phone: phone,
pdf_data: pdfBlob
})
});
const data = await apiResponse.json();
if (data.success) {
alert('{% trans "LPO sent via WhatsApp successfully!" %}');
const waModalEl = document.getElementById('whatsappModal');
const modalInstance = bootstrap.Modal.getInstance(waModalEl);
if (modalInstance) {
modalInstance.hide();
}
} else {
alert('{% trans "WhatsApp Error:" %} ' + data.error + '\n\n{% trans "Please check your System Settings > WhatsApp Gateway configuration." %}');
}
} catch (e) {
alert("{% trans 'An error occurred while generating or sending the LPO.' %}");
console.error(e);
} finally {
btn.disabled = false;
spinner.classList.add('d-none');
document.body.removeChild(tempContainer);
}
}
</script>
{% endblock %} {% endblock %}

View File

@ -213,16 +213,55 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<script> <script>
function downloadPDF() { function getCompactStyle() {
const style = document.createElement('style');
style.id = 'temp-pdf-style';
style.textContent = `
/* Cairo loaded in base.html */
#quotation-card {
font-family: 'Cairo', sans-serif !important;
padding: 20px !important;
direction: rtl;
}
#quotation-card * {
font-family: 'Cairo', sans-serif !important;
}
/* Compact overrides */
.p-5 { padding: 10px !important; }
.mb-5 { margin-bottom: 10px !important; }
.mx-5 { margin-left: 0 !important; margin-right: 0 !important; }
.h1, h1 { font-size: 22px !important; margin-bottom: 5px !important; }
.h3, h3 { font-size: 18px !important; }
.h5, h5 { font-size: 14px !important; }
.table th, .table td { padding: 4px 8px !important; font-size: 11px !important; }
.small { font-size: 10px !important; }
img { max-height: 50px !important; margin-bottom: 10px !important; }
.badge { padding: 2px 8px !important; font-size: 10px !important; }
`;
return style;
}
async function downloadPDF() {
const element = document.getElementById('quotation-card'); const element = document.getElementById('quotation-card');
const style = getCompactStyle();
document.head.appendChild(style);
// Wait for fonts
await document.fonts.ready;
await new Promise(resolve => setTimeout(resolve, 500));
const opt = { const opt = {
margin: 0, margin: [10, 10, 10, 10],
filename: 'Quotation_{{ quotation.quotation_number|default:quotation.id }}.pdf', filename: 'Quotation_{{ quotation.quotation_number|default:quotation.id }}.pdf',
image: { type: 'jpeg', quality: 0.98 }, image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, letterRendering: true }, html2canvas: { scale: 2, useCORS: true, letterRendering: false },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' } jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
}; };
html2pdf().set(opt).from(element).save();
html2pdf().set(opt).from(element).save().then(() => {
style.remove();
});
} }
</script> </script>

View File

@ -72,6 +72,11 @@
<td>{{ q.valid_until|date:"Y-m-d"|default:"-" }}</td> <td>{{ q.valid_until|date:"Y-m-d"|default:"-" }}</td>
<td class="text-end pe-4"> <td class="text-end pe-4">
<div class="btn-group shadow-sm rounded-3"> <div class="btn-group shadow-sm rounded-3">
<button type="button" class="btn btn-sm btn-white border text-success"
onclick="openWhatsAppModal('{{ q.id }}', '{{ q.customer.phone|default:'' }}', '{{ q.quotation_number|default:q.id }}')"
title="{% trans 'Send via WhatsApp' %}">
<i class="bi bi-whatsapp"></i>
</button>
<a href="{% url 'quotation_detail' q.id %}" class="btn btn-sm btn-white border" title="{% trans 'View & Print' %}"> <a href="{% url 'quotation_detail' q.id %}" class="btn btn-sm btn-white border" title="{% trans 'View & Print' %}">
<i class="bi bi-printer"></i> <i class="bi bi-printer"></i>
</a> </a>
@ -138,5 +143,181 @@
{% include "core/pagination.html" with page_obj=quotations %} {% include "core/pagination.html" with page_obj=quotations %}
</div> </div>
</div> </div>
<!-- WhatsApp Modal -->
<div class="modal fade" id="whatsappModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0 shadow rounded-4">
<div class="modal-header border-0">
<h5 class="fw-bold">{% trans "Send Quotation via WhatsApp" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div> </div>
<div class="modal-body">
<input type="hidden" id="waQuotationId">
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Quotation #" %}</label>
<input type="text" id="waQuotationNum" class="form-control-plaintext fw-bold" readonly>
</div>
<div class="mb-3">
<label class="form-label small fw-bold">{% trans "Phone Number" %}</label>
<input type="text" id="waPhone" class="form-control rounded-3" placeholder="e.g. 628123456789">
<div class="form-text">{% trans "Enter number with country code (e.g., 62...)" %}</div>
</div>
</div>
<div class="modal-footer border-0">
<button type="button" class="btn btn-light rounded-3" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="button" class="btn btn-success rounded-3 px-4" onclick="sendWhatsAppFromList()">
<span id="waSpinner" class="spinner-border spinner-border-sm d-none me-2"></span>
{% trans "Send Message" %}
</button>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<script>
function openWhatsAppModal(quotationId, phone, quotationNum) {
document.getElementById('waQuotationId').value = quotationId;
document.getElementById('waPhone').value = phone;
document.getElementById('waQuotationNum').value = quotationNum;
new bootstrap.Modal(document.getElementById('whatsappModal')).show();
}
async function sendWhatsAppFromList() {
const quotationId = document.getElementById('waQuotationId').value;
const phone = document.getElementById('waPhone').value;
const btn = document.querySelector('#whatsappModal .btn-success');
const spinner = document.getElementById('waSpinner');
if (!phone) {
alert("{% trans 'Please enter a phone number.' %}");
return;
}
btn.disabled = true;
spinner.classList.remove('d-none');
// Create a temporary container for the quotation
const tempContainer = document.createElement('div');
tempContainer.style.position = 'absolute';
tempContainer.style.left = '-9999px';
tempContainer.style.top = '0';
tempContainer.style.width = '790px'; // Slightly less than A4
document.body.appendChild(tempContainer);
try {
// 1. Fetch the quotation detail page
const quotationResponse = await fetch(`/quotations/${quotationId}/`);
const quotationHtml = await quotationResponse.text();
// 2. Parse and extract the quotation card and styles
const parser = new DOMParser();
const doc = parser.parseFromString(quotationHtml, 'text/html');
const quotationCard = doc.getElementById('quotation-card');
const styles = doc.querySelectorAll('style');
if (!quotationCard) {
throw new Error("Could not load quotation template");
}
// Append styles to head temporarily
const addedStyles = [];
// Force Cairo Font and Compact Styles
const customStyle = document.createElement('style');
customStyle.textContent = `
/* Cairo loaded in base.html */
#quotation-card {
font-family: 'Cairo', sans-serif !important;
padding: 20px !important;
background: white !important;
direction: rtl;
}
#quotation-card * {
font-family: 'Cairo', sans-serif !important;
}
/* Compact overrides */
.p-5 { padding: 10px !important; }
.mb-5 { margin-bottom: 10px !important; }
.mx-5 { margin-left: 0 !important; margin-right: 0 !important; }
.h1, h1 { font-size: 22px !important; margin-bottom: 5px !important; }
.h3, h3 { font-size: 18px !important; }
.h5, h5 { font-size: 14px !important; }
.table td, .table th { padding: 4px 8px !important; font-size: 11px !important; }
.small { font-size: 10px !important; }
img { max-height: 50px !important; margin-bottom: 10px !important; }
.badge { padding: 2px 8px !important; font-size: 10px !important; }
`;
document.head.appendChild(customStyle);
addedStyles.push(customStyle);
styles.forEach(style => {
const newStyle = document.createElement('style');
newStyle.textContent = style.textContent;
document.head.appendChild(newStyle);
addedStyles.push(newStyle);
});
// Clean up for PDF (remove buttons/non-printable if any leaked)
const noPrint = quotationCard.querySelectorAll('.d-print-none');
noPrint.forEach(el => el.remove());
tempContainer.appendChild(quotationCard);
// Wait for fonts and layout
await document.fonts.ready;
await new Promise(resolve => setTimeout(resolve, 800));
// 3. Generate PDF
const opt = {
margin: [10, 10, 10, 10],
filename: `Quotation_${quotationId}.pdf`,
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, letterRendering: false },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
};
const pdfBlob = await html2pdf().set(opt).from(quotationCard).outputPdf('datauristring');
// Remove temporary styles
addedStyles.forEach(style => style.remove());
// 4. Send to Backend
const response = await fetch("{% url 'send_quotation_whatsapp' %}", {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': '{{ csrf_token }}'
},
body: JSON.stringify({
quotation_id: quotationId,
phone: phone,
pdf_data: pdfBlob
})
});
const data = await response.json();
if (data.success) {
alert('{% trans "Quotation sent via WhatsApp successfully!" %}');
const waModalEl = document.getElementById('whatsappModal');
const modalInstance = bootstrap.Modal.getInstance(waModalEl);
if (modalInstance) {
modalInstance.hide();
}
} else {
alert('{% trans "WhatsApp Error:" %} ' + data.error + '\n\n{% trans "Please check your System Settings > WhatsApp Gateway configuration." %}');
}
} catch (e) {
alert("{% trans 'An error occurred while generating or sending the quotation.' %}");
console.error(e);
} finally {
btn.disabled = false;
spinner.classList.add('d-none');
document.body.removeChild(tempContainer);
}
}
</script>
{% endblock %} {% endblock %}

View File

@ -373,83 +373,6 @@
</button> </button>
</td> </td>
</tr> </tr>
<!-- Edit Device Modal -->
<div class="modal fade" id="editDeviceModal{{ device.id }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0">
<form action="{% url 'edit_device' device.id %}" method="post">
{% csrf_token %}
<div class="modal-header">
<h5 class="modal-title fw-bold">{% trans "Edit Device" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-semibold">{% trans "Device Name" %}</label>
<input type="text" name="name" class="form-control" value="{{ device.name }}" required>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-semibold">{% trans "Type" %}</label>
<select name="device_type" class="form-select">
<option value="counter" {% if device.device_type == 'counter' %}selected{% endif %}>{% trans "POS Counter" %}</option>
<option value="printer" {% if device.device_type == 'printer' %}selected{% endif %}>{% trans "Printer" %}</option>
<option value="scanner" {% if device.device_type == 'scanner' %}selected{% endif %}>{% trans "Scanner" %}</option>
<option value="scale" {% if device.device_type == 'scale' %}selected{% endif %}>{% trans "Weight Scale" %}</option>
<option value="display" {% if device.device_type == 'display' %}selected{% endif %}>{% trans "Customer Display" %}</option>
<option value="other" {% if device.device_type == 'other' %}selected{% endif %}>{% trans "Other" %}</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">{% trans "Connection" %}</label>
<select name="connection_type" class="form-select">
<option value="network" {% if device.connection_type == 'network' %}selected{% endif %}>{% trans "Network (IP)" %}</option>
<option value="usb" {% if device.connection_type == 'usb' %}selected{% endif %}>{% trans "USB" %}</option>
<option value="bluetooth" {% if device.connection_type == 'bluetooth' %}selected{% endif %}>{% trans "Bluetooth" %}</option>
</select>
</div>
<div class="col-md-8">
<label class="form-label fw-semibold">{% trans "IP Address" %}</label>
<input type="text" name="ip_address" class="form-control" value="{{ device.ip_address|default:'' }}">
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">{% trans "Port" %}</label>
<input type="number" name="port" class="form-control" value="{{ device.port|default:'' }}">
</div>
</div>
<div class="form-check form-switch mt-3">
<input class="form-check-input" type="checkbox" name="is_active" {% if device.is_active %}checked{% endif %}>
<label class="form-check-label">{% trans "Active" %}</label>
</div>
</div>
<div class="modal-footer bg-light border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-primary">{% trans "Save Changes" %}</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Device Modal -->
<div class="modal fade" id="deleteDeviceModal{{ device.id }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0">
<div class="modal-header">
<h5 class="modal-title fw-bold">{% trans "Confirm Delete" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>{% trans "Are you sure you want to delete" %} <strong>{{ device.name }}</strong>?</p>
</div>
<div class="modal-footer bg-light border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<a href="{% url 'delete_device' device.id %}" class="btn btn-danger">{% trans "Delete" %}</a>
</div>
</div>
</div>
</div>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="5" class="text-center py-5"> <td colspan="5" class="text-center py-5">
@ -509,53 +432,6 @@
</button> </button>
</td> </td>
</tr> </tr>
<!-- Edit Tier Modal -->
<div class="modal fade" id="editTierModal{{ tier.id }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0">
<form action="{% url 'edit_loyalty_tier' tier.id %}" method="post">
{% csrf_token %}
<div class="modal-header">
<h5 class="modal-title fw-bold">{% trans "Edit Loyalty Tier" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-semibold">{% trans "Name (EN)" %}</label>
<input type="text" name="name_en" class="form-control" value="{{ tier.name_en }}" required>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">{% trans "Name (AR)" %}</label>
<input type="text" name="name_ar" class="form-control" value="{{ tier.name_ar }}" required>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">{% trans "Min. Points" %}</label>
<input type="number" name="min_points" class="form-control" value="{{ tier.min_points }}" required>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">{% trans "Point Multiplier" %}</label>
<input type="number" step="0.01" name="point_multiplier" class="form-control" value="{{ tier.point_multiplier }}" required>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">{% trans "Discount (%)" %}</label>
<input type="number" step="0.01" name="discount_percentage" class="form-control" value="{{ tier.discount_percentage }}" required>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">{% trans "Color Code" %}</label>
<input type="color" name="color_code" class="form-control form-control-color w-100" value="{{ tier.color_code }}">
</div>
</div>
</div>
<div class="modal-footer bg-light border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-primary">{% trans "Save Changes" %}</button>
</div>
</form>
</div>
</div>
</div>
{% empty %} {% empty %}
<tr> <tr>
<td colspan="5" class="text-center py-5"> <td colspan="5" class="text-center py-5">
@ -658,6 +534,217 @@
</div> </div>
</div> </div>
<!-- ================= MODALS SECTION ================= -->
<!-- Payment Method Modals -->
{% for pm in payment_methods %}
<!-- Edit Modal -->
<div class="modal fade" id="editPaymentModal{{ pm.id }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0">
<form action="{% url 'edit_payment_method' pm.id %}" method="post">
{% csrf_token %}
<div class="modal-header">
<h5 class="modal-title fw-bold">{% trans "Edit Payment Method" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-semibold">{% trans "Name (English)" %}</label>
<input type="text" name="name_en" class="form-control" value="{{ pm.name_en }}" required>
</div>
<div class="mb-3">
<label class="form-label fw-semibold">{% trans "Name (Arabic)" %}</label>
<input type="text" name="name_ar" class="form-control" value="{{ pm.name_ar }}" required>
</div>
<div class="form-check form-switch">
<input class="form-check-input" type="checkbox" name="is_active" {% if pm.is_active %}checked{% endif %}>
<label class="form-check-label">{% trans "Active" %}</label>
</div>
</div>
<div class="modal-footer bg-light border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-primary">{% trans "Save Changes" %}</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Modal -->
<div class="modal fade" id="deletePaymentModal{{ pm.id }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0">
<div class="modal-header">
<h5 class="modal-title fw-bold">{% trans "Confirm Delete" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>{% trans "Are you sure you want to delete" %} <strong>{{ pm.name_en }}</strong>?</p>
<div class="alert alert-warning mb-0">
<i class="bi bi-exclamation-triangle me-2"></i>
{% trans "Deleting this will not affect historical records but it will no longer be available for new transactions." %}
</div>
</div>
<div class="modal-footer bg-light border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<a href="{% url 'delete_payment_method' pm.id %}" class="btn btn-danger">{% trans "Delete" %}</a>
</div>
</div>
</div>
</div>
{% endfor %}
<!-- Device Modals -->
{% for device in devices %}
<!-- Edit Device Modal -->
<div class="modal fade" id="editDeviceModal{{ device.id }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0">
<form action="{% url 'edit_device' device.id %}" method="post">
{% csrf_token %}
<div class="modal-header">
<h5 class="modal-title fw-bold">{% trans "Edit Device" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label class="form-label fw-semibold">{% trans "Device Name" %}</label>
<input type="text" name="name" class="form-control" value="{{ device.name }}" required>
</div>
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-semibold">{% trans "Type" %}</label>
<select name="device_type" class="form-select">
<option value="counter" {% if device.device_type == 'counter' %}selected{% endif %}>{% trans "POS Counter" %}</option>
<option value="printer" {% if device.device_type == 'printer' %}selected{% endif %}>{% trans "Printer" %}</option>
<option value="scanner" {% if device.device_type == 'scanner' %}selected{% endif %}>{% trans "Scanner" %}</option>
<option value="scale" {% if device.device_type == 'scale' %}selected{% endif %}>{% trans "Weight Scale" %}</option>
<option value="display" {% if device.device_type == 'display' %}selected{% endif %}>{% trans "Customer Display" %}</option>
<option value="other" {% if device.device_type == 'other' %}selected{% endif %}>{% trans "Other" %}</option>
</select>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">{% trans "Connection" %}</label>
<select name="connection_type" class="form-select">
<option value="network" {% if device.connection_type == 'network' %}selected{% endif %}>{% trans "Network (IP)" %}</option>
<option value="usb" {% if device.connection_type == 'usb' %}selected{% endif %}>{% trans "USB" %}</option>
<option value="bluetooth" {% if device.connection_type == 'bluetooth' %}selected{% endif %}>{% trans "Bluetooth" %}</option>
</select>
</div>
<div class="col-md-8">
<label class="form-label fw-semibold">{% trans "IP Address" %}</label>
<input type="text" name="ip_address" class="form-control" value="{{ device.ip_address|default:'' }}">
</div>
<div class="col-md-4">
<label class="form-label fw-semibold">{% trans "Port" %}</label>
<input type="number" name="port" class="form-control" value="{{ device.port|default:'' }}">
</div>
</div>
<div class="form-check form-switch mt-3">
<input class="form-check-input" type="checkbox" name="is_active" {% if device.is_active %}checked{% endif %}>
<label class="form-check-label">{% trans "Active" %}</label>
</div>
</div>
<div class="modal-footer bg-light border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-primary">{% trans "Save Changes" %}</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Device Modal -->
<div class="modal fade" id="deleteDeviceModal{{ device.id }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0">
<div class="modal-header">
<h5 class="modal-title fw-bold">{% trans "Confirm Delete" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>{% trans "Are you sure you want to delete" %} <strong>{{ device.name }}</strong>?</p>
</div>
<div class="modal-footer bg-light border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<a href="{% url 'delete_device' device.id %}" class="btn btn-danger">{% trans "Delete" %}</a>
</div>
</div>
</div>
</div>
{% endfor %}
<!-- Loyalty Tier Modals -->
{% for tier in loyalty_tiers %}
<!-- Edit Tier Modal -->
<div class="modal fade" id="editTierModal{{ tier.id }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0">
<form action="{% url 'edit_loyalty_tier' tier.id %}" method="post">
{% csrf_token %}
<div class="modal-header">
<h5 class="modal-title fw-bold">{% trans "Edit Loyalty Tier" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="row g-3">
<div class="col-md-6">
<label class="form-label fw-semibold">{% trans "Name (EN)" %}</label>
<input type="text" name="name_en" class="form-control" value="{{ tier.name_en }}" required>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">{% trans "Name (AR)" %}</label>
<input type="text" name="name_ar" class="form-control" value="{{ tier.name_ar }}" required>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">{% trans "Min. Points" %}</label>
<input type="number" name="min_points" class="form-control" value="{{ tier.min_points }}" required>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">{% trans "Point Multiplier" %}</label>
<input type="number" step="0.01" name="point_multiplier" class="form-control" value="{{ tier.point_multiplier }}" required>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">{% trans "Discount (%)" %}</label>
<input type="number" step="0.01" name="discount_percentage" class="form-control" value="{{ tier.discount_percentage }}" required>
</div>
<div class="col-md-6">
<label class="form-label fw-semibold">{% trans "Color Code" %}</label>
<input type="color" name="color_code" class="form-control form-control-color w-100" value="{{ tier.color_code }}">
</div>
</div>
</div>
<div class="modal-footer bg-light border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<button type="submit" class="btn btn-primary">{% trans "Save Changes" %}</button>
</div>
</form>
</div>
</div>
</div>
<!-- Delete Tier Modal (Optional, based on original code needing it, but not shown in original snippet for tiers, adding for consistency if button exists) -->
<div class="modal fade" id="deleteTierModal{{ tier.id }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0">
<div class="modal-header">
<h5 class="modal-title fw-bold">{% trans "Confirm Delete" %}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<p>{% trans "Are you sure you want to delete this tier?" %}</p>
</div>
<div class="modal-footer bg-light border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
<a href="{% url 'delete_loyalty_tier' tier.id %}" class="btn btn-danger">{% trans "Delete" %}</a>
</div>
</div>
</div>
</div>
{% endfor %}
<!-- Add Tier Modal --> <!-- Add Tier Modal -->
<div class="modal fade" id="addTierModal" tabindex="-1"> <div class="modal fade" id="addTierModal" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
@ -787,7 +874,7 @@
</div> </div>
<div class="modal-footer bg-light border-0"> <div class="modal-footer bg-light border-0">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{% trans "Close" %}</button>
<button type="button" class="btn btn-primary">{% trans "Save Device" %}</button> <button type="submit" class="btn btn-primary">{% trans "Save Device" %}</button>
</div> </div>
</form> </form>
</div> </div>

View File

@ -141,6 +141,8 @@ urlpatterns = [
# WhatsApp # WhatsApp
path('api/send-invoice-whatsapp/', views.send_invoice_whatsapp, name='send_invoice_whatsapp'), path('api/send-invoice-whatsapp/', views.send_invoice_whatsapp, name='send_invoice_whatsapp'),
path('api/send-quotation-whatsapp/', views.send_quotation_whatsapp, name='send_quotation_whatsapp'),
path('api/send-lpo-whatsapp/', views.send_lpo_whatsapp, name='send_lpo_whatsapp'),
path('api/test-whatsapp/', views.test_whatsapp_connection, name='test_whatsapp_connection'), path('api/test-whatsapp/', views.test_whatsapp_connection, name='test_whatsapp_connection'),
# Devices # Devices

View File

@ -80,6 +80,10 @@ def send_whatsapp_message(phone, message):
payload = {"phone": phone, "message": message} payload = {"phone": phone, "message": message}
response = requests.post(url, data=payload, headers=headers, timeout=10) response = requests.post(url, data=payload, headers=headers, timeout=10)
# Debug Logging
print(f"Wablas Send Message Response: {response.status_code} - {response.text}")
data = response.json() data = response.json()
if response.status_code == 200 and data.get('status') == True: if response.status_code == 200 and data.get('status') == True:
return True, "Message sent successfully." return True, "Message sent successfully."
@ -116,6 +120,10 @@ def send_whatsapp_document(phone, document_url, caption=""):
} }
response = requests.post(url, data=payload, headers=headers, timeout=15) response = requests.post(url, data=payload, headers=headers, timeout=15)
# Debug Logging
print(f"Wablas Send Document Response: {response.status_code} - {response.text}")
data = response.json() data = response.json()
if response.status_code == 200 and data.get('status') == True: if response.status_code == 200 and data.get('status') == True:
return True, "Document sent successfully." return True, "Document sent successfully."

View File

@ -43,16 +43,6 @@ logger = logging.getLogger(__name__)
@login_required @login_required
def index(request): def index(request):
# Auto-Fix Migration on Home Page Load (Temporary)
try:
from django.core.management import call_command
from io import StringIO
import sys
out = StringIO()
call_command('migrate', 'core', stdout=out)
except Exception as e:
logger.error(f"Migration Fix Failed: {e}")
settings = SystemSetting.objects.first() settings = SystemSetting.objects.first()
if not settings: if not settings:
settings = SystemSetting.objects.create() settings = SystemSetting.objects.create()
@ -234,9 +224,9 @@ def settings_view(request):
# Handle WhatsApp update manually to avoid validation errors on other fields # Handle WhatsApp update manually to avoid validation errors on other fields
settings.wablas_enabled = request.POST.get('wablas_enabled') == 'on' settings.wablas_enabled = request.POST.get('wablas_enabled') == 'on'
settings.wablas_token = request.POST.get('wablas_token', '') settings.wablas_token = request.POST.get('wablas_token', '').strip()
settings.wablas_server_url = request.POST.get('wablas_server_url', '') settings.wablas_server_url = request.POST.get('wablas_server_url', '').strip()
settings.wablas_secret_key = request.POST.get('wablas_secret_key', '') settings.wablas_secret_key = request.POST.get('wablas_secret_key', '').strip()
settings.save() settings.save()
messages.success(request, _("WhatsApp settings updated successfully.")) messages.success(request, _("WhatsApp settings updated successfully."))
return redirect(reverse('settings') + '#whatsapp') return redirect(reverse('settings') + '#whatsapp')
@ -1363,7 +1353,31 @@ def delete_device(request, pk):
@login_required @login_required
def test_whatsapp_connection(request): def test_whatsapp_connection(request):
return JsonResponse({'success': True, 'message': 'Connected'}) if request.method != 'POST':
return JsonResponse({'success': False, 'error': 'Invalid method'})
try:
data = json.loads(request.body)
phone = data.get('phone')
if not phone:
return JsonResponse({'success': False, 'error': 'Phone number required'})
success, msg = send_whatsapp_message(phone, "Test message from Smart Admin")
if success:
return JsonResponse({'success': True, 'message': msg})
else:
# Enhanced Error Handling for Common Wablas Issues
error_msg = str(msg)
if "Access denied" in error_msg and "IP" in error_msg:
error_msg += " <br><b>Action Required:</b> Go to Wablas Dashboard > Security and whitelist the IP shown in this error."
return JsonResponse({'success': False, 'error': error_msg})
except Exception as e:
logger.error(f"WhatsApp Test Error: {e}")
return JsonResponse({'success': False, 'error': str(e)})
@login_required @login_required
def send_invoice_whatsapp(request): def send_invoice_whatsapp(request):
@ -1432,11 +1446,157 @@ def send_invoice_whatsapp(request):
if success: if success:
return JsonResponse({'success': True, 'message': response_msg}) return JsonResponse({'success': True, 'message': response_msg})
else: else:
return JsonResponse({'success': False, 'error': response_msg}) # Enhanced Error Handling
error_msg = str(response_msg)
if "Access denied" in error_msg and "IP" in error_msg:
error_msg += " <br><b>Action Required:</b> Go to Wablas Dashboard > Security and whitelist the IP shown in this error."
return JsonResponse({'success': False, 'error': error_msg})
except Exception as e: except Exception as e:
logger.error(f"WhatsApp Error: {e}") logger.error(f"WhatsApp Error: {e}")
return JsonResponse({'success': False, 'error': str(e)}) # Changed to str(e) for clarity return JsonResponse({'success': False, 'error': str(e)})
@login_required
def send_quotation_whatsapp(request):
if request.method != 'POST':
return JsonResponse({'success': False, 'error': 'Method not allowed'})
try:
# Handle JSON payload
data = json.loads(request.body)
quotation_id = data.get('quotation_id')
phone = data.get('phone')
pdf_data = data.get('pdf_data') # Base64 string
except json.JSONDecodeError:
# Fallback to Form Data
quotation_id = request.POST.get('quotation_id')
phone = request.POST.get('phone')
pdf_data = None
if not quotation_id:
return JsonResponse({'success': False, 'error': 'Quotation ID missing'})
quotation = get_object_or_404(Quotation, pk=quotation_id)
if not phone:
if quotation.customer and quotation.customer.phone:
phone = quotation.customer.phone
else:
return JsonResponse({'success': False, 'error': 'Phone number missing'})
try:
# If PDF data is present, save and send document
if pdf_data:
# Remove header if present (data:application/pdf;base64,)
if ',' in pdf_data:
pdf_data = pdf_data.split(',')[1]
file_data = base64.b64decode(pdf_data)
dir_path = os.path.join(django_settings.MEDIA_ROOT, 'temp_quotations')
os.makedirs(dir_path, exist_ok=True)
filename = f"quotation_{quotation.id}_{int(timezone.now().timestamp())}.pdf"
file_path = os.path.join(dir_path, filename)
with open(file_path, 'wb') as f:
f.write(file_data)
# Construct URL
file_url = request.build_absolute_uri(django_settings.MEDIA_URL + 'temp_quotations/' + filename)
success, response_msg = send_whatsapp_document(phone, file_url, caption=f"Quotation #{quotation.quotation_number or quotation.id}")
else:
# Fallback to Text Link
# Note: You might need a public view for quotation if you want to send a link
# For now, we assume PDF is the primary method
message = (
f"Hello {quotation.customer.name if quotation.customer else 'Guest'},\n"
f"Here is your quotation #{quotation.quotation_number or quotation.id}.\n"
f"Total: {quotation.total_amount}\n"
f"Thank you!"
)
success, response_msg = send_whatsapp_message(phone, message)
if success:
return JsonResponse({'success': True, 'message': response_msg})
else:
# Enhanced Error Handling
error_msg = str(response_msg)
if "Access denied" in error_msg and "IP" in error_msg:
error_msg += " <br><b>Action Required:</b> Go to Wablas Dashboard > Security and whitelist the IP shown in this error."
return JsonResponse({'success': False, 'error': error_msg})
except Exception as e:
logger.error(f"WhatsApp Error: {e}")
return JsonResponse({'success': False, 'error': str(e)})
@login_required
def send_lpo_whatsapp(request):
if request.method != 'POST':
return JsonResponse({'success': False, 'error': 'Method not allowed'})
try:
data = json.loads(request.body)
lpo_id = data.get('lpo_id')
phone = data.get('phone')
pdf_data = data.get('pdf_data')
except json.JSONDecodeError:
lpo_id = request.POST.get('lpo_id')
phone = request.POST.get('phone')
pdf_data = None
if not lpo_id:
return JsonResponse({'success': False, 'error': 'LPO ID missing'})
lpo = get_object_or_404(PurchaseOrder, pk=lpo_id)
if not phone:
if lpo.supplier and lpo.supplier.phone:
phone = lpo.supplier.phone
else:
return JsonResponse({'success': False, 'error': 'Phone number missing'})
try:
if pdf_data:
if ',' in pdf_data:
pdf_data = pdf_data.split(',')[1]
file_data = base64.b64decode(pdf_data)
dir_path = os.path.join(django_settings.MEDIA_ROOT, 'temp_lpos')
os.makedirs(dir_path, exist_ok=True)
filename = f"lpo_{lpo.id}_{int(timezone.now().timestamp())}.pdf"
file_path = os.path.join(dir_path, filename)
with open(file_path, 'wb') as f:
f.write(file_data)
file_url = request.build_absolute_uri(django_settings.MEDIA_URL + 'temp_lpos/' + filename)
success, response_msg = send_whatsapp_document(phone, file_url, caption=f"LPO #{lpo.lpo_number or lpo.id}")
if success and lpo.status == 'draft':
lpo.status = 'sent'
lpo.save()
else:
success, response_msg = send_whatsapp_message(phone, f"Here is LPO #{lpo.lpo_number or lpo.id}")
if success:
return JsonResponse({'success': True, 'message': response_msg})
else:
error_msg = str(response_msg)
if "Access denied" in error_msg and "IP" in error_msg:
error_msg += " <br><b>Action Required:</b> Go to Wablas Dashboard > Security and whitelist the IP shown in this error."
return JsonResponse({'success': False, 'error': error_msg})
except Exception as e:
logger.error(f"WhatsApp Error: {e}")
return JsonResponse({'success': False, 'error': str(e)})
# --- LPO --- # --- LPO ---
@login_required @login_required

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

2
staticfiles/js/JsBarcode.all.min.js vendored Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB