adding backup/restore

This commit is contained in:
Flatlogic Bot 2026-02-11 09:47:21 +00:00
parent a30bc16ccc
commit bf533af9e8
33 changed files with 8585 additions and 564 deletions

View File

@ -0,0 +1,50 @@
# Generated by Django 5.2.7 on 2026-02-11 08:39
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('core', '0034_systemsetting_favicon'),
]
operations = [
migrations.RemoveField(
model_name='heldsale',
name='customer',
),
migrations.RemoveField(
model_name='heldsale',
name='notes',
),
migrations.RemoveField(
model_name='heldsale',
name='total_amount',
),
migrations.AddField(
model_name='heldsale',
name='customer_name',
field=models.CharField(blank=True, max_length=200, verbose_name='Customer Name'),
),
migrations.AddField(
model_name='heldsale',
name='note',
field=models.TextField(blank=True, verbose_name='Note'),
),
migrations.AlterField(
model_name='customer',
name='phone',
field=models.CharField(blank=True, default='968', max_length=20, verbose_name='Phone'),
),
migrations.AlterField(
model_name='heldsale',
name='cart_data',
field=models.TextField(verbose_name='Cart Data'),
),
migrations.AlterField(
model_name='supplier',
name='phone',
field=models.CharField(blank=True, default='968', max_length=20, verbose_name='Phone'),
),
]

View File

@ -61,7 +61,7 @@ class LoyaltyTier(models.Model):
class Customer(models.Model):
name = models.CharField(_("Name"), max_length=200)
phone = models.CharField(_("Phone"), max_length=20, blank=True)
phone = models.CharField(_("Phone"), max_length=20, blank=True, default='968')
email = models.EmailField(_("Email"), blank=True)
address = models.TextField(_("Address"), blank=True)
loyalty_points = models.DecimalField(_("Loyalty Points"), max_digits=15, decimal_places=2, default=0)
@ -96,7 +96,7 @@ class LoyaltyTransaction(models.Model):
class Supplier(models.Model):
name = models.CharField(_("Name"), max_length=200)
contact_person = models.CharField(_("Contact Person"), max_length=200, blank=True)
phone = models.CharField(_("Phone"), max_length=20, blank=True)
phone = models.CharField(_("Phone"), max_length=20, blank=True, default='968')
# created_at = models.DateTimeField(auto_now_add=True) <-- Removed to fix DB mismatch
def __str__(self):

View File

@ -12,9 +12,6 @@
<p class="text-muted small mb-0">{% trans "Welcome back! Here's what's happening with your business today." %}</p>
</div>
<div class="col-auto">
<a href="{% url 'fix_db' %}" class="btn btn-warning shadow-sm me-2" target="_blank">
<i class="bi bi-tools me-2"></i> {% trans "Fix Database" %}
</a>
<a href="{% url 'invoice_create' %}" class="btn btn-primary shadow-sm">
<i class="bi bi-plus-lg me-2"></i> {% trans "New Sale" %}
</a>

View File

