Autosave: 20260203-095407

This commit is contained in:
Flatlogic Bot 2026-02-03 09:54:08 +00:00
parent f19ade40ee
commit 00d9114ba0
18 changed files with 400 additions and 72 deletions

View 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'),
),
]

View File

@ -365,6 +365,7 @@ class SystemSetting(models.Model):
wablas_enabled = models.BooleanField(_("Enable WhatsApp Gateway"), default=False) wablas_enabled = models.BooleanField(_("Enable WhatsApp Gateway"), default=False)
wablas_token = models.CharField(_("Wablas API Token"), max_length=255, blank=True) 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_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): def __str__(self):
return self.business_name return self.business_name

View File

@ -72,6 +72,9 @@
<option value="small">Small Sticker (38mm x 25mm)</option> <option value="small">Small Sticker (38mm x 25mm)</option>
<option value="a4-24">A4 Sheet (3x8 = 24 labels)</option> <option value="a4-24">A4 Sheet (3x8 = 24 labels)</option>
<option value="a4-40">A4 Sheet (4x10 = 40 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> <option value="price-tag">Jewelry / Price Tag (Small)</option>
</select> </select>
</div> </div>
@ -153,21 +156,72 @@
@media print { @media print {
.no-print { display: none !important; } .no-print { display: none !important; }
.print-only { display: block; } .print-only { display: block; }
body { margin: 0; padding: 0; background: white; } body { margin: 0; padding: 0; background: white; -webkit-print-color-adjust: exact; }
@page { margin: 0; }
@page {
size: A4;
margin: 0;
}
.label-sheet { .label-sheet {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: flex-start; justify-content: flex-start;
padding: 5mm; 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 { .label-standard {
width: 50mm; width: 50mm;
height: 25mm; height: 25mm;
border: 0.1mm solid #eee; /* Light border for cutting/reference */ border: 0.1mm solid #eee;
margin: 1mm; margin: 1mm;
padding: 2mm; padding: 2mm;
text-align: center; text-align: center;
@ -199,7 +253,6 @@
.label-a4-24 { .label-a4-24 {
width: 63.5mm; width: 63.5mm;
height: 33.9mm; height: 33.9mm;
margin: 0;
padding: 2mm; padding: 2mm;
text-align: center; text-align: center;
display: flex; display: flex;
@ -213,7 +266,6 @@
.label-a4-40 { .label-a4-40 {
width: 48.5mm; width: 48.5mm;
height: 25.4mm; height: 25.4mm;
margin: 0;
padding: 1mm; padding: 1mm;
text-align: center; text-align: center;
display: flex; display: flex;
@ -223,6 +275,54 @@
page-break-inside: avoid; 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 */ /* Price Tag */
.label-price-tag { .label-price-tag {
width: 30mm; width: 30mm;
@ -244,14 +344,15 @@
.label-text { .label-text {
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
font-size: 8pt; font-size: 7pt;
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
width: 100%; width: 100%;
line-height: 1.2;
} }
.label-price { .label-price {
font-size: 10pt; font-size: 9pt;
font-weight: bold; font-weight: bold;
} }
} }
@ -377,23 +478,34 @@
const sheet = document.createElement('div'); const sheet = document.createElement('div');
sheet.className = 'label-sheet'; 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 => { queue.forEach(p => {
for (let i = 0; i < p.qty; i++) { for (let i = 0; i < p.qty; i++) {
const label = document.createElement('div'); const label = document.createElement('div');
label.className = `label-item label-${labelType}`; label.className = `label-item label-${labelType}`;
let content = ''; let content = '';
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>`; if (showName) content += `<div class="label-text">${p.name}</div>`;
content += `<svg id="barcode-${p.id}-${i}"></svg>`;
const svgId = `barcode-${p.id}-${i}`;
content += `<svg id="${svgId}"></svg>`;
if (showSKU || showPrice) { if (showSKU || showPrice) {
content += `<div class="label-text d-flex justify-content-between">`; content += `<div class="label-text d-flex justify-content-between">`;
if (showSKU) content += `<span>${p.sku}</span>`; if (showSKU) content += `<span>${p.sku}</span>`;
if (showPrice) content += `<span class="label-price">OMR ${parseFloat(p.price).toFixed(3)}</span>`; if (showPrice) content += `<span class="label-price">OMR ${parseFloat(p.price).toFixed(3)}</span>`;
content += `</div>`; content += `</div>`;
} }
}
label.innerHTML = content; label.innerHTML = content;
sheet.appendChild(label); sheet.appendChild(label);
@ -402,14 +514,22 @@
printArea.appendChild(sheet); printArea.appendChild(sheet);
// Generate barcodes for each SVG // Generate barcodes
queue.forEach(p => { queue.forEach(p => {
for (let i = 0; i < p.qty; i++) { for (let i = 0; i < p.qty; i++) {
const svgId = `barcode-${p.id}-${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, { JsBarcode(`#${svgId}`, p.sku, {
format: "CODE128", format: "CODE128",
width: 1.5, width: bWidth,
height: 35, height: bHeight,
displayValue: false, displayValue: false,
margin: 0 margin: 0
}); });
@ -417,13 +537,11 @@
}); });
} }
// Listeners for settings change
document.getElementById('labelType').addEventListener('change', preparePrint); document.getElementById('labelType').addEventListener('change', preparePrint);
document.getElementById('showName').addEventListener('change', () => { updatePreview(); preparePrint(); }); document.getElementById('showName').addEventListener('change', () => { updatePreview(); preparePrint(); });
document.getElementById('showPrice').addEventListener('change', () => { updatePreview(); preparePrint(); }); document.getElementById('showPrice').addEventListener('change', () => { updatePreview(); preparePrint(); });
document.getElementById('showSKU').addEventListener('change', () => { updatePreview(); preparePrint(); }); document.getElementById('showSKU').addEventListener('change', () => { updatePreview(); preparePrint(); });
// Initial Preview
updatePreview(); updatePreview();
</script> </script>
{% endblock %} {% endblock %}

View File

@ -284,7 +284,7 @@
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
if (data.success) { if (data.success) {
window.location.href = "{% url 'invoices' %}"; window.location.href = "/invoices/" + data.sale_id + "/?created=true";
} else { } else {
alert("Error: " + data.error); alert("Error: " + data.error);
this.isProcessing = false; this.isProcessing = false;

View File

@ -1,16 +1,26 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n %} {% 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 %} {% block content %}
<div class="container py-4"> <div class="container py-4">
<!-- Action Bar --> <!-- 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"> <a href="{% url 'invoices' %}" class="btn btn-light rounded-3">
<i class="bi bi-arrow-left me-2"></i>{% trans "Back to Invoices" %} / العودة إلى الفواتير <i class="bi bi-arrow-left me-2"></i>{% trans "Back to Invoices" %} / العودة إلى الفواتير
</a> </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"> <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 <i class="bi bi-file-earmark-pdf me-2"></i>{% trans "Download PDF" %} / تحميل PDF
</button> </button>
@ -243,6 +253,72 @@ function downloadPDF() {
}; };
html2pdf().set(opt).from(element).save(); 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> </script>
<style> <style>

View File

@ -1,7 +1,7 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load i18n static l10n %} {% load i18n static l10n %}
{% block title %}{% trans "POS" %} | {{ site_settings.business_name }}{% endblock %} {% block title %}{% trans "POS" %} | {{ settings.business_name }}{% endblock %}
{% block head %} {% block head %}
<style> <style>
@ -474,7 +474,7 @@
<!-- Receipt Modal --> <!-- Receipt Modal -->
<div class="modal fade no-print" id="receiptModal" tabindex="-1"> <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-content border-0 shadow rounded-4">
<div class="modal-body text-center p-4"> <div class="modal-body text-center p-4">
<i class="bi bi-check-circle-fill text-success display-1 mb-3"></i> <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()"> <button type="button" class="btn btn-primary rounded-3" onclick="printInvoice()">
<i class="bi bi-printer me-2"></i> {% trans "Print Invoice" %} <i class="bi bi-printer me-2"></i> {% trans "Print Invoice" %}
</button> </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> <button type="button" class="btn btn-outline-secondary rounded-3" data-bs-dismiss="modal">{% trans "Close" %}</button>
</div> </div>
</div> </div>
@ -854,6 +859,12 @@
document.getElementById('inv-total').innerText = data.business.currency + ' ' + formatAmount(data.sale.total); 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() { function printInvoice() {
window.print(); window.print();
} }

View File

@ -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> <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>
<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"> <div class="col-md-12">
<label class="form-label fw-semibold">{% trans "Wablas API Token" %}</label> <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"> <input type="text" name="wablas_token" class="form-control" value="{{ settings.wablas_token }}" placeholder="e.g. your_api_token_here">
@ -422,17 +428,17 @@
</div> </div>
<div class="col-md-12"> <div class="col-md-12">
<label class="form-label fw-semibold">{% trans "Wablas Server URL" %}</label> <label class="form-label fw-semibold">{% trans "Wablas Secret Key" %}</label>
<input type="url" name="wablas_server_url" class="form-control" value="{{ settings.wablas_server_url }}" placeholder="https://console.wablas.com"> <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 "Ensure it starts with https://. Example: https://console.wablas.com or your custom domain." %}</div> <div class="form-text">{% trans "Required for some Wablas API versions." %}</div>
</div>
</div> </div>
<div class="mt-4 pt-3 border-top d-flex justify-content-end"> <div class="col-12 mt-4">
<button type="submit" class="btn btn-success px-4 py-2"> <button type="submit" class="btn btn-success px-4 py-2">
<i class="bi bi-whatsapp me-2"></i> {% trans "Save WhatsApp Settings" %} <i class="bi bi-whatsapp me-2"></i> {% trans "Save WhatsApp Settings" %}
</button> </button>
</div> </div>
</div>
</form> </form>
</div> </div>
</div> </div>

View File

@ -119,5 +119,6 @@ urlpatterns = [
path('settings/loyalty/delete/<int:pk>/', views.delete_loyalty_tier, name='delete_loyalty_tier'), 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'), path('api/customer-loyalty/<int:pk>/', views.get_customer_loyalty_api, name='get_customer_loyalty_api'),
# WhatsApp # 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'), path('api/test-whatsapp/', views.test_whatsapp_connection, name='test_whatsapp_connection'),
] ]

View File

@ -86,7 +86,8 @@ def send_whatsapp_message(phone, message):
url = f"{server_url}/api/send-message" url = f"{server_url}/api/send-message"
headers = { headers = {
"Authorization": settings.wablas_token "Authorization": settings.wablas_token,
"Secret": settings.wablas_secret_key
} }
payload = { payload = {
@ -103,3 +104,45 @@ def send_whatsapp_message(phone, message):
return False, data.get('message', 'Unknown error from Wablas.') return False, data.get('message', 'Unknown error from Wablas.')
except Exception as e: 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)

View File

@ -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 as _
from django.utils.translation import gettext_lazy 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 from django.core.paginator import Paginator
import decimal import decimal
from django.contrib.auth.models import User, Group, Permission 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': str(e)}, status=400)
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405) 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 @login_required
def add_sale_payment(request, pk): def add_sale_payment(request, pk):
sale = get_object_or_404(Sale, pk=pk) sale = get_object_or_404(Sale, pk=pk)
@ -897,44 +944,51 @@ def settings_view(request):
if not settings: if not settings:
settings = SystemSetting.objects.create() settings = SystemSetting.objects.create()
if request.method == 'POST': if request.method == "POST":
settings.business_name = request.POST.get('business_name') if "business_name" in request.POST:
settings.address = request.POST.get('address') settings.business_name = request.POST.get("business_name") or "Meezan Accounting"
settings.phone = request.POST.get('phone') settings.address = request.POST.get("address", "")
settings.email = request.POST.get('email') settings.phone = request.POST.get("phone", "")
settings.currency_symbol = request.POST.get('currency_symbol') settings.email = request.POST.get("email", "")
settings.tax_rate = request.POST.get('tax_rate') settings.currency_symbol = request.POST.get("currency_symbol", "OMR")
settings.decimal_places = request.POST.get('decimal_places', 3) settings.tax_rate = request.POST.get("tax_rate", 0)
settings.vat_number = request.POST.get('vat_number') settings.decimal_places = request.POST.get("decimal_places", 3)
settings.registration_number = request.POST.get('registration_number') settings.vat_number = request.POST.get("vat_number", "")
settings.registration_number = request.POST.get("registration_number", "")
# Loyalty Settings settings.loyalty_enabled = request.POST.get("loyalty_enabled") == "on"
settings.loyalty_enabled = request.POST.get('loyalty_enabled') == 'on' settings.points_per_currency = request.POST.get("points_per_currency", 1.0)
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.currency_per_point = request.POST.get('currency_per_point', 0.010) settings.min_points_to_redeem = request.POST.get("min_points_to_redeem", 100)
settings.min_points_to_redeem = request.POST.get('min_points_to_redeem', 100)
# WhatsApp Settings if "logo" in request.FILES:
if "wablas_token" in request.POST: settings.logo = request.FILES["logo"]
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_enabled = request.POST.get("wablas_enabled") == "on"
settings.wablas_token = request.POST.get("wablas_token", "") settings.wablas_token = request.POST.get("wablas_token", "")
settings.wablas_server_url = request.POST.get("wablas_server_url", "") 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() settings.save()
messages.success(request, _("Settings updated successfully!")) messages.success(request, _("Settings updated successfully!"))
return redirect(reverse('settings') + '#profile')
payment_methods = PaymentMethod.objects.all() if "business_name" in request.POST:
loyalty_tiers = LoyaltyTier.objects.all().order_by('min_points') 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"))
return render(request, 'core/settings.html', { payment_methods = PaymentMethod.objects.all().order_by("name_en")
'settings': settings, loyalty_tiers = LoyaltyTier.objects.all().order_by("min_points")
'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 @login_required
def add_payment_method(request): def add_payment_method(request):

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB