Autosave: 20260211-044531
This commit is contained in:
parent
a9b274a48f
commit
48923270af
Binary file not shown.
@ -219,11 +219,8 @@
|
|||||||
page-break-after: always;
|
page-break-after: always;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
|
|
||||||
/* Use Flexbox for Grid Layout - More reliable for print */
|
/* Revert to Block + Float for reliability in all print engines */
|
||||||
display: flex !important;
|
display: block !important;
|
||||||
flex-wrap: wrap !important;
|
|
||||||
justify-content: flex-start !important;
|
|
||||||
align-items: flex-start !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Direct print container override */
|
/* Direct print container override */
|
||||||
@ -238,13 +235,24 @@
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
display: flex;
|
/* Float strategy for grid */
|
||||||
|
float: left !important;
|
||||||
|
/* Keep flex for INTERNAL content alignment only */
|
||||||
|
display: flex !important;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
/* REMOVED width: 100% to prevent full-width expansion */
|
|
||||||
page-break-inside: avoid;
|
page-break-inside: avoid;
|
||||||
float: left; /* Fallback */
|
page-break-after: avoid;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Clearfix for sheet if needed, though usually fixed height handles it */
|
||||||
|
.label-sheet::after {
|
||||||
|
content: "";
|
||||||
|
display: table;
|
||||||
|
clear: both;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- SHEET CONFIGURATIONS (Flexbox + Explicit Dimensions) --- */
|
/* --- SHEET CONFIGURATIONS (Flexbox + Explicit Dimensions) --- */
|
||||||
|
|||||||
@ -112,6 +112,11 @@
|
|||||||
<i class="bi bi-receipt"></i>
|
<i class="bi bi-receipt"></i>
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<button type="button" class="btn btn-sm btn-white border text-success"
|
||||||
|
onclick="openWhatsAppModal('{{ sale.id }}', '{{ sale.customer.phone|default:'' }}', '{{ sale.invoice_number|default:sale.id }}')"
|
||||||
|
title="{% trans 'Send via WhatsApp' %}">
|
||||||
|
<i class="bi bi-whatsapp"></i>
|
||||||
|
</button>
|
||||||
<a href="{% url 'invoice_detail' sale.id %}" class="btn btn-sm btn-white border" title="{% trans 'View & Print' %}">
|
<a href="{% url 'invoice_detail' sale.id %}" class="btn btn-sm btn-white border" title="{% trans 'View & Print' %}">
|
||||||
<i class="bi bi-printer"></i>
|
<i class="bi bi-printer"></i>
|
||||||
</a>
|
</a>
|
||||||
@ -202,5 +207,86 @@
|
|||||||
{% include "core/pagination.html" with page_obj=sales %}
|
{% include "core/pagination.html" with page_obj=sales %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- WhatsApp Modal -->
|
||||||
|
<div class="modal fade" id="whatsappModal" tabindex="-1">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content border-0 shadow rounded-4">
|
||||||
|
<div class="modal-header border-0">
|
||||||
|
<h5 class="fw-bold">{% trans "Send Invoice via WhatsApp" %}</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<input type="hidden" id="waSaleId">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Invoice #" %}</label>
|
||||||
|
<input type="text" id="waInvoiceNum" class="form-control-plaintext fw-bold" readonly>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Phone Number" %}</label>
|
||||||
|
<input type="text" id="waPhone" class="form-control rounded-3" placeholder="e.g. 628123456789">
|
||||||
|
<div class="form-text">{% trans "Enter number with country code (e.g., 62...)" %}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer border-0">
|
||||||
|
<button type="button" class="btn btn-light rounded-3" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
|
||||||
|
<button type="button" class="btn btn-success rounded-3 px-4" onclick="sendWhatsAppFromList()">
|
||||||
|
<span id="waSpinner" class="spinner-border spinner-border-sm d-none me-2"></span>
|
||||||
|
{% trans "Send Message" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function openWhatsAppModal(saleId, phone, invoiceNum) {
|
||||||
|
document.getElementById('waSaleId').value = saleId;
|
||||||
|
document.getElementById('waPhone').value = phone;
|
||||||
|
document.getElementById('waInvoiceNum').value = invoiceNum;
|
||||||
|
new bootstrap.Modal(document.getElementById('whatsappModal')).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendWhatsAppFromList() {
|
||||||
|
const saleId = document.getElementById('waSaleId').value;
|
||||||
|
const phone = document.getElementById('waPhone').value;
|
||||||
|
const btn = document.querySelector('#whatsappModal .btn-success');
|
||||||
|
const spinner = document.getElementById('waSpinner');
|
||||||
|
|
||||||
|
if (!phone) {
|
||||||
|
alert("{% trans 'Please enter a phone number.' %}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
spinner.classList.remove('d-none');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch("{% url 'send_invoice_whatsapp' %}", {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-CSRFToken': '{{ csrf_token }}'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ sale_id: saleId, phone: phone })
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
alert(data.message || "{% trans 'Message sent successfully!' %}");
|
||||||
|
bootstrap.Modal.getInstance(document.getElementById('whatsappModal')).hide();
|
||||||
|
} else {
|
||||||
|
alert(data.error || "{% trans 'Failed to send message.' %}");
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert("{% trans 'An error occurred.' %}");
|
||||||
|
console.error(e);
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
spinner.classList.add('d-none');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@ -15,6 +15,43 @@
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Form -->
|
||||||
|
<div class="card border-0 shadow-sm rounded-4 mb-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="GET" class="row g-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Search" %}</label>
|
||||||
|
<input type="text" name="q" class="form-control rounded-3" placeholder="{% trans 'Purchase / Invoice No' %}" value="{{ search_query }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Date From" %}</label>
|
||||||
|
<input type="date" name="start_date" class="form-control rounded-3" value="{{ start_date }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Date To" %}</label>
|
||||||
|
<input type="date" name="end_date" class="form-control rounded-3" value="{{ end_date }}">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label small fw-bold">{% trans "Supplier" %}</label>
|
||||||
|
<select name="supplier" class="form-select rounded-3">
|
||||||
|
<option value="">{% trans "All Suppliers" %}</option>
|
||||||
|
{% for s in suppliers %}
|
||||||
|
<option value="{{ s.id }}" {% if selected_supplier == s.id %}selected{% endif %}>{{ s.name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-2 d-flex align-items-end gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary w-100 rounded-3" title="{% trans 'Filter' %}">
|
||||||
|
<i class="bi bi-search"></i>
|
||||||
|
</button>
|
||||||
|
<a href="{% url 'purchases' %}" class="btn btn-light w-100 rounded-3 border" title="{% trans 'Clear' %}">
|
||||||
|
<i class="bi bi-x-lg"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
{% for message in messages %}
|
{% for message in messages %}
|
||||||
|
|||||||
@ -733,19 +733,55 @@ def delete_sale_return(request, pk):
|
|||||||
|
|
||||||
# --- Purchases ---
|
# --- Purchases ---
|
||||||
|
|
||||||
@login_required
|
|
||||||
@login_required
|
@login_required
|
||||||
def purchases(request):
|
def purchases(request):
|
||||||
purchases = Purchase.objects.all().order_by('-created_at')
|
# Base QuerySet
|
||||||
|
purchases_qs = Purchase.objects.select_related('supplier', 'created_by').all().order_by('-created_at')
|
||||||
|
|
||||||
|
# Filtering
|
||||||
|
search_query = request.GET.get('q', '')
|
||||||
|
start_date = request.GET.get('start_date')
|
||||||
|
end_date = request.GET.get('end_date')
|
||||||
|
supplier_id = request.GET.get('supplier')
|
||||||
|
|
||||||
|
if search_query:
|
||||||
|
purchases_qs = purchases_qs.filter(
|
||||||
|
Q(invoice_number__icontains=search_query) |
|
||||||
|
Q(notes__icontains=search_query) |
|
||||||
|
Q(id__icontains=search_query)
|
||||||
|
)
|
||||||
|
|
||||||
|
if start_date:
|
||||||
|
purchases_qs = purchases_qs.filter(created_at__date__gte=start_date)
|
||||||
|
|
||||||
|
if end_date:
|
||||||
|
purchases_qs = purchases_qs.filter(created_at__date__lte=end_date)
|
||||||
|
|
||||||
|
if supplier_id:
|
||||||
|
purchases_qs = purchases_qs.filter(supplier_id=supplier_id)
|
||||||
|
|
||||||
|
# Pagination
|
||||||
|
paginator = Paginator(purchases_qs, 20)
|
||||||
|
page_number = request.GET.get('page')
|
||||||
|
page_obj = paginator.get_page(page_number)
|
||||||
|
|
||||||
|
# Context Data
|
||||||
payment_methods = PaymentMethod.objects.filter(is_active=True)
|
payment_methods = PaymentMethod.objects.filter(is_active=True)
|
||||||
site_settings = SystemSetting.objects.first()
|
site_settings = SystemSetting.objects.first()
|
||||||
if not site_settings:
|
if not site_settings:
|
||||||
site_settings = SystemSetting.objects.create()
|
site_settings = SystemSetting.objects.create()
|
||||||
|
|
||||||
|
suppliers = Supplier.objects.all().order_by('name')
|
||||||
|
|
||||||
return render(request, 'core/purchases.html', {
|
return render(request, 'core/purchases.html', {
|
||||||
'purchases': purchases,
|
'purchases': page_obj,
|
||||||
'payment_methods': payment_methods,
|
'payment_methods': payment_methods,
|
||||||
'site_settings': site_settings
|
'site_settings': site_settings,
|
||||||
|
'suppliers': suppliers,
|
||||||
|
'search_query': search_query,
|
||||||
|
'start_date': start_date,
|
||||||
|
'end_date': end_date,
|
||||||
|
'selected_supplier': int(supplier_id) if supplier_id and supplier_id.isdigit() else None
|
||||||
})
|
})
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user