Revert to version 850f4f6

This commit is contained in:
Flatlogic Bot 2026-02-06 03:57:25 +00:00
parent c2c1cb5d82
commit 75d9aea042
16 changed files with 105 additions and 682 deletions

View File

@ -61,7 +61,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'core',
'accounting',
# 'hr',
'hr',
]
MIDDLEWARE = [

View File

@ -9,7 +9,7 @@ urlpatterns = [
path("i18n/", include("django.conf.urls.i18n")),
path("", include("core.urls")),
path("accounting/", include("accounting.urls")),
# path("hr/", include("hr.urls")),
path("hr/", include("hr.urls")),
]
if settings.DEBUG:

View File

@ -256,9 +256,6 @@ class Purchase(models.Model):
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="purchases")
created_at = models.DateTimeField(auto_now_add=True)
# New field to link back to LOP
lop = models.OneToOneField('LOP', on_delete=models.SET_NULL, null=True, blank=True, related_name="purchase")
def __str__(self):
return f"Purchase #{self.id} - {self.supplier.name if self.supplier else 'N/A'}"
@ -307,7 +304,7 @@ class SaleReturn(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Return #{self.id} for Sale #{self.sale.id if self.sale else 'N/A'}"
return f"Sale Return #{self.id} - {self.customer.name if self.customer else 'Guest'}"
class SaleReturnItem(models.Model):
sale_return = models.ForeignKey(SaleReturn, on_delete=models.CASCADE, related_name="items")
@ -329,74 +326,72 @@ class PurchaseReturn(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Return #{self.id} for Purchase #{self.purchase.id if self.purchase else 'N/A'}"
return f"Purchase Return #{self.id} - {self.supplier.name if self.supplier else 'N/A'}"
class PurchaseReturnItem(models.Model):
purchase_return = models.ForeignKey(PurchaseReturn, on_delete=models.CASCADE, related_name="items")
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.DecimalField(_("Quantity"), max_digits=15, decimal_places=2)
cost_price = models.DecimalField(_("Cost Price"), max_digits=12, decimal_places=3)
expiry_date = models.DateField(_("Expiry Date"), null=True, blank=True)
line_total = models.DecimalField(_("Line Total"), max_digits=15, decimal_places=3)
def __str__(self):
return f"{self.product.name_en} x {self.quantity}"
class SystemSetting(models.Model):
business_name = models.CharField(_("Business Name"), max_length=200, default="Meezan Accounting")
address = models.TextField(_("Address"), blank=True)
phone = models.CharField(_("Phone"), max_length=50, blank=True)
email = models.EmailField(_("Email"), blank=True)
vat_number = models.CharField(_("VAT Number"), max_length=50, blank=True)
logo = models.ImageField(_("Company Logo"), upload_to="company_logos/", blank=True, null=True)
currency = models.CharField(_("Currency"), max_length=10, default="SAR")
vat_percentage = models.DecimalField(_("Default VAT %"), max_digits=5, decimal_places=2, default=15.00)
footer_text = models.TextField(_("Invoice Footer Text"), blank=True)
decimal_places = models.IntegerField(_("Decimal Places"), default=2)
# Wablas (WhatsApp) Integration
wablas_enabled = models.BooleanField(_("Enable Wablas WhatsApp"), default=False)
wablas_server_url = models.URLField(_("Wablas Server URL"), blank=True, help_text=_("e.g., https://tegal.wablas.com"))
wablas_api_token = models.CharField(_("Wablas API Token"), max_length=255, blank=True)
wablas_secret_key = models.CharField(_("Wablas Secret Key"), max_length=255, blank=True, help_text=_("Used for verifying webhooks"))
def __str__(self):
return "System Settings"
class HeldSale(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name="held_sales", null=True, blank=True) # Changed to match migration
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="held_sales_created") # Added to match migration potentially
customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True, related_name="held_sales")
cart_data = models.JSONField(_("Cart Data"))
total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3)
notes = models.TextField(_("Notes"), blank=True)
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="held_sales")
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return f"Held Sale {self.id} - {self.customer.name if self.customer else 'Guest'}"
return f"Held Sale #{self.id} - {self.created_at.strftime('%Y-%m-%d %H:%M')}"
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
image = models.ImageField(_("Profile Picture"), upload_to='profile_pics/', null=True, blank=True)
phone = models.CharField(_("Phone Number"), max_length=20, blank=True)
bio = models.TextField(_("Bio"), blank=True)
role = models.CharField(max_length=20, choices=[('admin', 'Admin'), ('manager', 'Manager'), ('cashier', 'Cashier')], default='cashier')
class SystemSetting(models.Model):
business_name = models.CharField(_("Business Name"), max_length=200, default="Meezan Accounting")
address = models.TextField(_("Address"), blank=True)
phone = models.CharField(_("Phone"), max_length=20, blank=True)
email = models.EmailField(_("Email"), blank=True)
currency_symbol = models.CharField(_("Currency Symbol"), max_length=10, default="OMR")
tax_rate = models.DecimalField(_("Tax Rate (%)"), max_digits=5, decimal_places=2, default=0)
decimal_places = models.PositiveSmallIntegerField(_("Decimal Places"), default=3)
logo = models.ImageField(_("Logo"), upload_to="business_logos/", blank=True, null=True)
vat_number = models.CharField(_("VAT Number"), max_length=50, blank=True)
registration_number = models.CharField(_("Registration Number"), max_length=50, blank=True)
# Loyalty Settings
loyalty_enabled = models.BooleanField(_("Enable Loyalty System"), default=False)
points_per_currency = models.DecimalField(_("Points Earned per Currency Unit"), max_digits=10, decimal_places=2, default=1.0)
currency_per_point = models.DecimalField(_("Currency Value per Point"), max_digits=10, decimal_places=3, default=0.010)
min_points_to_redeem = models.PositiveIntegerField(_("Minimum Points to Redeem"), default=100)
# WhatsApp (Wablas) Settings
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.user.username
return self.business_name
class Device(models.Model):
DEVICE_TYPES = [
('printer', 'Printer'),
('scanner', 'Scanner'),
('scale', 'Weight Scale'),
('display', 'Customer Display'),
('other', 'Other'),
('printer', _('Printer')),
('scanner', _('Scanner')),
('scale', _('Weight Scale')),
('display', _('Customer Display')),
('other', _('Other')),
]
CONNECTION_TYPES = [
('network', 'Network (IP)'),
('usb', 'USB'),
('bluetooth', 'Bluetooth'),
('network', _('Network (IP)')),
('usb', _('USB')),
('bluetooth', _('Bluetooth')),
]
name = models.CharField(_("Device Name"), max_length=100)
device_type = models.CharField(_("Device Type"), max_length=20, choices=DEVICE_TYPES)
connection_type = models.CharField(_("Connection Type"), max_length=20, choices=CONNECTION_TYPES, default='network')
@ -407,33 +402,24 @@ class Device(models.Model):
created_at = models.DateTimeField(auto_now_add=True)
def __str__(self):
return self.name
return f"{self.name} ({self.get_device_type_display()})"
class LOP(models.Model):
STATUS_CHOICES = [
('draft', _('Draft')),
('converted', _('Converted to Purchase')),
('cancelled', _('Cancelled')),
]
supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, related_name="lops")
lop_number = models.CharField(_("LOP Number"), max_length=50, blank=True)
total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3)
status = models.CharField(_("Status"), max_length=20, choices=STATUS_CHOICES, default='draft')
date = models.DateField(_("Date"), default=timezone.now)
notes = models.TextField(_("Notes"), blank=True)
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, related_name="lops")
created_at = models.DateTimeField(auto_now_add=True)
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name="profile")
image = models.ImageField(_("Profile Picture"), upload_to="profile_pics/", blank=True, null=True)
phone = models.CharField(_("Phone Number"), max_length=20, blank=True)
bio = models.TextField(_("Bio"), blank=True)
def __str__(self):
return f"LOP #{self.id} - {self.supplier.name if self.supplier else 'N/A'}"
return self.user.username
class LOPItem(models.Model):
lop = models.ForeignKey(LOP, on_delete=models.CASCADE, related_name="items")
product = models.ForeignKey(Product, on_delete=models.CASCADE)
quantity = models.DecimalField(_("Quantity"), max_digits=15, decimal_places=2)
cost_price = models.DecimalField(_("Cost Price"), max_digits=12, decimal_places=3)
line_total = models.DecimalField(_("Line Total"), max_digits=15, decimal_places=3)
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
UserProfile.objects.create(user=instance)
def __str__(self):
return f"{self.product.name_en} x {self.quantity}"
@receiver(post_save, sender=User)
def save_user_profile(sender, instance, **kwargs):
if not hasattr(instance, 'profile'):
UserProfile.objects.create(user=instance)
instance.profile.save()

View File

@ -88,16 +88,11 @@
<!-- Purchases Group -->
<li class="sidebar-group-header mt-2">
<a href="#purchasesSubmenu" data-bs-toggle="collapse" aria-expanded="{% if url_name == 'purchases' or url_name == 'purchase_create' or url_name == 'purchase_detail' or url_name == 'supplier_payments' or 'purchases/returns' in path or url_name == 'lops' or url_name == 'lop_create' or url_name == 'lop_detail' %}true{% else %}false{% endif %}" class="dropdown-toggle-custom">
<a href="#purchasesSubmenu" data-bs-toggle="collapse" aria-expanded="{% if url_name == 'purchases' or url_name == 'purchase_create' or url_name == 'purchase_detail' or url_name == 'supplier_payments' or 'purchases/returns' in path %}true{% else %}false{% endif %}" class="dropdown-toggle-custom">
<span>{% trans "Purchases" %}</span>
<i class="bi bi-chevron-down chevron"></i>
</a>
<ul class="collapse list-unstyled sub-menu {% if url_name == 'purchases' or url_name == 'purchase_create' or url_name == 'purchase_detail' or url_name == 'supplier_payments' or 'purchases/returns' in path or url_name == 'lops' or url_name == 'lop_create' or url_name == 'lop_detail' %}show{% endif %}" id="purchasesSubmenu">
<li>
<a href="{% url 'lops' %}" class="{% if url_name == 'lops' or url_name == 'lop_create' or url_name == 'lop_detail' %}active{% endif %}">
<i class="bi bi-file-earmark-text"></i> {% trans "Local Purchase Orders (LOP)" %}
</a>
</li>
<ul class="collapse list-unstyled sub-menu {% if url_name == 'purchases' or url_name == 'purchase_create' or url_name == 'purchase_detail' or url_name == 'supplier_payments' or 'purchases/returns' in path %}show{% endif %}" id="purchasesSubmenu">
<li>
<a href="{% url 'purchase_create' %}" class="{% if url_name == 'purchase_create' %}active{% endif %}">
<i class="bi bi-plus-circle"></i> {% trans "New Purchase" %}
@ -224,6 +219,46 @@
</ul>
</li>
<!-- HR Group -->
<li class="sidebar-group-header mt-2">
<a href="#hrSubmenu" data-bs-toggle="collapse" aria-expanded="{% if 'hr/' in path %}true{% else %}false{% endif %}" class="dropdown-toggle-custom">
<span>{% trans "Human Resources" %}</span>
<i class="bi bi-chevron-down chevron"></i>
</a>
<ul class="collapse list-unstyled sub-menu {% if 'hr/' in path %}show{% endif %}" id="hrSubmenu">
<li>
<a href="{% url 'hr:dashboard' %}" class="{% if url_name == 'dashboard' and 'hr/' in path %}active{% endif %}">
<i class="bi bi-speedometer"></i> {% trans "Overview" %}
</a>
</li>
<li>
<a href="{% url 'hr:employee_list' %}" class="{% if 'hr/employees' in path %}active{% endif %}">
<i class="bi bi-people-fill"></i> {% trans "Employees" %}
</a>
</li>
<li>
<a href="{% url 'hr:department_list' %}" class="{% if 'hr/departments' in path %}active{% endif %}">
<i class="bi bi-diagram-3"></i> {% trans "Departments" %}
</a>
</li>
<li>
<a href="{% url 'hr:attendance_list' %}" class="{% if 'hr/attendance' in path %}active{% endif %}">
<i class="bi bi-clock-history"></i> {% trans "Attendance" %}
</a>
</li>
<li>
<a href="{% url 'hr:leave_list' %}" class="{% if 'hr/leave' in path %}active{% endif %}">
<i class="bi bi-calendar2-range"></i> {% trans "Leave Requests" %}
</a>
</li>
<li>
<a href="{% url 'hr:device_list' %}" class="{% if 'hr/devices' in path %}active{% endif %}">
<i class="bi bi-fingerprint"></i> {% trans "Biometric Devices" %}
</a>
</li>
</ul>
</li>
<!-- Reports Group -->
<li class="sidebar-group-header mt-2">
<a href="#reportsSubmenu" data-bs-toggle="collapse" aria-expanded="{% if url_name == 'reports' or url_name == 'customer_statement' or url_name == 'supplier_statement' or url_name == 'cashflow_report' %}true{% else %}false{% endif %}" class="dropdown-toggle-custom">

View File

@ -1,241 +0,0 @@
{% extends 'base.html' %}
{% load i18n l10n %}
{% block title %}{% trans "New LOP" %} | {{ site_settings.business_name }}{% endblock %}
{% block content %}
<div class="container-fluid px-4" id="lopApp">
<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-file-earmark-text me-2 text-primary"></i>{% trans "Create Local Purchase Order" %}</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 "LOP Number / Reference" %}</label>
<input type="text" class="form-control rounded-3 shadow-none border-secondary-subtle" v-model="lopNumber" placeholder="{% trans 'e.g. LOP-001' %}">
</div>
</div>
<!-- Item Selection -->
<div class="mb-4">
<label class="form-label small fw-bold">{% trans "Add Items" %}</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 ]]</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" 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="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>
<!-- 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 "LOP Summary" %}</h5>
<div class="d-flex justify-content-between mb-2">
<span class="text-muted">{% trans "Total Amount" %}</span>
<span class="fw-bold">[[ currencySymbol ]][[ subtotal.toFixed(decimalPlaces) ]]</span>
</div>
<hr class="my-4">
<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="saveLOP">
<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 "Save LOP" %}
</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 }}
},
{% endfor %}
],
searchQuery: '',
filteredProducts: [],
cart: [],
supplierId: '',
lopNumber: '',
notes: '',
currencySymbol: '{{ site_settings.currency_symbol|escapejs }}',
decimalPlaces: {{ site_settings.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
});
}
this.searchQuery = '';
this.filteredProducts = [];
},
removeItem(index) {
this.cart.splice(index, 1);
},
saveLOP() {
if (this.isProcessing) return;
this.isProcessing = true;
const payload = {
supplier_id: this.supplierId,
lop_number: this.lopNumber,
items: this.cart.map(item => ({
id: item.id,
quantity: item.quantity,
price: item.price,
line_total: item.price * item.quantity
})),
total_amount: this.subtotal,
notes: this.notes
};
fetch("{% url 'create_lop_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 'lops' %}";
} else {
alert("Error: " + data.error);
this.isProcessing = false;
}
})
.catch(err => {
console.error(err);
alert("An unexpected error occurred.");
this.isProcessing = false;
});
}
}
}).mount('#lopApp');
</script>
{% endlocalize %}
{% endblock %}

