adding backup/restore
This commit is contained in:
parent
a30bc16ccc
commit
bf533af9e8
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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'),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -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):
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
52
core/templates/pdf/base_pdf.html
Normal file
52
core/templates/pdf/base_pdf.html
Normal 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>
|
||||
108
core/templates/pdf/invoice_pdf.html
Normal file
108
core/templates/pdf/invoice_pdf.html
Normal 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 %}
|
||||
88
core/templates/pdf/lpo_pdf.html
Normal file
88
core/templates/pdf/lpo_pdf.html
Normal 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 %}
|
||||
129
core/templates/pdf/quotation_pdf.html
Normal file
129
core/templates/pdf/quotation_pdf.html
Normal 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 %}
|
||||
@ -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'),
|
||||
]
|
||||
|
||||
257
core/views.py
257
core/views.py
@ -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)
|
||||
|
||||
file_data = base64.b64decode(pdf_data)
|
||||
dir_path = os.path.join(django_settings.MEDIA_ROOT, 'temp_invoices')
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
# 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)
|
||||
|
||||
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)
|
||||
|
||||
with open(file_path, 'wb') as f:
|
||||
f.write(file_data)
|
||||
file_url = request.build_absolute_uri(django_settings.MEDIA_URL + 'temp_invoices/' + filename)
|
||||
|
||||
# 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)
|
||||
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)
|
||||
|
||||
file_data = base64.b64decode(pdf_data)
|
||||
dir_path = os.path.join(django_settings.MEDIA_ROOT, 'temp_quotations')
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
with open(file_path, 'wb') as f:
|
||||
f.write(file_data)
|
||||
file_url = request.build_absolute_uri(django_settings.MEDIA_URL + 'temp_quotations/' + filename)
|
||||
|
||||
# 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)
|
||||
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)
|
||||
|
||||
file_data = base64.b64decode(pdf_data)
|
||||
dir_path = os.path.join(django_settings.MEDIA_ROOT, 'temp_lpos')
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
with open(file_path, 'wb') as f:
|
||||
f.write(file_data)
|
||||
file_url = request.build_absolute_uri(django_settings.MEDIA_URL + 'temp_lpos/' + filename)
|
||||
|
||||
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}")
|
||||
|
||||
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 and lpo.status == 'draft':
|
||||
lpo.status = 'sent'
|
||||
lpo.save()
|
||||
|
||||
if success:
|
||||
return JsonResponse({'success': True, 'message': response_msg})
|
||||
@ -2043,3 +2045,72 @@ def recall_held_sale_api(request, pk):
|
||||
@login_required
|
||||
def delete_held_sale_api(request, pk):
|
||||
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')
|
||||
|
||||
BIN
media/temp_invoices/invoice_26_1770795695.pdf
Normal file
BIN
media/temp_invoices/invoice_26_1770795695.pdf
Normal file
Binary file not shown.
BIN
media/temp_invoices/invoice_26_1770797380.pdf
Normal file
BIN
media/temp_invoices/invoice_26_1770797380.pdf
Normal file
Binary file not shown.
BIN
media/temp_invoices/invoice_26_1770798382.pdf
Normal file
BIN
media/temp_invoices/invoice_26_1770798382.pdf
Normal file
Binary file not shown.
BIN
media/temp_invoices/invoice_26_1770798676.pdf
Normal file
BIN
media/temp_invoices/invoice_26_1770798676.pdf
Normal file
Binary file not shown.
BIN
media/temp_invoices/invoice_26_1770802715.pdf
Normal file
BIN
media/temp_invoices/invoice_26_1770802715.pdf
Normal file
Binary file not shown.
BIN
media/temp_invoices/invoice_26_1770802858.pdf
Normal file
BIN
media/temp_invoices/invoice_26_1770802858.pdf
Normal file
Binary file not shown.
BIN
media/temp_quotations/quotation_1_1770797431.pdf
Normal file
BIN
media/temp_quotations/quotation_1_1770797431.pdf
Normal file
Binary file not shown.
@ -4,3 +4,4 @@ python-dotenv==1.1.1
|
||||
gunicorn==21.2.0
|
||||
requests
|
||||
openpyxl
|
||||
WeasyPrint
|
||||
|
||||
1449
static/fonts/Cairo-Bold.ttf
Normal file
1449
static/fonts/Cairo-Bold.ttf
Normal file
File diff suppressed because one or more lines are too long
1449
static/fonts/Cairo-Regular.ttf
Normal file
1449
static/fonts/Cairo-Regular.ttf
Normal file
File diff suppressed because one or more lines are too long
1449
staticfiles/fonts/Cairo-Bold.ttf
Normal file
1449
staticfiles/fonts/Cairo-Bold.ttf
Normal file
File diff suppressed because one or more lines are too long
1449
staticfiles/fonts/Cairo-Regular.ttf
Normal file
1449
staticfiles/fonts/Cairo-Regular.ttf
Normal file
File diff suppressed because one or more lines are too long
2117
tmp/backup_app_38086_20260211_094244.sql
Normal file
2117
tmp/backup_app_38086_20260211_094244.sql
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user