Autosave: 20260203-095407
This commit is contained in:
parent
f19ade40ee
commit
00d9114ba0
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
18
core/migrations/0019_systemsetting_wablas_secret_key.py
Normal file
18
core/migrations/0019_systemsetting_wablas_secret_key.py
Normal file
@ -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'),
|
||||
),
|
||||
]
|
||||
Binary file not shown.
@ -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
|
||||
|
||||
@ -72,6 +72,9 @@
|
||||
<option value="small">Small Sticker (38mm x 25mm)</option>
|
||||
<option value="a4-24">A4 Sheet (3x8 = 24 labels)</option>
|
||||
<option value="a4-40">A4 Sheet (4x10 = 40 labels)</option>
|
||||
<option value="l7651">A4 Avery L7651 (5x13 = 65 labels)</option>
|
||||
<option value="l7656">A4 Avery L7656 (4x21 = 84 labels)</option>
|
||||
<option value="l7156">A4 Avery L7156 (3x7 = 21 labels)</option>
|
||||
<option value="price-tag">Jewelry / Price Tag (Small)</option>
|
||||
</select>
|
||||
</div>
|
||||
@ -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 += `<div class="label-text">${p.name}</div>`;
|
||||
|
||||
const svgId = `barcode-${p.id}-${i}`;
|
||||
content += `<svg id="${svgId}"></svg>`;
|
||||
|
||||
if (showSKU || showPrice) {
|
||||
content += `<div class="label-text d-flex justify-content-between">`;
|
||||
if (showSKU) content += `<span>${p.sku}</span>`;
|
||||
if (showPrice) content += `<span class="label-price">OMR ${parseFloat(p.price).toFixed(3)}</span>`;
|
||||
content += `</div>`;
|
||||
if (labelType === 'l7656') {
|
||||
// Horizontal layout for very short labels
|
||||
if (showName) content += `<div class="label-text" style="max-width: 40px">${p.name}</div>`;
|
||||
content += `<svg id="barcode-${p.id}-${i}"></svg>`;
|
||||
if (showPrice) content += `<div class="label-text label-price">${parseFloat(p.price).toFixed(3)}</div>`;
|
||||
} else {
|
||||
if (showName) content += `<div class="label-text">${p.name}</div>`;
|
||||
content += `<svg id="barcode-${p.id}-${i}"></svg>`;
|
||||
if (showSKU || showPrice) {
|
||||
content += `<div class="label-text d-flex justify-content-between">`;
|
||||
if (showSKU) content += `<span>${p.sku}</span>`;
|
||||
if (showPrice) content += `<span class="label-price">OMR ${parseFloat(p.price).toFixed(3)}</span>`;
|
||||
content += `</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
</script>
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
@ -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;
|
||||
|
||||
@ -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 %}
|
||||
<div class="container py-4">
|
||||
<!-- Action Bar -->
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 d-print-none">
|
||||
<div class="d-flex justify-content-between align-items-center mb-4 d-print-none flex-wrap gap-3">
|
||||
<a href="{% url 'invoices' %}" class="btn btn-light rounded-3">
|
||||
<i class="bi bi-arrow-left me-2"></i>{% trans "Back to Invoices" %} / العودة إلى الفواتير
|
||||
</a>
|
||||
<div class="d-flex gap-2">
|
||||
<div class="d-flex gap-2 align-items-center">
|
||||
{% if settings.wablas_enabled or site_settings.wablas_enabled %}
|
||||
<div class="d-flex align-items-center bg-white border rounded-3 p-1 shadow-sm">
|
||||
<i class="bi bi-whatsapp text-success mx-2"></i>
|
||||
<input type="text" id="whatsappPhoneDirect" class="form-control form-control-sm border-0" style="width: 140px;" value="{{ sale.customer.phone|default:'' }}" placeholder="{% trans "Phone Number" %}">
|
||||
<button id="btnSendWhatsAppDirect" onclick="sendWhatsAppDirect()" class="btn btn-success btn-sm rounded-2 px-3 ms-1">
|
||||
<span id="whatsappSpinnerDirect" class="spinner-border spinner-border-sm d-none"></span>
|
||||
{% trans "Send" %} / إرسال
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<button onclick="downloadPDF()" class="btn btn-outline-primary rounded-3 px-4">
|
||||
<i class="bi bi-file-earmark-pdf me-2"></i>{% trans "Download PDF" %} / تحميل PDF
|
||||
</button>
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load i18n static l10n %}
|
||||
|
||||
{% block title %}{% trans "POS" %} | {{ site_settings.business_name }}{% endblock %}
|
||||
{% block title %}{% trans "POS" %} | {{ settings.business_name }}{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<style>
|
||||
@ -474,7 +474,7 @@
|
||||
|
||||
<!-- Receipt Modal -->
|
||||
<div class="modal fade no-print" id="receiptModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-dialog modal-sm modal-dialog-centered">
|
||||
<div class="modal-content border-0 shadow rounded-4">
|
||||
<div class="modal-body text-center p-4">
|
||||
<i class="bi bi-check-circle-fill text-success display-1 mb-3"></i>
|
||||
@ -484,6 +484,11 @@
|
||||
<button type="button" class="btn btn-primary rounded-3" onclick="printInvoice()">
|
||||
<i class="bi bi-printer me-2"></i> {% trans "Print Invoice" %}
|
||||
</button>
|
||||
{% if settings.wablas_enabled or site_settings.wablas_enabled %}
|
||||
<button type="button" class="btn btn-outline-success rounded-3" onclick="goToWhatsApp()">
|
||||
<i class="bi bi-whatsapp me-2"></i> {% trans "Send via WhatsApp" %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<button type="button" class="btn btn-outline-secondary rounded-3" data-bs-dismiss="modal">{% trans "Close" %}</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -854,6 +859,12 @@
|
||||
document.getElementById('inv-total').innerText = data.business.currency + ' ' + formatAmount(data.sale.total);
|
||||
}
|
||||
|
||||
function goToWhatsApp() {
|
||||
if (lastSaleData && lastSaleData.sale_id) {
|
||||
window.location.href = "/invoices/" + lastSaleData.sale_id + "/?created=true";
|
||||
}
|
||||
}
|
||||
|
||||
function printInvoice() {
|
||||
window.print();
|
||||
}
|
||||
|
||||
@ -415,6 +415,12 @@
|
||||
<p class="text-muted small">{% trans "When enabled, you can send automated messages, invoices, and alerts via WhatsApp using the Wablas gateway." %}</p>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-semibold">{% trans "Wablas Server URL" %}</label>
|
||||
<input type="text" name="wablas_server_url" class="form-control" value="{{ settings.wablas_server_url }}" placeholder="e.g. https://console.wablas.com">
|
||||
<div class="form-text">{% trans "The server URL provided by Wablas." %}</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-semibold">{% trans "Wablas API Token" %}</label>
|
||||
<input type="text" name="wablas_token" class="form-control" value="{{ settings.wablas_token }}" placeholder="e.g. your_api_token_here">
|
||||
@ -422,16 +428,16 @@
|
||||
</div>
|
||||
|
||||
<div class="col-md-12">
|
||||
<label class="form-label fw-semibold">{% trans "Wablas Server URL" %}</label>
|
||||
<input type="url" name="wablas_server_url" class="form-control" value="{{ settings.wablas_server_url }}" placeholder="https://console.wablas.com">
|
||||
<div class="form-text">{% trans "Ensure it starts with https://. Example: https://console.wablas.com or your custom domain." %}</div>
|
||||
<label class="form-label fw-semibold">{% trans "Wablas Secret Key" %}</label>
|
||||
<input type="text" name="wablas_secret_key" class="form-control" value="{{ settings.wablas_secret_key }}" placeholder="e.g. your_secret_key_here">
|
||||
<div class="form-text">{% trans "Required for some Wablas API versions." %}</div>
|
||||
</div>
|
||||
|
||||
<div class="col-12 mt-4">
|
||||
<button type="submit" class="btn btn-success px-4 py-2">
|
||||
<i class="bi bi-whatsapp me-2"></i> {% trans "Save WhatsApp Settings" %}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 pt-3 border-top d-flex justify-content-end">
|
||||
<button type="submit" class="btn btn-success px-4 py-2">
|
||||
<i class="bi bi-whatsapp me-2"></i> {% trans "Save WhatsApp Settings" %}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -119,5 +119,6 @@ urlpatterns = [
|
||||
path('settings/loyalty/delete/<int:pk>/', views.delete_loyalty_tier, name='delete_loyalty_tier'),
|
||||
path('api/customer-loyalty/<int:pk>/', views.get_customer_loyalty_api, name='get_customer_loyalty_api'),
|
||||
# WhatsApp
|
||||
path('api/send-invoice-whatsapp/', views.send_invoice_whatsapp, name='send_invoice_whatsapp'),
|
||||
path('api/test-whatsapp/', views.test_whatsapp_connection, name='test_whatsapp_connection'),
|
||||
]
|
||||
|
||||
@ -86,7 +86,8 @@ def send_whatsapp_message(phone, message):
|
||||
url = f"{server_url}/api/send-message"
|
||||
|
||||
headers = {
|
||||
"Authorization": settings.wablas_token
|
||||
"Authorization": settings.wablas_token,
|
||||
"Secret": settings.wablas_secret_key
|
||||
}
|
||||
|
||||
payload = {
|
||||
@ -102,4 +103,46 @@ def send_whatsapp_message(phone, message):
|
||||
else:
|
||||
return False, data.get('message', 'Unknown error from Wablas.')
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
return False, str(e)
|
||||
|
||||
def send_whatsapp_document(phone, document_url, caption=""):
|
||||
"""
|
||||
Sends a document via WhatsApp using Wablas gateway.
|
||||
document_url should be a public URL to the file.
|
||||
"""
|
||||
from .models import SystemSetting
|
||||
settings = SystemSetting.objects.first()
|
||||
|
||||
if not settings or not settings.wablas_enabled:
|
||||
return False, "WhatsApp gateway is disabled."
|
||||
|
||||
if not settings.wablas_token or not settings.wablas_server_url:
|
||||
return False, "Wablas configuration is incomplete."
|
||||
|
||||
# Clean phone number (remove non-digits)
|
||||
phone = ''.join(filter(str.isdigit, str(phone)))
|
||||
|
||||
# Ensure URL is properly formatted
|
||||
server_url = settings.wablas_server_url.rstrip('/')
|
||||
url = f"{server_url}/api/send-document"
|
||||
|
||||
headers = {
|
||||
"Authorization": settings.wablas_token,
|
||||
"Secret": settings.wablas_secret_key
|
||||
}
|
||||
|
||||
payload = {
|
||||
"phone": phone,
|
||||
"document": document_url,
|
||||
"caption": caption
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(url, data=payload, headers=headers, timeout=15)
|
||||
data = response.json()
|
||||
if response.status_code == 200 and data.get('status') == True:
|
||||
return True, "Document sent successfully."
|
||||
else:
|
||||
return False, data.get('message', 'Unknown error from Wablas.')
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
116
core/views.py
116
core/views.py
@ -1,6 +1,9 @@
|
||||
import base64
|
||||
import os
|
||||
from django.conf import settings as django_settings
|
||||
from django.utils.translation import gettext as _
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from .utils import number_to_words_en
|
||||
from .utils import number_to_words_en, send_whatsapp_document
|
||||
from django.core.paginator import Paginator
|
||||
import decimal
|
||||
from django.contrib.auth.models import User, Group, Permission
|
||||
@ -530,6 +533,50 @@ def create_sale_api(request):
|
||||
return JsonResponse({'success': False, 'error': str(e)}, status=400)
|
||||
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
|
||||
|
||||
@csrf_exempt
|
||||
@login_required
|
||||
def send_invoice_whatsapp(request):
|
||||
if request.method == 'POST':
|
||||
try:
|
||||
data = json.loads(request.body)
|
||||
sale_id = data.get('sale_id')
|
||||
phone = data.get('phone')
|
||||
pdf_base64 = data.get('pdf_data')
|
||||
|
||||
if not phone or not pdf_base64:
|
||||
return JsonResponse({'success': False, 'error': 'Missing phone or PDF data.'}, status=400)
|
||||
|
||||
if ',' in pdf_base64:
|
||||
pdf_base64 = pdf_base64.split(',')[1]
|
||||
|
||||
pdf_content = base64.b64decode(pdf_base64)
|
||||
|
||||
temp_dir = os.path.join(django_settings.MEDIA_ROOT, 'temp_invoices')
|
||||
if not os.path.exists(temp_dir):
|
||||
os.makedirs(temp_dir)
|
||||
|
||||
filename = f'Invoice_{sale_id}.pdf'
|
||||
file_path_pdf = os.path.join(temp_dir, filename)
|
||||
|
||||
with open(file_path_pdf, 'wb') as f_pdf:
|
||||
f_pdf.write(pdf_content)
|
||||
|
||||
base_url = request.build_absolute_uri('/')
|
||||
document_url = f"{base_url.rstrip('/')}{django_settings.MEDIA_URL}temp_invoices/{filename}"
|
||||
|
||||
sale = Sale.objects.filter(id=sale_id).first()
|
||||
invoice_num = sale.invoice_number if sale and sale.invoice_number else sale_id
|
||||
caption = f'Invoice #{invoice_num}'
|
||||
|
||||
success, message = send_whatsapp_document(phone, document_url, caption)
|
||||
|
||||
return JsonResponse({'success': success, 'message': message})
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({'success': False, 'error': str(e)}, status=500)
|
||||
|
||||
return JsonResponse({'success': False, 'error': 'Invalid request method.'}, status=405)
|
||||
|
||||
@login_required
|
||||
def add_sale_payment(request, pk):
|
||||
sale = get_object_or_404(Sale, pk=pk)
|
||||
@ -897,44 +944,51 @@ def settings_view(request):
|
||||
if not settings:
|
||||
settings = SystemSetting.objects.create()
|
||||
|
||||
if request.method == 'POST':
|
||||
settings.business_name = request.POST.get('business_name')
|
||||
settings.address = request.POST.get('address')
|
||||
settings.phone = request.POST.get('phone')
|
||||
settings.email = request.POST.get('email')
|
||||
settings.currency_symbol = request.POST.get('currency_symbol')
|
||||
settings.tax_rate = request.POST.get('tax_rate')
|
||||
settings.decimal_places = request.POST.get('decimal_places', 3)
|
||||
settings.vat_number = request.POST.get('vat_number')
|
||||
settings.registration_number = request.POST.get('registration_number')
|
||||
if request.method == "POST":
|
||||
if "business_name" in request.POST:
|
||||
settings.business_name = request.POST.get("business_name") or "Meezan Accounting"
|
||||
settings.address = request.POST.get("address", "")
|
||||
settings.phone = request.POST.get("phone", "")
|
||||
settings.email = request.POST.get("email", "")
|
||||
settings.currency_symbol = request.POST.get("currency_symbol", "OMR")
|
||||
settings.tax_rate = request.POST.get("tax_rate", 0)
|
||||
settings.decimal_places = request.POST.get("decimal_places", 3)
|
||||
settings.vat_number = request.POST.get("vat_number", "")
|
||||
settings.registration_number = request.POST.get("registration_number", "")
|
||||
|
||||
settings.loyalty_enabled = request.POST.get("loyalty_enabled") == "on"
|
||||
settings.points_per_currency = request.POST.get("points_per_currency", 1.0)
|
||||
settings.currency_per_point = request.POST.get("currency_per_point", 0.010)
|
||||
settings.min_points_to_redeem = request.POST.get("min_points_to_redeem", 100)
|
||||
|
||||
if "logo" in request.FILES:
|
||||
settings.logo = request.FILES["logo"]
|
||||
|
||||
# Loyalty Settings
|
||||
settings.loyalty_enabled = request.POST.get('loyalty_enabled') == 'on'
|
||||
settings.points_per_currency = request.POST.get('points_per_currency', 1.0)
|
||||
settings.currency_per_point = request.POST.get('currency_per_point', 0.010)
|
||||
settings.min_points_to_redeem = request.POST.get('min_points_to_redeem', 100)
|
||||
|
||||
# WhatsApp Settings
|
||||
if "wablas_token" in request.POST:
|
||||
elif "wablas_token" in request.POST or "wablas_enabled" in request.POST or "wablas_server_url" in request.POST:
|
||||
settings.wablas_enabled = request.POST.get("wablas_enabled") == "on"
|
||||
settings.wablas_token = request.POST.get("wablas_token", "")
|
||||
settings.wablas_server_url = request.POST.get("wablas_server_url", "")
|
||||
settings.wablas_secret_key = request.POST.get("wablas_secret_key", "")
|
||||
|
||||
if 'logo' in request.FILES:
|
||||
settings.logo = request.FILES['logo']
|
||||
|
||||
settings.save()
|
||||
messages.success(request, _("Settings updated successfully!"))
|
||||
return redirect(reverse('settings') + '#profile')
|
||||
|
||||
if "business_name" in request.POST:
|
||||
return redirect(reverse("settings") + "#profile")
|
||||
elif "wablas_token" in request.POST or "wablas_enabled" in request.POST or "wablas_server_url" in request.POST:
|
||||
return redirect(reverse("settings") + "#whatsapp")
|
||||
else:
|
||||
return redirect(reverse("settings"))
|
||||
|
||||
payment_methods = PaymentMethod.objects.all().order_by("name_en")
|
||||
loyalty_tiers = LoyaltyTier.objects.all().order_by("min_points")
|
||||
|
||||
payment_methods = PaymentMethod.objects.all()
|
||||
loyalty_tiers = LoyaltyTier.objects.all().order_by('min_points')
|
||||
|
||||
return render(request, 'core/settings.html', {
|
||||
'settings': settings,
|
||||
'payment_methods': payment_methods,
|
||||
'loyalty_tiers': loyalty_tiers
|
||||
})
|
||||
context = {
|
||||
"settings": settings,
|
||||
"payment_methods": payment_methods,
|
||||
"loyalty_tiers": loyalty_tiers
|
||||
}
|
||||
return render(request, "core/settings.html", context)
|
||||
|
||||
@login_required
|
||||
def add_payment_method(request):
|
||||
|
||||
BIN
media/temp_invoices/Invoice_18.pdf
Normal file
BIN
media/temp_invoices/Invoice_18.pdf
Normal file
Binary file not shown.
BIN
staticfiles/pasted-20260203-040015-70bc78c5.jpg
Normal file
BIN
staticfiles/pasted-20260203-040015-70bc78c5.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
BIN
staticfiles/vm-shot-2026-02-03T03-59-49-172Z.jpg
Normal file
BIN
staticfiles/vm-shot-2026-02-03T03-59-49-172Z.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
Loading…
x
Reference in New Issue
Block a user