diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 5a0a5f0..27ce5b1 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 1e7cf90..439bd2a 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/utils.cpython-311.pyc b/core/__pycache__/utils.cpython-311.pyc index fcfe627..a6171c4 100644 Binary files a/core/__pycache__/utils.cpython-311.pyc and b/core/__pycache__/utils.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index b5bcced..29bb55f 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/migrations/0019_systemsetting_wablas_secret_key.py b/core/migrations/0019_systemsetting_wablas_secret_key.py new file mode 100644 index 0000000..eb8ba4e --- /dev/null +++ b/core/migrations/0019_systemsetting_wablas_secret_key.py @@ -0,0 +1,18 @@ +# Generated by Django 5.2.7 on 2026-02-03 05:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0018_systemsetting_wablas_enabled_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='systemsetting', + name='wablas_secret_key', + field=models.CharField(blank=True, max_length=255, verbose_name='Wablas Secret Key'), + ), + ] diff --git a/core/migrations/__pycache__/0019_systemsetting_wablas_secret_key.cpython-311.pyc b/core/migrations/__pycache__/0019_systemsetting_wablas_secret_key.cpython-311.pyc new file mode 100644 index 0000000..5071d30 Binary files /dev/null and b/core/migrations/__pycache__/0019_systemsetting_wablas_secret_key.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 016d2a5..a0d518a 100644 --- a/core/models.py +++ b/core/models.py @@ -365,6 +365,7 @@ class SystemSetting(models.Model): wablas_enabled = models.BooleanField(_("Enable WhatsApp Gateway"), default=False) wablas_token = models.CharField(_("Wablas API Token"), max_length=255, blank=True) wablas_server_url = models.URLField(_("Wablas Server URL"), blank=True, help_text="Example: https://console.wablas.com") + wablas_secret_key = models.CharField(_("Wablas Secret Key"), max_length=255, blank=True) def __str__(self): return self.business_name diff --git a/core/templates/core/barcode_labels.html b/core/templates/core/barcode_labels.html index 72c7dcc..83b81b7 100644 --- a/core/templates/core/barcode_labels.html +++ b/core/templates/core/barcode_labels.html @@ -72,6 +72,9 @@ + + + @@ -153,21 +156,72 @@ @media print { .no-print { display: none !important; } .print-only { display: block; } - body { margin: 0; padding: 0; background: white; } - @page { margin: 0; } + body { margin: 0; padding: 0; background: white; -webkit-print-color-adjust: exact; } + + @page { + size: A4; + margin: 0; + } .label-sheet { display: flex; flex-wrap: wrap; justify-content: flex-start; padding: 5mm; + width: 210mm; + margin: 0 auto; + background: white; } - /* Standard Sticker (50x25) */ + /* Specific A4 Grid Layouts to ensure alignment */ + .sheet-a4-24 { + display: grid !important; + grid-template-columns: repeat(3, 63.5mm); + grid-auto-rows: 33.9mm; + padding: 12.9mm 9.75mm !important; + gap: 0; + } + + .sheet-a4-40 { + display: grid !important; + grid-template-columns: repeat(4, 48.5mm); + grid-auto-rows: 25.4mm; + padding: 21.5mm 8mm !important; + gap: 0; + } + + /* Avery L7651 (5x13) */ + .sheet-l7651 { + display: grid !important; + grid-template-columns: repeat(5, 38.1mm); + grid-auto-rows: 21.2mm; + padding: 10.7mm 9.75mm !important; + gap: 0; + } + + /* Avery L7656 (4x21) */ + .sheet-l7656 { + display: grid !important; + grid-template-columns: repeat(4, 46mm); + grid-auto-rows: 11.1mm; + padding: 31.95mm 13mm !important; + gap: 0; + } + + /* Avery L7156 (3x7) */ + .sheet-l7156 { + display: grid !important; + grid-template-columns: repeat(3, 63.5mm); + grid-auto-rows: 38.1mm; + padding: 15.15mm 9.75mm !important; + gap: 0; + } + + /* Standard Sticker (50x25) - usually for thermal printers */ .label-standard { width: 50mm; height: 25mm; - border: 0.1mm solid #eee; /* Light border for cutting/reference */ + border: 0.1mm solid #eee; margin: 1mm; padding: 2mm; text-align: center; @@ -199,7 +253,6 @@ .label-a4-24 { width: 63.5mm; height: 33.9mm; - margin: 0; padding: 2mm; text-align: center; display: flex; @@ -213,7 +266,6 @@ .label-a4-40 { width: 48.5mm; height: 25.4mm; - margin: 0; padding: 1mm; text-align: center; display: flex; @@ -223,6 +275,54 @@ page-break-inside: avoid; } + /* Avery L7651 (5x13) */ + .label-l7651 { + width: 38.1mm; + height: 21.2mm; + padding: 1mm; + text-align: center; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + page-break-inside: avoid; + } + + /* Avery L7656 (4x21) */ + .label-l7656 { + width: 46mm; + height: 11.1mm; + padding: 0.5mm; + text-align: center; + display: flex; + flex-direction: row; + justify-content: space-around; + align-items: center; + page-break-inside: avoid; + } + .label-l7656 svg { + height: 8mm !important; + width: auto; + } + .label-l7656 .label-text { + font-size: 5pt !important; + width: auto !important; + margin: 0 2px; + } + + /* Avery L7156 (3x7) */ + .label-l7156 { + width: 63.5mm; + height: 38.1mm; + padding: 2mm; + text-align: center; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + page-break-inside: avoid; + } + /* Price Tag */ .label-price-tag { width: 30mm; @@ -244,14 +344,15 @@ .label-text { font-family: Arial, sans-serif; - font-size: 8pt; + font-size: 7pt; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; width: 100%; + line-height: 1.2; } .label-price { - font-size: 10pt; + font-size: 9pt; font-weight: bold; } } @@ -376,6 +477,13 @@ const sheet = document.createElement('div'); sheet.className = 'label-sheet'; + + // Add specific sheet class for grid layouts + if (labelType === 'a4-24') sheet.classList.add('sheet-a4-24'); + else if (labelType === 'a4-40') sheet.classList.add('sheet-a4-40'); + else if (labelType === 'l7651') sheet.classList.add('sheet-l7651'); + else if (labelType === 'l7656') sheet.classList.add('sheet-l7656'); + else if (labelType === 'l7156') sheet.classList.add('sheet-l7156'); queue.forEach(p => { for (let i = 0; i < p.qty; i++) { @@ -383,16 +491,20 @@ label.className = `label-item label-${labelType}`; let content = ''; - if (showName) content += `
${p.name}
`; - - const svgId = `barcode-${p.id}-${i}`; - content += ``; - - if (showSKU || showPrice) { - content += `
`; - if (showSKU) content += `${p.sku}`; - if (showPrice) content += `OMR ${parseFloat(p.price).toFixed(3)}`; - content += `
`; + if (labelType === 'l7656') { + // Horizontal layout for very short labels + if (showName) content += `
${p.name}
`; + content += ``; + if (showPrice) content += `
${parseFloat(p.price).toFixed(3)}
`; + } else { + if (showName) content += `
${p.name}
`; + content += ``; + if (showSKU || showPrice) { + content += `
`; + if (showSKU) content += `${p.sku}`; + if (showPrice) content += `OMR ${parseFloat(p.price).toFixed(3)}`; + content += `
`; + } } label.innerHTML = content; @@ -402,14 +514,22 @@ printArea.appendChild(sheet); - // Generate barcodes for each SVG + // Generate barcodes queue.forEach(p => { for (let i = 0; i < p.qty; i++) { const svgId = `barcode-${p.id}-${i}`; + let bWidth = 1.5; + let bHeight = 30; + + if (labelType === 'l7651') { bWidth = 1.0; bHeight = 20; } + else if (labelType === 'l7656') { bWidth = 0.8; bHeight = 8; } + else if (labelType === 'a4-40') { bWidth = 1.2; bHeight = 25; } + else if (labelType === 'price-tag') { bWidth = 1.0; bHeight = 15; } + JsBarcode(`#${svgId}`, p.sku, { format: "CODE128", - width: 1.5, - height: 35, + width: bWidth, + height: bHeight, displayValue: false, margin: 0 }); @@ -417,13 +537,11 @@ }); } - // Listeners for settings change document.getElementById('labelType').addEventListener('change', preparePrint); document.getElementById('showName').addEventListener('change', () => { updatePreview(); preparePrint(); }); document.getElementById('showPrice').addEventListener('change', () => { updatePreview(); preparePrint(); }); document.getElementById('showSKU').addEventListener('change', () => { updatePreview(); preparePrint(); }); - // Initial Preview updatePreview(); -{% endblock %} +{% endblock %} \ No newline at end of file diff --git a/core/templates/core/invoice_create.html b/core/templates/core/invoice_create.html index 56cb8f2..134a336 100644 --- a/core/templates/core/invoice_create.html +++ b/core/templates/core/invoice_create.html @@ -284,7 +284,7 @@ .then(res => res.json()) .then(data => { if (data.success) { - window.location.href = "{% url 'invoices' %}"; + window.location.href = "/invoices/" + data.sale_id + "/?created=true"; } else { alert("Error: " + data.error); this.isProcessing = false; diff --git a/core/templates/core/invoice_detail.html b/core/templates/core/invoice_detail.html index ad255a6..4f96ac3 100644 --- a/core/templates/core/invoice_detail.html +++ b/core/templates/core/invoice_detail.html @@ -1,16 +1,26 @@ {% extends 'base.html' %} {% load i18n %} -{% block title %}{% trans "Sales Invoice" %} #{{ sale.invoice_number|default:sale.id }} | {{ site_settings.business_name }}{% endblock %} +{% block title %}{% trans "Sales Invoice" %} #{{ sale.invoice_number|default:sale.id }} | {{ settings.business_name }}{% endblock %} {% block content %}
-
+
{% trans "Back to Invoices" %} / العودة إلى الفواتير -
+
+ {% if settings.wablas_enabled or site_settings.wablas_enabled %} +
+ + + +
+ {% endif %} @@ -243,6 +253,72 @@ function downloadPDF() { }; html2pdf().set(opt).from(element).save(); } + +async function sendWhatsAppDirect() { + const phone = document.getElementById('whatsappPhoneDirect').value; + const spinner = document.getElementById('whatsappSpinnerDirect'); + const btn = document.getElementById('btnSendWhatsAppDirect'); + + if (!phone) { + alert("{% trans 'Please enter a WhatsApp number.' %}"); + return; + } + + spinner.classList.remove('d-none'); + btn.disabled = true; + + try { + const element = document.getElementById('invoice-card'); + const opt = { + margin: 0, + filename: 'Invoice_{{ sale.invoice_number|default:sale.id }}.pdf', + image: { type: 'jpeg', quality: 0.98 }, + html2canvas: { scale: 2, useCORS: true, letterRendering: true }, + 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: { + 'Content-Type': 'application/json', + 'X-CSRFToken': '{{ csrf_token }}' + }, + body: JSON.stringify({ + sale_id: {{ sale.id }}, + phone: phone, + pdf_data: pdfBlob + }) + }); + + const data = await response.json(); + + if (data.success) { + alert(data.message || "{% trans 'Invoice sent successfully!' %}"); + } else { + alert(data.error || data.message || "{% trans 'Failed to send invoice.' %}"); + } + } catch (error) { + console.error(error); + alert("{% trans 'An error occurred while sending the invoice.' %}"); + } finally { + spinner.classList.add('d-none'); + btn.disabled = false; + } +} + +// Auto-trigger if 'created=true' is in URL +document.addEventListener("DOMContentLoaded", function() { + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.get("created") === "true") { + const phone = document.getElementById('whatsappPhoneDirect').value; + if (phone) { + sendWhatsAppDirect(); + } + } +});