View File

@ -1,108 +0,0 @@
{% extends 'base.html' %}
{% load i18n humanize %}
{% block title %}{% trans "LOP Details" %} #{{ lop.lop_number|default:lop.id }} | {{ site_settings.business_name }}{% endblock %}
{% block content %}
<div class="container-fluid px-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-0">{% trans "LOP" %} #{{ lop.lop_number|default:lop.id }}</h2>
<p class="text-muted small mb-0">{{ lop.created_at|date:"F j, Y" }}</p>
</div>
<div class="d-flex gap-2">
<a href="{% url 'lops' %}" class="btn btn-light rounded-3 shadow-sm border">
<i class="bi bi-arrow-left me-2"></i>{% trans "Back" %}
</a>
{% if lop.status == 'draft' %}
<a href="{% url 'convert_lop_to_purchase' lop.id %}" class="btn btn-primary rounded-3 shadow-sm" onclick="return confirm('{% trans "Are you sure?" %}')">
<i class="bi bi-arrow-repeat me-2"></i>{% trans "Convert to Purchase" %}
</a>
{% endif %}
<button onclick="window.print()" class="btn btn-secondary rounded-3 shadow-sm">
<i class="bi bi-printer me-2"></i>{% trans "Print" %}
</button>
</div>
</div>
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-5">
<!-- Header -->
<div class="row mb-5">
<div class="col-6">
<h3 class="fw-bold text-primary mb-2">{{ site_settings.business_name }}</h3>
<p class="text-muted mb-0" style="white-space: pre-line;">{{ site_settings.address }}
{{ site_settings.phone }}
{{ site_settings.email }}</p>
</div>
<div class="col-6 text-end">
<h1 class="fw-bold text-secondary mb-2">{% trans "LOCAL PURCHASE ORDER" %}</h1>
<p class="mb-1"><strong>{% trans "LOP #" %}:</strong> {{ lop.lop_number|default:lop.id }}</p>
<p class="mb-1"><strong>{% trans "Date" %}:</strong> {{ lop.created_at|date:"Y-m-d" }}</p>
<p class="mb-0"><strong>{% trans "Status" %}:</strong>
{% if lop.status == 'converted' %}<span class="badge bg-success">{% trans "Converted" %}</span>
{% elif lop.status == 'cancelled' %}<span class="badge bg-danger">{% trans "Cancelled" %}</span>
{% else %}<span class="badge bg-secondary">{% trans "Draft" %}</span>{% endif %}
</p>
</div>
</div>
<!-- Supplier Info -->
<div class="row mb-5">
<div class="col-6">
<h6 class="text-uppercase text-muted fw-bold small mb-3">{% trans "Bill To" %}</h6>
<h5 class="fw-bold mb-1">{{ lop.supplier.name|default:"-" }}</h5>
<p class="text-muted mb-0">{{ lop.supplier.phone }}</p>
</div>
</div>
<!-- Items -->
<div class="table-responsive mb-5">
<table class="table table-borderless">
<thead class="bg-light">
<tr>
<th class="ps-4 py-3">{% trans "Description" %}</th>
<th class="text-center py-3">{% trans "Quantity" %}</th>
<th class="text-end py-3">{% trans "Unit Cost" %}</th>
<th class="text-end pe-4 py-3">{% trans "Total" %}</th>
</tr>
</thead>
<tbody>
{% for item in lop.items.all %}
<tr class="border-bottom">
<td class="ps-4 py-3">
<div class="fw-bold">{{ item.product.name_en }}</div>
<div class="text-muted small">{{ item.product.name_ar }}</div>
</td>
<td class="text-center py-3">{{ item.quantity }}</td>
<td class="text-end py-3">{{ site_settings.currency_symbol }}{{ item.cost_price|floatformat:3 }}</td>
<td class="text-end pe-4 py-3 fw-bold">{{ site_settings.currency_symbol }}{{ item.line_total|floatformat:3 }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Totals -->
<div class="row justify-content-end">
<div class="col-md-5 col-lg-4">
<div class="d-flex justify-content-between mb-3">
<span class="fw-bold h5">{% trans "Total Amount" %}</span>
<span class="fw-bold h5 text-primary">{{ site_settings.currency_symbol }}{{ lop.total_amount|floatformat:3 }}</span>
</div>
{% if amount_in_words %}
<p class="text-muted small text-end fst-italic">{{ amount_in_words }}</p>
{% endif %}
</div>
</div>
{% if lop.notes %}
<div class="mt-5">
<h6 class="fw-bold">{% trans "Notes" %}</h6>
<p class="text-muted">{{ lop.notes }}</p>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@ -1,120 +0,0 @@
{% extends 'base.html' %}
{% load i18n %}
{% block title %}{% trans "Local Purchase Orders (LOP)" %} | {{ site_settings.business_name }}{% endblock %}
{% block content %}
<div class="container-fluid px-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h2 class="fw-bold mb-0">{% trans "Local Purchase Orders (LOP)" %}</h2>
<p class="text-muted small mb-0">{% trans "Manage local purchase orders and convert them to invoices" %}</p>
</div>
<a href="{% url 'lop_create' %}" class="btn btn-primary rounded-3 px-4 shadow-sm">
<i class="bi bi-plus-circle me-2"></i>{% trans "Create LOP" %}
</a>
</div>
{% if messages %}
<div class="mb-4">
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show rounded-3 shadow-sm border-0" role="alert">
<i class="bi bi-info-circle me-2"></i>{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
{% endif %}
<div class="card border-0 shadow-sm rounded-4">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover align-middle mb-0">
<thead class="bg-light">
<tr>
<th class="ps-4">{% trans "LOP #" %}</th>
<th>{% trans "Date" %}</th>
<th>{% trans "Supplier" %}</th>
<th>{% trans "Total" %}</th>
<th>{% trans "User" %}</th>
<th>{% trans "Status" %}</th>
<th class="text-end pe-4">{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for lop in lops %}
<tr>
<td class="ps-4 fw-bold">
<a href="{% url 'lop_detail' lop.id %}" class="text-decoration-none">
{{ lop.lop_number|default:lop.id }}
</a>
</td>
<td>{{ lop.created_at|date:"Y-m-d" }}</td>
<td>{{ lop.supplier.name|default:"-" }}</td>
<td class="fw-bold text-dark">{{ site_settings.currency_symbol }}{{ lop.total_amount|floatformat:3 }}</td>
<td>
<span class="text-muted small">
<i class="bi bi-person me-1"></i>{{ lop.created_by.username|default:"System" }}
</span>
</td>
<td>
{% if lop.status == 'converted' %}
<span class="badge bg-success-subtle text-success rounded-pill px-3">{% trans "Converted" %}</span>
{% elif lop.status == 'cancelled' %}
<span class="badge bg-danger-subtle text-danger rounded-pill px-3">{% trans "Cancelled" %}</span>
{% else %}
<span class="badge bg-secondary-subtle text-secondary rounded-pill px-3">{% trans "Draft" %}</span>
{% endif %}
</td>
<td class="text-end pe-4">
<div class="btn-group shadow-sm rounded-3">
<a href="{% url 'lop_detail' lop.id %}" class="btn btn-sm btn-white border" title="{% trans 'View' %}">
<i class="bi bi-eye"></i>
</a>
{% if lop.status == 'draft' %}
<a href="{% url 'convert_lop_to_purchase' lop.id %}" class="btn btn-sm btn-white border text-primary" title="{% trans 'Convert to Purchase Invoice' %}" onclick="return confirm('{% trans "Are you sure you want to convert this LOP to a Purchase Invoice? Stock will be updated." %}')">
<i class="bi bi-arrow-repeat"></i>
</a>
{% endif %}
{% if lop.status != 'converted' %}
<button type="button" class="btn btn-sm btn-white border text-danger" data-bs-toggle="modal" data-bs-target="#deleteModal{{ lop.id }}">
<i class="bi bi-trash"></i>
</button>
{% endif %}
</div>
<!-- Delete Modal -->
<div class="modal fade text-start" id="deleteModal{{ lop.id }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content border-0 shadow rounded-4">
<div class="modal-body p-4 text-center">
<div class="text-danger mb-3">
<i class="bi bi-exclamation-octagon" style="font-size: 3rem;"></i>
</div>
<h4 class="fw-bold">{% trans "Delete LOP?" %}</h4>
<p class="text-muted">{% trans "This action cannot be undone." %}</p>
<div class="d-grid gap-2">
<a href="{% url 'delete_lop' lop.id %}" class="btn btn-danger rounded-3 py-2">{% trans "Yes, Delete" %}</a>
<button type="button" class="btn btn-light rounded-3 py-2" data-bs-dismiss="modal">{% trans "Cancel" %}</button>
</div>
</div>
</div>
</div>
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="7" class="text-center py-5">
<p class="text-muted">{% trans "No LOPs recorded yet." %}</p>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% include "core/pagination.html" with page_obj=lops %}
</div>
</div>
</div>
{% endblock %}

View File

@ -45,12 +45,6 @@ urlpatterns = [
path('api/create-sale-return/', views.create_sale_return_api, name='create_sale_return_api'),
# Purchases (Invoices)
path('purchases/lops/', views.lops, name='lops'),
path('purchases/lops/create/', views.lop_create, name='lop_create'),
path('purchases/lops/<int:pk>/', views.lop_detail, name='lop_detail'),
path('purchases/lops/<int:pk>/convert/', views.convert_lop_to_purchase, name='convert_lop_to_purchase'),
path('purchases/lops/<int:pk>/delete/', views.delete_lop, name='delete_lop'),
path('api/create-lop/', views.create_lop_api, name='create_lop_api'),
path('purchases/create/', views.purchase_create, name='purchase_create'),
path('purchases/<int:pk>/', views.purchase_detail, name='purchase_detail'),
path('purchases/payment/<int:pk>/', views.add_purchase_payment, name='add_purchase_payment'),

View File

@ -23,7 +23,7 @@ from .models import ( Expense, ExpenseCategory,
SaleItem, SalePayment, SystemSetting,
Quotation, QuotationItem,
SaleReturn, SaleReturnItem, PurchaseReturn, PurchaseReturnItem,
PaymentMethod, HeldSale, LoyaltyTier, LoyaltyTransaction, LOP, LOPItem
PaymentMethod, HeldSale, LoyaltyTier, LoyaltyTransaction
, Device)
import json
from datetime import timedelta
@ -2524,121 +2524,3 @@ def customer_display(request):
"""
settings = SystemSetting.objects.first()
return render(request, 'core/customer_display.html', {'settings': settings})
# --- LOP Views ---
@login_required
def lops(request):
lops_qs = LOP.objects.all().select_related('supplier', 'created_by').order_by('-created_at')
paginator = Paginator(lops_qs, 25)
page_number = request.GET.get('page')
lops_list = paginator.get_page(page_number)
suppliers = Supplier.objects.all().order_by('name')
return render(request, 'core/lops.html', {'lops': lops_list, 'suppliers': suppliers})
@login_required
def lop_create(request):
products = Product.objects.filter(is_active=True)
suppliers = Supplier.objects.all()
return render(request, 'core/lop_create.html', {
'products': products,
'suppliers': suppliers
})
@login_required
def lop_detail(request, pk):
lop = get_object_or_404(LOP, pk=pk)
settings = SystemSetting.objects.first()
return render(request, 'core/lop_detail.html', {
'lop': lop,
'settings': settings,
'amount_in_words': number_to_words_en(lop.total_amount)
})
@csrf_exempt
@login_required
def create_lop_api(request):
if request.method == 'POST':
try:
data = json.loads(request.body)
supplier_id = data.get('supplier_id')
lop_number = data.get('lop_number', '')
items = data.get('items', [])
total_amount = data.get('total_amount', 0)
notes = data.get('notes', '')
supplier = None
if supplier_id:
supplier = Supplier.objects.get(id=supplier_id)
lop = LOP.objects.create(
supplier=supplier,
lop_number=lop_number,
total_amount=total_amount,
notes=notes,
created_by=request.user
)
for item in items:
product = Product.objects.get(id=item['id'])
LOPItem.objects.create(
lop=lop,
product=product,
quantity=item['quantity'],
cost_price=item['price'],
line_total=item['line_total']
)
return JsonResponse({'success': True, 'lop_id': lop.id})
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=400)
return JsonResponse({'success': False, 'error': 'Invalid request'}, status=405)
@login_required
def convert_lop_to_purchase(request, pk):
lop = get_object_or_404(LOP, pk=pk)
if lop.status == 'converted':
messages.warning(request, _("This LOP has already been converted to a Purchase."))
return redirect('lops')
# Create Purchase from LOP
purchase = Purchase.objects.create(
supplier=lop.supplier,
lop=lop,
total_amount=lop.total_amount,
balance_due=lop.total_amount,
payment_type='credit', # Default to credit, user can pay later
status='unpaid',
notes=lop.notes,
created_by=request.user
)
# Create PurchaseItems and Update Stock
for item in lop.items.all():
PurchaseItem.objects.create(
purchase=purchase,
product=item.product,
quantity=item.quantity,
cost_price=item.cost_price,
line_total=item.line_total
)
# Add to Stock
item.product.stock_quantity += item.quantity
item.product.save()
# Update LOP Status
lop.status = 'converted'
lop.save()
messages.success(request, _("LOP converted to Purchase Invoice successfully!"))
return redirect('purchase_detail', pk=purchase.pk)
@login_required
def delete_lop(request, pk):
lop = get_object_or_404(LOP, pk=pk)
if lop.status == 'converted':
messages.error(request, _("Cannot delete a converted LOP."))
return redirect('lops')
lop.delete()
messages.success(request, _("LOP deleted successfully!"))
return redirect('lops')

View File

@ -6,11 +6,6 @@ import pymysql
pymysql.install_as_MySQLdb()
# Monkey-patch MySQLdb version to satisfy Django's requirements
import MySQLdb
if not hasattr(MySQLdb, 'version_info'):
MySQLdb.version_info = (1, 4, 6, 'final', 0)
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')

View File

@ -1,5 +1,5 @@
Django>=5.0,<6.0
Django==5.2.7
pymysql
python-dotenv
# pyzk <-- Commented out: Likely incompatible with Python 3.14 on Windows
requests
python-dotenv==1.1.1
pyzk==0.9
requests==2.32.3