Autosave: 20260211-073630
This commit is contained in:
parent
48923270af
commit
a30bc16ccc
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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 {
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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 %}
|
||||||
@ -118,6 +123,186 @@
|
|||||||
{% include "core/pagination.html" with page_obj=orders %}
|
{% include "core/pagination.html" with page_obj=orders %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</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>
|
||||||
</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 %}
|
||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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 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>
|
</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 %}
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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."
|
||||||
|
|||||||
192
core/views.py
192
core/views.py
@ -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
|
||||||
|
|||||||
BIN
media/temp_invoices/invoice_26_1770793647.pdf
Normal file
BIN
media/temp_invoices/invoice_26_1770793647.pdf
Normal file
Binary file not shown.
BIN
media/temp_invoices/invoice_26_1770794941.pdf
Normal file
BIN
media/temp_invoices/invoice_26_1770794941.pdf
Normal file
Binary file not shown.
BIN
media/temp_invoices/invoice_26_1770795250.pdf
Normal file
BIN
media/temp_invoices/invoice_26_1770795250.pdf
Normal file
Binary file not shown.
BIN
media/temp_quotations/quotation_1_1770794329.pdf
Normal file
BIN
media/temp_quotations/quotation_1_1770794329.pdf
Normal file
Binary file not shown.
2
staticfiles/js/JsBarcode.all.min.js
vendored
Normal file
2
staticfiles/js/JsBarcode.all.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
staticfiles/pasted-20260210-182651-b15404c1.png
Normal file
BIN
staticfiles/pasted-20260210-182651-b15404c1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.8 KiB |
Loading…
x
Reference in New Issue
Block a user