@ -21,7 +21,7 @@
</button>
</div>
{% endif %}
<button onclick="downloadPDF()" class="btn btn-outline-primary rounded-3 px-4">
<button onclick="location.href='{% url 'download_invoice_pdf' sale.id %}'" class="btn btn-outline-primary rounded-3 px-4">
<i class="bi bi-file-earmark-pdf me-2"></i>{% trans "Download PDF" %} / تحميل PDF
</button>
<button onclick="window.print()" class="btn btn-primary rounded-3 px-4 shadow-sm">
@ -248,59 +248,7 @@
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<script>
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 style = getCompactStyle();
document.head.appendChild(style);
// Wait for fonts
await document.fonts.ready;
await new Promise(resolve => setTimeout(resolve, 500));
const opt = {
margin: [10, 10, 10, 10],
filename: 'Invoice_{{ sale.invoice_number|default:sale.id }}.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, letterRendering: false },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
};
html2pdf().set(opt).from(element).save().then(() => {
style.remove();
});
}
async function sendWhatsAppDirect() {
const phone = document.getElementById('whatsappPhoneDirect').value;
const spinner = document.getElementById('whatsappSpinnerDirect');
@ -314,26 +262,7 @@ async function sendWhatsAppDirect() {
spinner.classList.remove('d-none');
btn.disabled = true;
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 = {
margin: [10, 10, 10, 10],
filename: 'Invoice_{{ sale.invoice_number|default:sale.id }}.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, letterRendering: false },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
};
// Generate PDF as base64
const pdfBlob = await html2pdf().set(opt).from(element).outputPdf('datauristring');
const response = await fetch("{% url 'send_invoice_whatsapp' %}", {
method: 'POST',
headers: {
@ -342,8 +271,7 @@ async function sendWhatsAppDirect() {
},
body: JSON.stringify({
sale_id: {{ sale.id }},
phone: phone,
pdf_data: pdfBlob
phone: phone
})
});
@ -360,10 +288,10 @@ async function sendWhatsAppDirect() {
} finally {
spinner.classList.add('d-none');
btn.disabled = false;
style.remove();
}
}
<style>
@media print {
@page {

View File

@ -240,7 +240,6 @@
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<script>
function openWhatsAppModal(saleId, phone, invoiceNum) {
document.getElementById('waSaleId').value = saleId;
@ -263,93 +262,8 @@ async function sendWhatsAppFromList() {
btn.disabled = true;
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 {
// 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
// Call Backend API to generate and send PDF
const response = await fetch("{% url 'send_invoice_whatsapp' %}", {
method: 'POST',
headers: {
@ -358,8 +272,7 @@ async function sendWhatsAppFromList() {
},
body: JSON.stringify({
sale_id: saleId,
phone: phone,
pdf_data: pdfBlob
phone: phone
})
});
@ -376,12 +289,11 @@ async function sendWhatsAppFromList() {
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 invoice.' %}");
alert("{% trans 'An error occurred while sending the invoice.' %}");
console.error(e);
} finally {
btn.disabled = false;
spinner.classList.add('d-none');
document.body.removeChild(tempContainer);
}
}
</script>

View File

@ -19,7 +19,7 @@
</button>
</form>
{% endif %}
<button onclick="downloadPDF()" class="btn btn-outline-primary rounded-3 px-4">
<button onclick="location.href='{% url 'download_lpo_pdf' order.id %}'" class="btn btn-outline-primary rounded-3 px-4">
<i class="bi bi-file-earmark-pdf me-2"></i>{% trans "Download PDF" %} / تحميل PDF
</button>
<button onclick="window.print()" class="btn btn-primary rounded-3 px-4 shadow-sm">
@ -167,58 +167,6 @@
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<script>
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 style = getCompactStyle();
document.head.appendChild(style);
// Wait for fonts
await document.fonts.ready;
await new Promise(resolve => setTimeout(resolve, 500));
const opt = {
margin: [10, 10, 10, 10],
filename: 'LPO_{{ order.lpo_number|default:order.id }}.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, letterRendering: false },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
};
html2pdf().set(opt).from(element).save().then(() => {
style.remove();
});
}
</script>
<style>
@media print {

View File

@ -160,7 +160,6 @@
</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;
@ -183,93 +182,8 @@ async function sendWhatsAppFromList() {
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
// Call Backend API to generate and send PDF
const apiResponse = await fetch("{% url 'send_lpo_whatsapp' %}", {
method: 'POST',
headers: {
@ -278,8 +192,7 @@ async function sendWhatsAppFromList() {
},
body: JSON.stringify({
lpo_id: lpoId,
phone: phone,
pdf_data: pdfBlob
phone: phone
})
});
@ -292,16 +205,17 @@ async function sendWhatsAppFromList() {
if (modalInstance) {
modalInstance.hide();
}
// Reload to update status if needed
window.location.reload();
} 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.' %}");
alert("{% trans 'An error occurred while sending the LPO.' %}");
console.error(e);
} finally {
btn.disabled = false;
spinner.classList.add('d-none');
document.body.removeChild(tempContainer);
}
}
</script>

View File

@ -16,7 +16,7 @@
<i class="bi bi-arrow-right-circle me-2"></i>{% trans "Convert to Invoice" %} / تحويل إلى فاتورة
</button>
{% endif %}
<button onclick="downloadPDF()" class="btn btn-outline-primary rounded-3 px-4">
<button onclick="location.href='{% url 'download_quotation_pdf' quotation.id %}'" class="btn btn-outline-primary rounded-3 px-4">
<i class="bi bi-file-earmark-pdf me-2"></i>{% trans "Download PDF" %} / تحميل PDF
</button>
<button onclick="window.print()" class="btn btn-primary rounded-3 px-4 shadow-sm">
@ -211,59 +211,6 @@
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2pdf.js/0.10.1/html2pdf.bundle.min.js"></script>
<script>
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 style = getCompactStyle();
document.head.appendChild(style);
// Wait for fonts
await document.fonts.ready;
await new Promise(resolve => setTimeout(resolve, 500));
const opt = {
margin: [10, 10, 10, 10],
filename: 'Quotation_{{ quotation.quotation_number|default:quotation.id }}.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, letterRendering: false },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' }
};
html2pdf().set(opt).from(element).save().then(() => {
style.remove();
});
}
</script>
<style>
@media print {

View File

@ -175,7 +175,6 @@
</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;
@ -198,93 +197,8 @@ async function sendWhatsAppFromList() {
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
// Call Backend API to generate and send PDF
const response = await fetch("{% url 'send_quotation_whatsapp' %}", {
method: 'POST',
headers: {
@ -293,8 +207,7 @@ async function sendWhatsAppFromList() {
},
body: JSON.stringify({
quotation_id: quotationId,
phone: phone,
pdf_data: pdfBlob
phone: phone
})
});
@ -311,12 +224,11 @@ async function sendWhatsAppFromList() {
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.' %}");
alert("{% trans 'An error occurred while sending the quotation.' %}");
console.error(e);
} finally {
btn.disabled = false;
spinner.classList.add('d-none');
document.body.removeChild(tempContainer);
}
}
</script>

View File

@ -61,6 +61,11 @@
<i class="bi bi-whatsapp me-2"></i>{% trans "WhatsApp Gateway" %}
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link fw-bold px-4" id="database-tab" data-bs-toggle="pill" data-bs-target="#database" type="button" role="tab">
<i class="bi bi-database me-2"></i>{% trans "Database" %}
</button>
</li>
</ul>
<div class="tab-content" id="settingsTabsContent">
@ -531,6 +536,48 @@
</div>
</div>
</div>
<!-- Database Tab -->
<div class="tab-pane fade" id="database" role="tabpanel">
<div class="row">
<div class="col-lg-6">
<div class="card shadow-sm border-0 glassmorphism mb-4">
<div class="card-header bg-transparent border-0 py-3">
<h5 class="card-title mb-0 fw-bold">{% trans "Backup Database" %}</h5>
</div>
<div class="card-body">
<p class="text-muted">{% trans "Download a full SQL dump of your database. Keep this file safe." %}</p>
<a href="{% url 'backup_database' %}" class="btn btn-primary w-100">
<i class="bi bi-download me-2"></i> {% trans "Download Backup (.sql)" %}
</a>
</div>
</div>
</div>
<div class="col-lg-6">
<div class="card shadow-sm border-0 glassmorphism mb-4">
<div class="card-header bg-transparent border-0 py-3">
<h5 class="card-title mb-0 fw-bold text-danger">{% trans "Restore Database" %}</h5>
</div>
<div class="card-body">
<div class="alert alert-warning">
<i class="bi bi-exclamation-triangle-fill me-2"></i>
<strong>{% trans "Warning:" %}</strong> {% trans "Restoring will OVERWRITE all current data. This cannot be undone." %}
</div>
<form action="{% url 'restore_database' %}" method="post" enctype="multipart/form-data" onsubmit="return confirm('{% trans "Are you sure? This will wipe the current database!" %}');">
{% csrf_token %}
<div class="mb-3">
<label class="form-label">{% trans "Select Backup File (.sql)" %}</label>
<input type="file" name="backup_file" class="form-control" accept=".sql" required>
</div>
<button type="submit" class="btn btn-danger w-100">
<i class="bi bi-upload me-2"></i> {% trans "Restore Database" %}
</button>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,52 @@
{% load static %}
<!DOCTYPE html>
<html lang="ar" dir="rtl">
<head>
<meta charset="UTF-8">
<title>{% block title %}Document{% endblock %}</title>
<style>
/* Use system fonts that support Arabic */
@font-face {
font-family: 'Noto Sans Arabic';
src: local('Noto Sans Arabic'), local('NotoSansArabic-Regular');
}
body {
font-family: 'Noto Sans Arabic', 'Arial', sans-serif !important;
font-size: 12px;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Noto Sans Arabic', 'Arial', sans-serif !important;
font-weight: bold;
}
@page {
size: A4;
margin: 1cm;
@bottom-center {
content: "Page " counter(page) " of " counter(pages);
font-family: 'Noto Sans Arabic', 'Arial', sans-serif;
font-size: 10px;
}
}
table {
width: 100%;
table-layout: fixed;
word-wrap: break-word;
}
.badge {
color: black !important;
border: 1px solid #ccc;
background: transparent !important;
}
/* WeasyPrint specific fixes */
img {
max-width: 100%;
}
</style>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.rtl.min.css">
{% block extra_css %}{% endblock %}
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,108 @@
{% extends "pdf/base_pdf.html" %}
{% load i18n %}
{% block title %}{% trans "Invoice" %} #{{ sale.invoice_number|default:sale.id }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col-6">
{% if settings.logo %}
<img src="{{ settings.logo.url }}" alt="Logo" style="max-height: 80px;" class="mb-3">
{% else %}
<h3 class="text-primary fw-bold">{{ settings.business_name }}</h3>
{% endif %}
<div class="small text-muted">
<div>{{ settings.address }}</div>
<div>{{ settings.phone }}</div>
<div>{{ settings.email }}</div>
{% if settings.vat_number %}
<div>{% trans "VAT" %}: {{ settings.vat_number }}</div>
{% endif %}
</div>
</div>
<div class="col-6 text-end">
<h1 class="text-muted text-uppercase mb-3">{% trans "Tax Invoice" %} / فاتورة ضريبية</h1>
<div class="mb-2"><strong>{% trans "Invoice #" %} / رقم الفاتورة:</strong> {{ sale.invoice_number|default:sale.id }}</div>
<div class="mb-2"><strong>{% trans "Date" %} / التاريخ:</strong> {{ sale.created_at|date:"Y-m-d H:i" }}</div>
</div>
</div>
<div class="row mb-4 border-top border-bottom py-3">
<div class="col-6">
<h6 class="text-uppercase text-muted fw-bold">{% trans "Bill To" %} / العميل</h6>
<h5 class="fw-bold">{{ sale.customer.name|default:"Guest" }}</h5>
{% if sale.customer.phone %}<div>{{ sale.customer.phone }}</div>{% endif %}
{% if sale.customer.address %}<div>{{ sale.customer.address }}</div>{% endif %}
</div>
<div class="col-6 text-end">
<span class="badge rounded-pill px-3 py-2">{{ sale.get_status_display }}</span>
</div>
</div>
<table class="table table-striped table-sm">
<thead>
<tr class="table-light">
<th style="width: 40%">{% trans "Item" %} / الصنف</th>
<th class="text-center">{% trans "Price" %} / السعر</th>
<th class="text-center">{% trans "Qty" %} / الكمية</th>
<th class="text-end">{% trans "Total" %} / المجموع</th>
</tr>
</thead>
<tbody>
{% for item in sale.items.all %}
<tr>
<td>
<div class="fw-bold">{{ item.product.name_ar }}</div>
<div class="small text-muted">{{ item.product.name_en }}</div>
</td>
<td class="text-center">{{ item.unit_price|floatformat:3 }}</td>
<td class="text-center">{{ item.quantity|floatformat:2 }}</td>
<td class="text-end">{{ item.line_total|floatformat:3 }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="2"></td>
<td class="text-center fw-bold">{% trans "Subtotal" %} / المجموع الفرعي</td>
<td class="text-end fw-bold">{{ sale.subtotal|floatformat:3 }}</td>
</tr>
{% if sale.discount > 0 %}
<tr>
<td colspan="2"></td>
<td class="text-center fw-bold text-danger">{% trans "Discount" %} / الخصم</td>
<td class="text-end fw-bold text-danger">-{{ sale.discount|floatformat:3 }}</td>
</tr>
{% endif %}
{% if sale.vat_amount > 0 %}
<tr>
<td colspan="2"></td>
<td class="text-center fw-bold">{% trans "VAT" %} / الضريبة</td>
<td class="text-end fw-bold">{{ sale.vat_amount|floatformat:3 }}</td>
</tr>
{% endif %}
<tr class="bg-light">
<td colspan="2"></td>
<td class="text-center fw-bold h6">{% trans "Total" %} / الإجمالي</td>
<td class="text-end fw-bold h6">{{ sale.total_amount|floatformat:3 }} {{ settings.currency_symbol }}</td>
</tr>
<tr>
<td colspan="2"></td>
<td class="text-center fw-bold">{% trans "Paid" %} / المدفوع</td>
<td class="text-end fw-bold">{{ sale.paid_amount|floatformat:3 }}</td>
</tr>
</tfoot>
</table>
{% if amount_in_words %}
<div class="mb-4 p-2 bg-light rounded border">
<strong>{% trans "Amount in Words" %} / المبلغ بالحروف:</strong> {{ amount_in_words }}
</div>
{% endif %}
<div class="text-center text-muted small mt-5 pt-3 border-top">
{% trans "Generated by" %} {{ settings.business_name }}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,88 @@
{% extends "pdf/base_pdf.html" %}
{% load i18n %}
{% block title %}{% trans "Purchase Order" %} #{{ lpo.lpo_number|default:lpo.id }}{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row mb-4">
<div class="col-6">
{% if settings.logo %}
<img src="{{ settings.logo.url }}" alt="Logo" style="max-height: 80px;" class="mb-3">
{% else %}
<h3 class="text-primary fw-bold">{{ settings.business_name }}</h3>
{% endif %}
<div class="small text-muted">
<div>{{ settings.address }}</div>
<div>{{ settings.phone }}</div>
<div>{{ settings.email }}</div>
</div>
</div>
<div class="col-6 text-end">
<h1 class="text-muted text-uppercase mb-3">{% trans "Purchase Order" %} / طلب شراء</h1>
<div class="mb-2"><strong>{% trans "LPO #" %} / رقم الطلب:</strong> {{ lpo.lpo_number|default:lpo.id }}</div>
<div class="mb-2"><strong>{% trans "Date" %} / التاريخ:</strong> {{ lpo.created_at|date:"Y-m-d" }}</div>
<div class="mb-2"><strong>{% trans "Expected" %} / المتوقع:</strong> {{ lpo.expected_date|date:"Y-m-d"|default:"-" }}</div>
</div>
</div>
<div class="row mb-4 border-top border-bottom py-3">
<div class="col-6">
<h6 class="text-uppercase text-muted fw-bold">{% trans "Vendor" %} / المورد</h6>
<h5 class="fw-bold">{{ lpo.supplier.name }}</h5>
{% if lpo.supplier.phone %}<div>{{ lpo.supplier.phone }}</div>{% endif %}
</div>
<div class="col-6 text-end">
<span class="badge rounded-pill px-3 py-2">{{ lpo.get_status_display }}</span>
</div>
</div>
<table class="table table-striped table-sm">
<thead>
<tr class="table-light">
<th style="width: 40%">{% trans "Item" %} / الصنف</th>
<th class="text-center">{% trans "Cost" %} / التكلفة</th>
<th class="text-center">{% trans "Qty" %} / الكمية</th>
<th class="text-end">{% trans "Total" %} / المجموع</th>
</tr>
</thead>
<tbody>
{% for item in lpo.items.all %}
<tr>
<td>
<div class="fw-bold">{{ item.product.name_ar }}</div>
<div class="small text-muted">{{ item.product.name_en }}</div>
</td>
<td class="text-center">{{ item.cost_price|floatformat:3 }}</td>
<td class="text-center">{{ item.quantity|floatformat:2 }}</td>
<td class="text-end">{{ item.line_total|floatformat:3 }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr class="bg-light">
<td colspan="2"></td>
<td class="text-center fw-bold h6">{% trans "Total" %} / الإجمالي</td>
<td class="text-end fw-bold h6">{{ lpo.total_amount|floatformat:3 }} {{ settings.currency_symbol }}</td>
</tr>
</tfoot>
</table>
{% if amount_in_words %}
<div class="mb-4 p-2 bg-light rounded border">
<strong>{% trans "Amount in Words" %} / المبلغ بالحروف:</strong> {{ amount_in_words }}
</div>
{% endif %}
{% if lpo.notes %}
<div class="mb-4">
<h6 class="text-uppercase text-muted fw-bold">{% trans "Notes" %} / ملاحظات</h6>
<div class="small">{{ lpo.notes }}</div>
</div>
{% endif %}
<div class="text-center text-muted small mt-5 pt-3 border-top">
{% trans "Generated by" %} {{ settings.business_name }}
</div>
</div>
{% endblock %}

View File

@ -0,0 +1,129 @@
{% extends "pdf/base_pdf.html" %}
{% load i18n %}
{% block title %}{% trans "Quotation" %} #{{ quotation.quotation_number|default:quotation.id }}{% endblock %}
{% block content %}
<div class="container-fluid">
<!-- Header -->
<div class="row mb-4">
<!-- Company Info (Right in RTL) -->
<div class="col-6">
{% if settings.logo %}
<img src="{{ settings.logo.url }}" alt="Logo" style="max-height: 80px;" class="mb-3">
{% else %}
<h3 class="text-primary fw-bold">{{ settings.business_name }}</h3>
{% endif %}
<div class="small text-muted">
<div>{{ settings.address }}</div>
<div>{{ settings.phone }}</div>
<div>{{ settings.email }}</div>
{% if settings.vat_number %}
<div>{% trans "VAT" %}: {{ settings.vat_number }}</div>
{% endif %}
</div>
</div>
<!-- Doc Info (Left in RTL) -->
<div class="col-6 text-end">
<h1 class="text-muted text-uppercase mb-3">{% trans "Quotation" %} / عرض سعر</h1>
<div class="mb-2">
<strong>{% trans "Quotation #" %} / رقم العرض:</strong> {{ quotation.quotation_number|default:quotation.id }}
</div>
<div class="mb-2">
<strong>{% trans "Date" %} / التاريخ:</strong> {{ quotation.created_at|date:"Y-m-d" }}
</div>
<div class="mb-2">
<strong>{% trans "Valid Until" %} / سارٍ حتى:</strong> {{ quotation.valid_until|date:"Y-m-d"|default:"-" }}
</div>
</div>
</div>
<!-- Customer & Status -->
<div class="row mb-4 border-top border-bottom py-3">
<div class="col-6">
<h6 class="text-uppercase text-muted fw-bold">{% trans "Bill To" %} / العميل</h6>
<h5 class="fw-bold">{{ quotation.customer.name|default:"Guest" }}</h5>
{% if quotation.customer.phone %}
<div>{{ quotation.customer.phone }}</div>
{% endif %}
{% if quotation.customer.address %}
<div>{{ quotation.customer.address }}</div>
{% endif %}
</div>
<div class="col-6 text-end">
<!-- Status badge text -->
<span class="badge rounded-pill px-3 py-2">
{% if quotation.status == 'converted' %}{% trans "Converted" %} / محول لفاتورة
{% elif quotation.status == 'accepted' %}{% trans "Accepted" %} / مقبول
{% elif quotation.status == 'rejected' %}{% trans "Rejected" %} / مرفوض
{% else %}{% trans "Open" %} / مفتوح
{% endif %}
</span>
</div>
</div>
<!-- Table -->
<table class="table table-striped table-sm">
<thead>
<tr class="table-light">
<th style="width: 40%">{% trans "Item" %} / الصنف</th>
<th class="text-center">{% trans "Price" %} / السعر</th>
<th class="text-center">{% trans "Qty" %} / الكمية</th>
<th class="text-end">{% trans "Total" %} / المجموع</th>
</tr>
</thead>
<tbody>
{% for item in quotation.items.all %}
<tr>
<td>
<div class="fw-bold">{{ item.product.name_ar }}</div>
<div class="small text-muted">{{ item.product.name_en }}</div>
</td>
<td class="text-center">{{ item.unit_price|floatformat:3 }}</td>
<td class="text-center">{{ item.quantity|floatformat:2 }}</td>
<td class="text-end">{{ item.line_total|floatformat:3 }}</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
<tr>
<td colspan="2"></td>
<td class="text-center fw-bold">{% trans "Subtotal" %} / المجموع الفرعي</td>
<td class="text-end fw-bold">{{ quotation.total_amount|add:quotation.discount|floatformat:3 }}</td>
</tr>
{% if quotation.discount > 0 %}
<tr>
<td colspan="2"></td>
<td class="text-center fw-bold text-danger">{% trans "Discount" %} / الخصم</td>
<td class="text-end fw-bold text-danger">-{{ quotation.discount|floatformat:3 }}</td>
</tr>
{% endif %}
<tr class="bg-light">
<td colspan="2"></td>
<td class="text-center fw-bold h6">{% trans "Total" %} / الإجمالي</td>
<td class="text-end fw-bold h6">{{ quotation.total_amount|floatformat:3 }} {{ settings.currency_symbol }}</td>
</tr>
</tfoot>
</table>
<!-- Amount in Words -->
{% if amount_in_words %}
<div class="mb-4 p-2 bg-light rounded border">
<strong>{% trans "Amount in Words" %} / المبلغ بالحروف:</strong> {{ amount_in_words }}
</div>
{% endif %}
<!-- Terms -->
{% if quotation.terms_and_conditions %}
<div class="mb-4">
<h6 class="text-uppercase text-muted fw-bold">{% trans "Terms & Conditions" %} / الشروط والأحكام</h6>
<div class="small" style="white-space: pre-line;">{{ quotation.terms_and_conditions }}</div>
</div>
{% endif %}
<!-- Footer -->
<div class="text-center text-muted small mt-5 pt-3 border-top">
{% trans "Generated by" %} {{ settings.business_name }}
</div>
</div>
{% endblock %}

View File

@ -163,4 +163,8 @@ urlpatterns = [
path('sessions/start/', views.start_session, name='start_session'),
path('sessions/close/', views.close_session, name='close_session'),
path('sessions/<int:pk>/', views.session_detail, name='session_detail'),
# Database Backup/Restore
path('settings/backup/', views.backup_database, name='backup_database'),
path('settings/restore/', views.restore_database, name='restore_database'),
]

View File

@ -3,7 +3,8 @@ from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth import update_session_auth_hash
from django.urls import reverse
from django.http import JsonResponse, HttpResponse
from django.http import JsonResponse, HttpResponse, FileResponse
import subprocess
from django.core.paginator import Paginator
from django.db import transaction
from django.db.models import Sum, Q, Count, F
@ -36,6 +37,8 @@ from .forms import (
)
from .utils import number_to_words_en, send_whatsapp_message, send_whatsapp_document
from .views_import import *
from weasyprint import HTML
from django.template.loader import render_to_string
logger = logging.getLogger(__name__)
@ -1379,22 +1382,56 @@ def test_whatsapp_connection(request):
logger.error(f"WhatsApp Test Error: {e}")
return JsonResponse({'success': False, 'error': str(e)})
# --- PDF & WhatsApp Helpers ---
def get_pdf_context(obj, doc_type):
settings = SystemSetting.objects.first()
if not settings:
settings = SystemSetting.objects.create()
amount = 0
if doc_type == 'quotation':
amount = obj.total_amount
elif doc_type == 'invoice':
amount = obj.total_amount
elif doc_type == 'lpo':
amount = obj.total_amount
return {
doc_type: obj,
'sale' if doc_type == 'invoice' else doc_type: obj,
'settings': settings,
'site_settings': settings,
'amount_in_words': number_to_words_en(amount)
}
def generate_pdf_file(template, context, request):
html_string = render_to_string(template, context, request=request)
base_url = request.build_absolute_uri('/')
return HTML(string=html_string, base_url=base_url).write_pdf()
@login_required
def download_invoice_pdf(request, pk):
sale = get_object_or_404(Sale, pk=pk)
context = get_pdf_context(sale, 'invoice')
pdf = generate_pdf_file('pdf/invoice_pdf.html', context, request)
response = HttpResponse(pdf, content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="Invoice_{sale.invoice_number or sale.id}.pdf"'
return response
@login_required
def send_invoice_whatsapp(request):
if request.method != 'POST':
return JsonResponse({'success': False, 'error': 'Method not allowed'})
try:
# Handle JSON payload
# Simple JSON handling
data = json.loads(request.body)
sale_id = data.get('sale_id')
phone = data.get('phone')
pdf_data = data.get('pdf_data') # Base64 string
except json.JSONDecodeError:
# Fallback to Form Data
sale_id = request.POST.get('sale_id')
phone = request.POST.get('phone')
pdf_data = None
if not sale_id:
return JsonResponse({'success': False, 'error': 'Sale ID missing'})
@ -1408,71 +1445,56 @@ def send_invoice_whatsapp(request):
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]
# Generate PDF Server-Side
context = get_pdf_context(sale, 'invoice')
pdf_bytes = generate_pdf_file('pdf/invoice_pdf.html', context, request)
# Save to temp
dir_path = os.path.join(django_settings.MEDIA_ROOT, 'temp_invoices')
os.makedirs(dir_path, exist_ok=True)
filename = f"invoice_{sale.id}_{int(timezone.now().timestamp())}.pdf"
file_path = os.path.join(dir_path, filename)
with open(file_path, 'wb') as f:
f.write(pdf_bytes)
file_data = base64.b64decode(pdf_data)
dir_path = os.path.join(django_settings.MEDIA_ROOT, 'temp_invoices')
os.makedirs(dir_path, exist_ok=True)
filename = f"invoice_{sale.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_invoices/' + filename)
success, response_msg = send_whatsapp_document(phone, file_url, caption=f"Invoice #{sale.invoice_number or sale.id}")
else:
# Fallback to Text Link
receipt_url = request.build_absolute_uri(reverse('sale_receipt', args=[sale.pk]))
message = (
f"Hello {sale.customer.name if sale.customer else 'Guest'},\n"
f"Here is your invoice #{sale.invoice_number or sale.id}.\n"
f"Total: {sale.total_amount}\n"
f"View Invoice: {receipt_url}\n"
f"Thank you for your business!"
)
success, response_msg = send_whatsapp_message(phone, message)
file_url = request.build_absolute_uri(django_settings.MEDIA_URL + 'temp_invoices/' + filename)
success, response_msg = send_whatsapp_document(phone, file_url, caption=f"Invoice #{sale.invoice_number or sale.id}")
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 download_quotation_pdf(request, pk):
quotation = get_object_or_404(Quotation, pk=pk)
context = get_pdf_context(quotation, 'quotation')
pdf = generate_pdf_file('pdf/quotation_pdf.html', context, request)
response = HttpResponse(pdf, content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="Quotation_{quotation.quotation_number or quotation.id}.pdf"'
return response
@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'})
@ -1486,54 +1508,42 @@ def send_quotation_whatsapp(request):
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]
context = get_pdf_context(quotation, 'quotation')
pdf_bytes = generate_pdf_file('pdf/quotation_pdf.html', context, request)
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(pdf_bytes)
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)
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}")
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 download_lpo_pdf(request, pk):
lpo = get_object_or_404(PurchaseOrder, pk=pk)
context = get_pdf_context(lpo, 'lpo')
pdf = generate_pdf_file('pdf/lpo_pdf.html', context, request)
response = HttpResponse(pdf, content_type='application/pdf')
response['Content-Disposition'] = f'attachment; filename="LPO_{lpo.lpo_number or lpo.id}.pdf"'
return response
@login_required
def send_lpo_whatsapp(request):
if request.method != 'POST':
@ -1543,11 +1553,9 @@ def send_lpo_whatsapp(request):
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'})
@ -1561,30 +1569,24 @@ def send_lpo_whatsapp(request):
return JsonResponse({'success': False, 'error': 'Phone number missing'})
try:
if pdf_data:
if ',' in pdf_data:
pdf_data = pdf_data.split(',')[1]
context = get_pdf_context(lpo, 'lpo')
pdf_bytes = generate_pdf_file('pdf/lpo_pdf.html', context, request)
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(pdf_bytes)
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}")
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()
if success:
return JsonResponse({'success': True, 'message': response_msg})
@ -2042,4 +2044,73 @@ def recall_held_sale_api(request, pk):
@login_required
def delete_held_sale_api(request, pk):
return JsonResponse({'success': True})
return JsonResponse({'success': True})
@login_required
def backup_database(request):
if not request.user.is_superuser:
messages.error(request, _("You are not authorized to perform this action."))
return redirect('settings')
db_settings = django_settings.DATABASES['default']
db_name = db_settings['NAME']
db_user = db_settings['USER']
db_password = db_settings['PASSWORD']
db_host = db_settings['HOST']
filename = f"backup_{db_name}_{timezone.now().strftime('%Y%m%d_%H%M%S')}.sql"
file_path = os.path.join(django_settings.BASE_DIR, 'tmp', filename)
os.makedirs(os.path.dirname(file_path), exist_ok=True)
# Use mysqldump
command = f"mysqldump -h {db_host} -u {db_user} -p'{db_password}' {db_name} > {file_path}"
try:
subprocess.check_call(command, shell=True)
response = FileResponse(open(file_path, 'rb'), content_type='application/sql')
response['Content-Disposition'] = f'attachment; filename="{filename}"'
return response
except Exception as e:
messages.error(request, f"Backup failed: {str(e)}")
return redirect('settings')
@login_required
def restore_database(request):
if not request.user.is_superuser:
messages.error(request, _("You are not authorized to perform this action."))
return redirect('settings')
if request.method == 'POST' and request.FILES.get('backup_file'):
backup_file = request.FILES['backup_file']
# Security check: Ensure it's a sql file
if not backup_file.name.endswith('.sql'):
messages.error(request, _("Invalid file format. Please upload a .sql file."))
return redirect('settings')
db_settings = django_settings.DATABASES['default']
db_name = db_settings['NAME']
db_user = db_settings['USER']
db_password = db_settings['PASSWORD']
db_host = db_settings['HOST']
# Save uploaded file temporarily
temp_path = os.path.join(django_settings.BASE_DIR, 'tmp', 'restore.sql')
os.makedirs(os.path.dirname(temp_path), exist_ok=True)
with open(temp_path, 'wb+') as destination:
for chunk in backup_file.chunks():
destination.write(chunk)
# Use mysql to restore
command = f"mysql -h {db_host} -u {db_user} -p'{db_password}' {db_name} < {temp_path}"
try:
subprocess.check_call(command, shell=True)
messages.success(request, _("Database restored successfully!"))
except Exception as e:
messages.error(request, f"Restore failed: {str(e)}")
finally:
if os.path.exists(temp_path):
os.remove(temp_path)
return redirect('settings')

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -4,3 +4,4 @@ python-dotenv==1.1.1
gunicorn==21.2.0
requests
openpyxl
WeasyPrint

1449
static/fonts/Cairo-Bold.ttf Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff