Autosave: 20260211-044531
This commit is contained in:
parent
a9b274a48f
commit
48923270af
Binary file not shown.
@ -219,11 +219,8 @@
|
||||
page-break-after: always;
|
||||
box-sizing: border-box;
|
||||
|
||||
/* Use Flexbox for Grid Layout - More reliable for print */
|
||||
display: flex !important;
|
||||
flex-wrap: wrap !important;
|
||||
justify-content: flex-start !important;
|
||||
align-items: flex-start !important;
|
||||
/* Revert to Block + Float for reliability in all print engines */
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
/* Direct print container override */
|
||||
@ -238,13 +235,24 @@
|
||||
box-sizing: border-box;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
/* Float strategy for grid */
|
||||
float: left !important;
|
||||
/* Keep flex for INTERNAL content alignment only */
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
/* REMOVED width: 100% to prevent full-width expansion */
|
||||
|
||||
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) --- */
|
||||
|
||||
@ -112,6 +112,11 @@
|
||||
<i class="bi bi-receipt"></i>
|
||||
</a>
|
||||
{% 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' %}">
|
||||
<i class="bi bi-printer"></i>
|
||||
</a>
|
||||
@ -202,5 +207,86 @@
|
||||
{% include "core/pagination.html" with page_obj=sales %}
|
||||
</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 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 %}
|
||||
@ -15,6 +15,43 @@
|
||||
</a>
|
||||
</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 %}
|
||||
<div class="mb-4">
|
||||
{% for message in messages %}
|
||||
|
||||
@ -733,19 +733,55 @@ def delete_sale_return(request, pk):
|
||||
|
||||
# --- Purchases ---
|
||||
|
||||
@login_required
|
||||
@login_required
|
||||
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)
|
||||
site_settings = SystemSetting.objects.first()
|
||||
if not site_settings:
|
||||
site_settings = SystemSetting.objects.create()
|
||||
|
||||
|
||||
suppliers = Supplier.objects.all().order_by('name')
|
||||
|
||||
return render(request, 'core/purchases.html', {
|
||||
'purchases': purchases,
|
||||
'purchases': page_obj,
|
||||
'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
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user