297 lines
15 KiB
HTML
297 lines
15 KiB
HTML
{% extends 'base.html' %}
|
|
{% load i18n l10n %}
|
|
|
|
{% block title %}{% trans "New Purchase" %} | {{ site_settings.business_name }}{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="container-fluid px-4" id="purchaseApp">
|
|
<div class="row">
|
|
<!-- Main Form -->
|
|
<div class="col-lg-8">
|
|
<div class="card border-0 shadow-sm rounded-4 mb-4">
|
|
<div class="card-header bg-white border-0 pt-4 px-4">
|
|
<h5 class="fw-bold mb-0"><i class="bi bi-cart-plus me-2 text-primary"></i>{% trans "Create Purchase Invoice" %}</h5>
|
|
</div>
|
|
<div class="card-body p-4">
|
|
<!-- Supplier & Invoice Info -->
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md-6">
|
|
<label class="form-label small fw-bold">{% trans "Supplier" %}</label>
|
|
<select class="form-select rounded-3 shadow-none border-secondary-subtle" v-model="supplierId">
|
|
<option value="">{% trans "Select Supplier" %}</option>
|
|
{% for supplier in suppliers %}
|
|
<option value="{{ supplier.id }}">{{ supplier.name }}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label small fw-bold">{% trans "Reference / Invoice #" %}</label>
|
|
<input type="text" class="form-control rounded-3 shadow-none border-secondary-subtle" v-model="invoiceNumber" placeholder="{% trans 'e.g. INV-2024-001' %}">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Item Selection -->
|
|
<div class="mb-4">
|
|
<label class="form-label small fw-bold">{% trans "Add Items to Invoice" %}</label>
|
|
<div class="input-group">
|
|
<span class="input-group-text bg-light border-end-0 border-secondary-subtle"><i class="bi bi-search"></i></span>
|
|
<input type="text" class="form-control rounded-3 border-start-0 border-secondary-subtle shadow-none" placeholder="{% trans 'Search by Name or SKU...' %}" v-model="searchQuery" @input="filterProducts">
|
|
</div>
|
|
|
|
<div class="position-relative">
|
|
<div class="list-group position-absolute w-100 shadow rounded-3 mt-1" style="z-index: 1000;" v-if="filteredProducts.length > 0">
|
|
<button v-for="product in filteredProducts" :key="product.id" class="list-group-item list-group-item-action border-0 py-3" @click="addItem(product)">
|
|
<div class="d-flex justify-content-between">
|
|
<div>
|
|
<span class="fw-bold">[[ product.name_en ]]</span> / [[ product.name_ar ]]
|
|
<div class="text-muted small">SKU: [[ product.sku ]] | Stock: [[ product.stock ]]</div>
|
|
</div>
|
|
<div class="text-primary fw-bold">[[ currencySymbol ]][[ product.cost_price ]]</div>
|
|
</div>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Items Table -->
|
|
<div class="table-responsive">
|
|
<table class="table align-middle">
|
|
<thead class="bg-light-subtle">
|
|
<tr class="small text-uppercase text-muted fw-bold">
|
|
<th style="width: 40%;">{% trans "Product" %}</th>
|
|
<th class="text-center">{% trans "Cost Price" %}</th>
|
|
<th class="text-center">{% trans "Expiry Date" %}</th>
|
|
<th class="text-center" style="width: 15%;">{% trans "Quantity" %}</th>
|
|
<th class="text-end">{% trans "Total" %}</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="(item, index) in cart" :key="index">
|
|
<td>
|
|
<div class="fw-bold">[[ item.name_en ]]</div>
|
|
<div class="text-muted small">[[ item.sku ]]</div>
|
|
</td>
|
|
<td>
|
|
<input type="number" step="0.001" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" v-model="item.price" @input="calculateTotal">
|
|
</td>
|
|
<td>
|
|
<input type="date" class="form-control form-control-sm border-0 border-bottom rounded-0" v-model="item.expiry_date">
|
|
</td>
|
|
<td>
|
|
<input type="number" class="form-control form-control-sm text-center border-0 border-bottom rounded-0" step="0.01" v-model="item.quantity" @input="calculateTotal">
|
|
</td>
|
|
<td class="text-end fw-bold">[[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]]</td>
|
|
<td class="text-end">
|
|
<button class="btn btn-link text-danger p-0" @click="removeItem(index)"><i class="bi bi-x-circle"></i></button>
|
|
</td>
|
|
</tr>
|
|
<tr v-if="cart.length === 0">
|
|
<td colspan="5" class="text-center py-5 text-muted">
|
|
{% trans "No items added yet." %}
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Order Summary -->
|
|
<div class="col-lg-4">
|
|
<div class="card border-0 shadow-sm rounded-4 sticky-top" style="top: 20px;">
|
|
<div class="card-body p-4">
|
|
<h5 class="fw-bold mb-4">{% trans "Purchase Summary" %}</h5>
|
|
|
|
<div class="d-flex justify-content-between mb-2">
|
|
<span class="text-muted">{% trans "Subtotal" %}</span>
|
|
<span class="fw-bold">[[ currencySymbol ]][[ subtotal.toFixed(decimalPlaces) ]]</span>
|
|
</div>
|
|
|
|
<hr class="my-4">
|
|
|
|
<div class="d-flex justify-content-between mb-4">
|
|
<h4 class="fw-bold mb-0">{% trans "Grand Total" %}</h4>
|
|
<h4 class="fw-bold text-primary mb-0">[[ currencySymbol ]][[ subtotal.toFixed(decimalPlaces) ]]</h4>
|
|
</div>
|
|
|
|
<!-- Payment Details -->
|
|
<div class="mb-3">
|
|
<label class="form-label small fw-bold">{% trans "Payment Type" %}</label>
|
|
<select class="form-select rounded-3 shadow-none border-secondary-subtle" v-model="paymentType">
|
|
<option value="cash">{% trans "Full Cash" %}</option>
|
|
<option value="credit">{% trans "Full Credit" %}</option>
|
|
<option value="partial">{% trans "Partial Payment" %}</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-3" v-if="paymentType !== 'credit'">
|
|
<label class="form-label small fw-bold">{% trans "Payment Method" %}</label>
|
|
<select class="form-select rounded-3 shadow-none border-secondary-subtle" v-model="paymentMethodId">
|
|
{% for method in payment_methods %}
|
|
<option value="{{ method.id }}">{% if LANGUAGE_CODE == 'ar' %}{{ method.name_ar }}{% else %}{{ method.name_en }}{% endif %}</option>
|
|
{% endfor %}
|
|
</select>
|
|
</div>
|
|
|
|
<div class="mb-3" v-if="paymentType === 'partial'">
|
|
<label class="form-label small fw-bold">{% trans "Amount Paid" %}</label>
|
|
<input type="number" step="0.001" class="form-control rounded-3" v-model="paidAmount">
|
|
</div>
|
|
|
|
<div class="mb-4" v-if="paymentType !== 'cash'">
|
|
<label class="form-label small fw-bold">{% trans "Due Date" %}</label>
|
|
<input type="date" class="form-control rounded-3" v-model="dueDate">
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<label class="form-label small fw-bold">{% trans "Notes" %}</label>
|
|
<textarea class="form-control rounded-3" rows="2" v-model="notes"></textarea>
|
|
</div>
|
|
|
|
<div class="d-grid">
|
|
<button class="btn btn-primary rounded-3 py-3 fw-bold shadow-sm" :disabled="isProcessing || cart.length === 0 || !supplierId" @click="savePurchase">
|
|
<span v-if="isProcessing" class="spinner-border spinner-border-sm me-2"></span>
|
|
<i class="bi bi-check2-circle me-2" v-else></i>
|
|
{% trans "Finalize Purchase" %}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Vue.js 3 -->
|
|
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
|
|
{% localize off %}
|
|
<script>
|
|
const { createApp } = Vue;
|
|
|
|
createApp({
|
|
delimiters: ['[[', ']]'],
|
|
data() {
|
|
return {
|
|
products: [
|
|
{% for p in products %}
|
|
{
|
|
id: {{ p.id|default:0 }},
|
|
name_en: "{{ p.name_en|escapejs }}",
|
|
name_ar: "{{ p.name_ar|escapejs }}",
|
|
sku: "{{ p.sku|escapejs }}",
|
|
cost_price: {{ p.cost_price|default:0 }},
|
|
stock: {{ p.stock_quantity|default:0 }}
|
|
},
|
|
{% endfor %}
|
|
],
|
|
searchQuery: '',
|
|
filteredProducts: [],
|
|
cart: [],
|
|
supplierId: '',
|
|
invoiceNumber: '',
|
|
paymentType: 'cash',
|
|
paymentMethodId: '{% if payment_methods.first %}{{ payment_methods.first.id }}{% endif %}',
|
|
paidAmount: 0,
|
|
dueDate: '',
|
|
notes: '',
|
|
currencySymbol: '{{ site_settings.currency_symbol|escapejs }}',
|
|
decimalPlaces: {{ decimal_places|default:3 }},
|
|
isProcessing: false
|
|
}
|
|
},
|
|
computed: {
|
|
subtotal() {
|
|
return this.cart.reduce((total, item) => total + (item.price * item.quantity), 0);
|
|
}
|
|
},
|
|
methods: {
|
|
filterProducts() {
|
|
if (this.searchQuery.length > 1) {
|
|
const query = this.searchQuery.toLowerCase();
|
|
this.filteredProducts = this.products.filter(p =>
|
|
p.name_en.toLowerCase().includes(query) ||
|
|
p.sku.toLowerCase().includes(query) ||
|
|
p.name_ar.includes(query)
|
|
).slice(0, 5);
|
|
} else {
|
|
this.filteredProducts = [];
|
|
}
|
|
},
|
|
addItem(product) {
|
|
const existing = this.cart.find(item => item.id === product.id);
|
|
if (existing) {
|
|
existing.quantity++;
|
|
} else {
|
|
this.cart.push({
|
|
id: product.id,
|
|
name_en: product.name_en,
|
|
sku: product.sku,
|
|
price: product.cost_price,
|
|
quantity: 1,
|
|
expiry_date: ""
|
|
});
|
|
}
|
|
this.searchQuery = '';
|
|
this.filteredProducts = [];
|
|
},
|
|
removeItem(index) {
|
|
this.cart.splice(index, 1);
|
|
},
|
|
savePurchase() {
|
|
if (this.isProcessing) return;
|
|
this.isProcessing = true;
|
|
|
|
let actualPaidAmount = 0;
|
|
if (this.paymentType === 'cash') {
|
|
actualPaidAmount = this.subtotal;
|
|
} else if (this.paymentType === 'partial') {
|
|
actualPaidAmount = this.paidAmount;
|
|
}
|
|
|
|
const payload = {
|
|
supplier_id: this.supplierId,
|
|
invoice_number: this.invoiceNumber,
|
|
items: this.cart.map(item => ({
|
|
id: item.id,
|
|
quantity: item.quantity,
|
|
price: item.price,
|
|
expiry_date: item.expiry_date,
|
|
line_total: item.price * item.quantity
|
|
})),
|
|
total_amount: this.subtotal,
|
|
paid_amount: actualPaidAmount,
|
|
payment_type: this.paymentType,
|
|
payment_method_id: this.paymentMethodId,
|
|
due_date: this.dueDate,
|
|
notes: this.notes
|
|
};
|
|
|
|
fetch("{% url 'create_purchase_api' %}", {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-CSRFToken': '{{ csrf_token }}'
|
|
},
|
|
body: JSON.stringify(payload)
|
|
})
|
|
.then(res => res.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
window.location.href = "{% url 'purchases' %}";
|
|
} else {
|
|
alert("Error: " + data.error);
|
|
this.isProcessing = false;
|
|
}
|
|
})
|
|
.catch(err => {
|
|
console.error(err);
|
|
alert("An unexpected error occurred.");
|
|
this.isProcessing = false;
|
|
});
|
|
}
|
|
}
|
|
}).mount('#purchaseApp');
|
|
</script>
|
|
{% endlocalize %}
|
|
{% endblock %} |