diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index b46e844..05a5292 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 64ce50c..43a98ce 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index 5d50632..780774b 100644 --- a/config/settings.py +++ b/config/settings.py @@ -61,7 +61,7 @@ INSTALLED_APPS = [ 'django.contrib.staticfiles', 'core', 'accounting', - 'hr', + # 'hr', ] MIDDLEWARE = [ diff --git a/config/urls.py b/config/urls.py index aebc6d8..d2136e6 100644 --- a/config/urls.py +++ b/config/urls.py @@ -9,10 +9,10 @@ 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: urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets") urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) - urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) \ No newline at end of file diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 6f485b3..2195584 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 8c5c3aa..48da3d8 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index f7d89ae..5f36b04 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 41dcb05..efce641 100644 --- a/core/models.py +++ b/core/models.py @@ -255,6 +255,9 @@ class Purchase(models.Model): notes = models.TextField(_("Notes"), blank=True) 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'}" @@ -304,7 +307,7 @@ class SaleReturn(models.Model): created_at = models.DateTimeField(auto_now_add=True) def __str__(self): - return f"Sale Return #{self.id} - {self.customer.name if self.customer else 'Guest'}" + return f"Return #{self.id} for Sale #{self.sale.id if self.sale else 'N/A'}" class SaleReturnItem(models.Model): sale_return = models.ForeignKey(SaleReturn, on_delete=models.CASCADE, related_name="items") @@ -326,72 +329,74 @@ class PurchaseReturn(models.Model): created_at = models.DateTimeField(auto_now_add=True) def __str__(self): - return f"Purchase Return #{self.id} - {self.supplier.name if self.supplier else 'N/A'}" + return f"Return #{self.id} for Purchase #{self.purchase.id if self.purchase 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.created_at.strftime('%Y-%m-%d %H:%M')}" + return f"Held Sale {self.id} - {self.customer.name if self.customer else 'Guest'}" -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) +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') def __str__(self): - return self.business_name - + return self.user.username 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') @@ -402,24 +407,33 @@ class Device(models.Model): created_at = models.DateTimeField(auto_now_add=True) def __str__(self): - return f"{self.name} ({self.get_device_type_display()})" + return self.name -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) +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) def __str__(self): - return self.user.username + return f"LOP #{self.id} - {self.supplier.name if self.supplier else 'N/A'}" -@receiver(post_save, sender=User) -def create_user_profile(sender, instance, created, **kwargs): - if created: - UserProfile.objects.create(user=instance) +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 save_user_profile(sender, instance, **kwargs): - if not hasattr(instance, 'profile'): - UserProfile.objects.create(user=instance) - instance.profile.save() \ No newline at end of file + def __str__(self): + return f"{self.product.name_en} x {self.quantity}" \ No newline at end of file diff --git a/core/templates/base.html b/core/templates/base.html index ab40dcd..6ea5e87 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -88,11 +88,16 @@
| {% trans "Product" %} | +{% trans "Cost Price" %} | +{% trans "Quantity" %} | +{% trans "Total" %} | ++ |
|---|---|---|---|---|
|
+ [[ item.name_en ]]
+ [[ item.sku ]]
+ |
+ + + | ++ + | +[[ currencySymbol ]][[ (item.price * item.quantity).toFixed(decimalPlaces) ]] | ++ + | +
| + {% trans "No items added yet." %} + | +||||
{{ lop.created_at|date:"F j, Y" }}
+{{ site_settings.address }} + {{ site_settings.phone }} + {{ site_settings.email }}
+{% trans "LOP #" %}: {{ lop.lop_number|default:lop.id }}
+{% trans "Date" %}: {{ lop.created_at|date:"Y-m-d" }}
+{% trans "Status" %}: + {% if lop.status == 'converted' %}{% trans "Converted" %} + {% elif lop.status == 'cancelled' %}{% trans "Cancelled" %} + {% else %}{% trans "Draft" %}{% endif %} +
+{{ lop.supplier.phone }}
+| {% trans "Description" %} | +{% trans "Quantity" %} | +{% trans "Unit Cost" %} | +{% trans "Total" %} | +
|---|---|---|---|
|
+ {{ item.product.name_en }}
+ {{ item.product.name_ar }}
+ |
+ {{ item.quantity }} | +{{ site_settings.currency_symbol }}{{ item.cost_price|floatformat:3 }} | +{{ site_settings.currency_symbol }}{{ item.line_total|floatformat:3 }} | +
{{ amount_in_words }}
+ {% endif %} +{{ lop.notes }}
+{% trans "Manage local purchase orders and convert them to invoices" %}
+| {% trans "LOP #" %} | +{% trans "Date" %} | +{% trans "Supplier" %} | +{% trans "Total" %} | +{% trans "User" %} | +{% trans "Status" %} | +{% trans "Actions" %} | +
|---|---|---|---|---|---|---|
| + + {{ lop.lop_number|default:lop.id }} + + | +{{ lop.created_at|date:"Y-m-d" }} | +{{ lop.supplier.name|default:"-" }} | +{{ site_settings.currency_symbol }}{{ lop.total_amount|floatformat:3 }} | ++ + {{ lop.created_by.username|default:"System" }} + + | ++ {% if lop.status == 'converted' %} + {% trans "Converted" %} + {% elif lop.status == 'cancelled' %} + {% trans "Cancelled" %} + {% else %} + {% trans "Draft" %} + {% endif %} + | +
+
+
+
+
+ {% if lop.status == 'draft' %}
+
+
+
+ {% endif %}
+ {% if lop.status != 'converted' %}
+
+ {% endif %}
+
+
+
+
+
+
+
+
+ |
+
|
+ {% trans "No LOPs recorded yet." %} + |
+ ||||||