diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 5a870f5..7a79afc 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 783a3e3..040e7fa 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 4ecbf23..21ff9b5 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 f1c90d1..1ac853d 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 5466d1b..a255268 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,15 +1,19 @@ from django.contrib import admin -from .models import Category, Product, Customer, Supplier, Sale, SaleItem, Purchase +from .models import Category, Unit, Product, Customer, Supplier, Sale, SaleItem, Purchase, SystemSetting @admin.register(Category) class CategoryAdmin(admin.ModelAdmin): list_display = ('name_en', 'name_ar', 'slug') prepopulated_fields = {'slug': ('name_en',)} +@admin.register(Unit) +class UnitAdmin(admin.ModelAdmin): + list_display = ('name_en', 'name_ar', 'short_name') + @admin.register(Product) class ProductAdmin(admin.ModelAdmin): - list_display = ('name_en', 'name_ar', 'sku', 'price', 'stock_quantity', 'category') - list_filter = ('category',) + list_display = ('name_en', 'name_ar', 'sku', 'price', 'stock_quantity', 'category', 'unit') + list_filter = ('category', 'unit') search_fields = ('name_en', 'name_ar', 'sku') @admin.register(Customer) @@ -34,3 +38,7 @@ class SaleAdmin(admin.ModelAdmin): class PurchaseAdmin(admin.ModelAdmin): list_display = ('id', 'supplier', 'total_amount', 'created_at') list_filter = ('supplier', 'created_at') + +@admin.register(SystemSetting) +class SystemSettingAdmin(admin.ModelAdmin): + list_display = ('business_name', 'phone', 'email', 'vat_number') \ No newline at end of file diff --git a/core/migrations/0003_remove_systemsetting_logo_url_systemsetting_logo_and_more.py b/core/migrations/0003_remove_systemsetting_logo_url_systemsetting_logo_and_more.py new file mode 100644 index 0000000..d92d685 --- /dev/null +++ b/core/migrations/0003_remove_systemsetting_logo_url_systemsetting_logo_and_more.py @@ -0,0 +1,67 @@ +# Generated by Django 5.2.7 on 2026-02-02 07:45 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0002_systemsetting'), + ] + + operations = [ + migrations.RemoveField( + model_name='systemsetting', + name='logo_url', + ), + migrations.AddField( + model_name='systemsetting', + name='logo', + field=models.ImageField(blank=True, null=True, upload_to='business_logos/', verbose_name='Logo'), + ), + migrations.AddField( + model_name='systemsetting', + name='registration_number', + field=models.CharField(blank=True, max_length=50, verbose_name='Registration Number'), + ), + migrations.AddField( + model_name='systemsetting', + name='vat_number', + field=models.CharField(blank=True, max_length=50, verbose_name='VAT Number'), + ), + migrations.AlterField( + model_name='product', + name='price', + field=models.DecimalField(decimal_places=3, max_digits=12, verbose_name='Price'), + ), + migrations.AlterField( + model_name='purchase', + name='total_amount', + field=models.DecimalField(decimal_places=3, max_digits=15), + ), + migrations.AlterField( + model_name='sale', + name='discount', + field=models.DecimalField(decimal_places=3, default=0, max_digits=15), + ), + migrations.AlterField( + model_name='sale', + name='total_amount', + field=models.DecimalField(decimal_places=3, max_digits=15), + ), + migrations.AlterField( + model_name='saleitem', + name='line_total', + field=models.DecimalField(decimal_places=3, max_digits=15), + ), + migrations.AlterField( + model_name='saleitem', + name='unit_price', + field=models.DecimalField(decimal_places=3, max_digits=12), + ), + migrations.AlterField( + model_name='systemsetting', + name='currency_symbol', + field=models.CharField(default='OMR', max_length=10, verbose_name='Currency Symbol'), + ), + ] diff --git a/core/migrations/0004_unit_product_unit.py b/core/migrations/0004_unit_product_unit.py new file mode 100644 index 0000000..91f3689 --- /dev/null +++ b/core/migrations/0004_unit_product_unit.py @@ -0,0 +1,28 @@ +# Generated by Django 5.2.7 on 2026-02-02 08:00 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0003_remove_systemsetting_logo_url_systemsetting_logo_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='Unit', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name_en', models.CharField(max_length=50, verbose_name='Name (English)')), + ('name_ar', models.CharField(max_length=50, verbose_name='Name (Arabic)')), + ('short_name', models.CharField(max_length=10, verbose_name='Short Name')), + ], + ), + migrations.AddField( + model_name='product', + name='unit', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='products', to='core.unit'), + ), + ] diff --git a/core/migrations/0005_product_cost_price_product_is_active_and_more.py b/core/migrations/0005_product_cost_price_product_is_active_and_more.py new file mode 100644 index 0000000..51d89cb --- /dev/null +++ b/core/migrations/0005_product_cost_price_product_is_active_and_more.py @@ -0,0 +1,59 @@ +# Generated by Django 5.2.7 on 2026-02-02 08:19 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0004_unit_product_unit'), + ] + + operations = [ + migrations.AddField( + model_name='product', + name='cost_price', + field=models.DecimalField(decimal_places=3, default=0, max_digits=12, verbose_name='Cost Price'), + ), + migrations.AddField( + model_name='product', + name='is_active', + field=models.BooleanField(default=True, verbose_name='Active'), + ), + migrations.AddField( + model_name='product', + name='opening_stock', + field=models.PositiveIntegerField(default=0, verbose_name='Opening Stock'), + ), + migrations.AddField( + model_name='product', + name='supplier', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='products', to='core.supplier'), + ), + migrations.AddField( + model_name='product', + name='vat', + field=models.DecimalField(decimal_places=2, default=0, max_digits=5, verbose_name='VAT (%)'), + ), + migrations.AlterField( + model_name='product', + name='image', + field=models.ImageField(blank=True, null=True, upload_to='product_images/', verbose_name='Product Image'), + ), + migrations.AlterField( + model_name='product', + name='price', + field=models.DecimalField(decimal_places=3, max_digits=12, verbose_name='Sale Price'), + ), + migrations.AlterField( + model_name='product', + name='sku', + field=models.CharField(max_length=50, unique=True, verbose_name='Barcode/SKU'), + ), + migrations.AlterField( + model_name='product', + name='stock_quantity', + field=models.PositiveIntegerField(default=0, verbose_name='In Stock'), + ), + ] diff --git a/core/migrations/0006_purchase_balance_due_purchase_due_date_and_more.py b/core/migrations/0006_purchase_balance_due_purchase_due_date_and_more.py new file mode 100644 index 0000000..ec823ad --- /dev/null +++ b/core/migrations/0006_purchase_balance_due_purchase_due_date_and_more.py @@ -0,0 +1,82 @@ +# Generated by Django 5.2.7 on 2026-02-02 08:35 + +import django.db.models.deletion +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0005_product_cost_price_product_is_active_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='purchase', + name='balance_due', + field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Balance Due'), + ), + migrations.AddField( + model_name='purchase', + name='due_date', + field=models.DateField(blank=True, null=True, verbose_name='Due Date'), + ), + migrations.AddField( + model_name='purchase', + name='invoice_number', + field=models.CharField(blank=True, max_length=50, verbose_name='Invoice Number'), + ), + migrations.AddField( + model_name='purchase', + name='notes', + field=models.TextField(blank=True, verbose_name='Notes'), + ), + migrations.AddField( + model_name='purchase', + name='paid_amount', + field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Paid Amount'), + ), + migrations.AddField( + model_name='purchase', + name='payment_type', + field=models.CharField(choices=[('cash', 'Cash'), ('credit', 'Credit'), ('partial', 'Partial')], default='cash', max_length=20, verbose_name='Payment Type'), + ), + migrations.AddField( + model_name='purchase', + name='status', + field=models.CharField(choices=[('paid', 'Paid'), ('partial', 'Partial'), ('unpaid', 'Unpaid')], default='paid', max_length=20, verbose_name='Status'), + ), + migrations.AlterField( + model_name='purchase', + name='supplier', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='purchases', to='core.supplier'), + ), + migrations.AlterField( + model_name='purchase', + name='total_amount', + field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Total Amount'), + ), + migrations.CreateModel( + name='PurchaseItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(verbose_name='Quantity')), + ('cost_price', models.DecimalField(decimal_places=3, max_digits=12, verbose_name='Cost Price')), + ('line_total', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Line Total')), + ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.product')), + ('purchase', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.purchase')), + ], + ), + migrations.CreateModel( + name='PurchasePayment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Amount')), + ('payment_date', models.DateField(default=django.utils.timezone.now, verbose_name='Payment Date')), + ('payment_method', models.CharField(default='Cash', max_length=50, verbose_name='Payment Method')), + ('notes', models.TextField(blank=True, verbose_name='Notes')), + ('purchase', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='core.purchase')), + ], + ), + ] diff --git a/core/migrations/0007_sale_balance_due_sale_due_date_sale_invoice_number_and_more.py b/core/migrations/0007_sale_balance_due_sale_due_date_sale_invoice_number_and_more.py new file mode 100644 index 0000000..7595004 --- /dev/null +++ b/core/migrations/0007_sale_balance_due_sale_due_date_sale_invoice_number_and_more.py @@ -0,0 +1,91 @@ +# Generated by Django 5.2.7 on 2026-02-02 09:25 + +import django.db.models.deletion +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0006_purchase_balance_due_purchase_due_date_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='sale', + name='balance_due', + field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Balance Due'), + ), + migrations.AddField( + model_name='sale', + name='due_date', + field=models.DateField(blank=True, null=True, verbose_name='Due Date'), + ), + migrations.AddField( + model_name='sale', + name='invoice_number', + field=models.CharField(blank=True, max_length=50, verbose_name='Invoice Number'), + ), + migrations.AddField( + model_name='sale', + name='notes', + field=models.TextField(blank=True, verbose_name='Notes'), + ), + migrations.AddField( + model_name='sale', + name='paid_amount', + field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Paid Amount'), + ), + migrations.AddField( + model_name='sale', + name='payment_type', + field=models.CharField(choices=[('cash', 'Cash'), ('credit', 'Credit'), ('partial', 'Partial')], default='cash', max_length=20, verbose_name='Payment Type'), + ), + migrations.AddField( + model_name='sale', + name='status', + field=models.CharField(choices=[('paid', 'Paid'), ('partial', 'Partial'), ('unpaid', 'Unpaid')], default='paid', max_length=20, verbose_name='Status'), + ), + migrations.AlterField( + model_name='sale', + name='customer', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sales', to='core.customer'), + ), + migrations.AlterField( + model_name='sale', + name='discount', + field=models.DecimalField(decimal_places=3, default=0, max_digits=15, verbose_name='Discount'), + ), + migrations.AlterField( + model_name='sale', + name='total_amount', + field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Total Amount'), + ), + migrations.AlterField( + model_name='saleitem', + name='line_total', + field=models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Line Total'), + ), + migrations.AlterField( + model_name='saleitem', + name='quantity', + field=models.PositiveIntegerField(verbose_name='Quantity'), + ), + migrations.AlterField( + model_name='saleitem', + name='unit_price', + field=models.DecimalField(decimal_places=3, max_digits=12, verbose_name='Unit Price'), + ), + migrations.CreateModel( + name='SalePayment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.DecimalField(decimal_places=3, max_digits=15, verbose_name='Amount')), + ('payment_date', models.DateField(default=django.utils.timezone.now, verbose_name='Payment Date')), + ('payment_method', models.CharField(default='Cash', max_length=50, verbose_name='Payment Method')), + ('notes', models.TextField(blank=True, verbose_name='Notes')), + ('sale', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='core.sale')), + ], + ), + ] diff --git a/core/migrations/__pycache__/0003_remove_systemsetting_logo_url_systemsetting_logo_and_more.cpython-311.pyc b/core/migrations/__pycache__/0003_remove_systemsetting_logo_url_systemsetting_logo_and_more.cpython-311.pyc new file mode 100644 index 0000000..afbf149 Binary files /dev/null and b/core/migrations/__pycache__/0003_remove_systemsetting_logo_url_systemsetting_logo_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0004_unit_product_unit.cpython-311.pyc b/core/migrations/__pycache__/0004_unit_product_unit.cpython-311.pyc new file mode 100644 index 0000000..b80601f Binary files /dev/null and b/core/migrations/__pycache__/0004_unit_product_unit.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0005_product_cost_price_product_is_active_and_more.cpython-311.pyc b/core/migrations/__pycache__/0005_product_cost_price_product_is_active_and_more.cpython-311.pyc new file mode 100644 index 0000000..30e4dc4 Binary files /dev/null and b/core/migrations/__pycache__/0005_product_cost_price_product_is_active_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0006_purchase_balance_due_purchase_due_date_and_more.cpython-311.pyc b/core/migrations/__pycache__/0006_purchase_balance_due_purchase_due_date_and_more.cpython-311.pyc new file mode 100644 index 0000000..f09ea95 Binary files /dev/null and b/core/migrations/__pycache__/0006_purchase_balance_due_purchase_due_date_and_more.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0007_sale_balance_due_sale_due_date_sale_invoice_number_and_more.cpython-311.pyc b/core/migrations/__pycache__/0007_sale_balance_due_sale_due_date_sale_invoice_number_and_more.cpython-311.pyc new file mode 100644 index 0000000..f1d228d Binary files /dev/null and b/core/migrations/__pycache__/0007_sale_balance_due_sale_due_date_sale_invoice_number_and_more.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 5568852..4569aea 100644 --- a/core/models.py +++ b/core/models.py @@ -1,5 +1,6 @@ from django.db import models from django.utils.translation import gettext_lazy as _ +from django.utils import timezone class Category(models.Model): name_en = models.CharField(_("Name (English)"), max_length=100) @@ -12,15 +13,29 @@ class Category(models.Model): def __str__(self): return f"{self.name_en} / {self.name_ar}" +class Unit(models.Model): + name_en = models.CharField(_("Name (English)"), max_length=50) + name_ar = models.CharField(_("Name (Arabic)"), max_length=50) + short_name = models.CharField(_("Short Name"), max_length=10) + + def __str__(self): + return f"{self.name_en} / {self.name_ar}" + class Product(models.Model): category = models.ForeignKey(Category, on_delete=models.CASCADE, related_name="products") + unit = models.ForeignKey(Unit, on_delete=models.SET_NULL, null=True, blank=True, related_name="products") + supplier = models.ForeignKey('Supplier', on_delete=models.SET_NULL, null=True, blank=True, related_name="products") name_en = models.CharField(_("Name (English)"), max_length=200) name_ar = models.CharField(_("Name (Arabic)"), max_length=200) - sku = models.CharField(_("SKU"), max_length=50, unique=True) + sku = models.CharField(_("Barcode/SKU"), max_length=50, unique=True) description = models.TextField(_("Description"), blank=True) - price = models.DecimalField(_("Price"), max_digits=10, decimal_places=2) - stock_quantity = models.PositiveIntegerField(_("Stock Quantity"), default=0) - image = models.URLField(_("Product Image"), blank=True, null=True) + cost_price = models.DecimalField(_("Cost Price"), max_digits=12, decimal_places=3, default=0) + price = models.DecimalField(_("Sale Price"), max_digits=12, decimal_places=3) + vat = models.DecimalField(_("VAT (%)"), max_digits=5, decimal_places=2, default=0) + opening_stock = models.PositiveIntegerField(_("Opening Stock"), default=0) + stock_quantity = models.PositiveIntegerField(_("In Stock"), default=0) + image = models.ImageField(_("Product Image"), upload_to="product_images/", blank=True, null=True) + is_active = models.BooleanField(_("Active"), default=True) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): @@ -44,34 +59,132 @@ class Supplier(models.Model): return self.name class Sale(models.Model): - customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True) - total_amount = models.DecimalField(max_digits=12, decimal_places=2) - discount = models.DecimalField(max_digits=12, decimal_places=2, default=0) + PAYMENT_TYPE_CHOICES = [ + ('cash', _('Cash')), + ('credit', _('Credit')), + ('partial', _('Partial')), + ] + STATUS_CHOICES = [ + ('paid', _('Paid')), + ('partial', _('Partial')), + ('unpaid', _('Unpaid')), + ] + + customer = models.ForeignKey(Customer, on_delete=models.SET_NULL, null=True, blank=True, related_name="sales") + invoice_number = models.CharField(_("Invoice Number"), max_length=50, blank=True) + total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3) + paid_amount = models.DecimalField(_("Paid Amount"), max_digits=15, decimal_places=3, default=0) + balance_due = models.DecimalField(_("Balance Due"), max_digits=15, decimal_places=3, default=0) + discount = models.DecimalField(_("Discount"), max_digits=15, decimal_places=3, default=0) + payment_type = models.CharField(_("Payment Type"), max_length=20, choices=PAYMENT_TYPE_CHOICES, default='cash') + status = models.CharField(_("Status"), max_length=20, choices=STATUS_CHOICES, default='paid') + due_date = models.DateField(_("Due Date"), null=True, blank=True) + notes = models.TextField(_("Notes"), blank=True) created_at = models.DateTimeField(auto_now_add=True) def __str__(self): - return f"Sale #{self.id} - {self.total_amount}" + return f"Sale #{self.id} - {self.customer.name if self.customer else 'Guest'}" + + def update_balance(self): + payments_total = self.payments.aggregate(total=models.Sum('amount'))['total'] or 0 + self.paid_amount = payments_total + self.balance_due = self.total_amount - self.paid_amount + if self.balance_due <= 0: + self.status = 'paid' + elif self.paid_amount > 0: + self.status = 'partial' + else: + self.status = 'unpaid' + self.save() class SaleItem(models.Model): sale = models.ForeignKey(Sale, on_delete=models.CASCADE, related_name="items") product = models.ForeignKey(Product, on_delete=models.CASCADE) - quantity = models.PositiveIntegerField() - unit_price = models.DecimalField(max_digits=10, decimal_places=2) - line_total = models.DecimalField(max_digits=12, decimal_places=2) + quantity = models.PositiveIntegerField(_("Quantity")) + unit_price = models.DecimalField(_("Unit Price"), max_digits=12, decimal_places=3) + 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 SalePayment(models.Model): + sale = models.ForeignKey(Sale, on_delete=models.CASCADE, related_name="payments") + amount = models.DecimalField(_("Amount"), max_digits=15, decimal_places=3) + payment_date = models.DateField(_("Payment Date"), default=timezone.now) + payment_method = models.CharField(_("Payment Method"), max_length=50, default="Cash") + notes = models.TextField(_("Notes"), blank=True) + + def __str__(self): + return f"Payment of {self.amount} for Sale #{self.sale.id}" class Purchase(models.Model): - supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True) - total_amount = models.DecimalField(max_digits=12, decimal_places=2) + PAYMENT_TYPE_CHOICES = [ + ('cash', _('Cash')), + ('credit', _('Credit')), + ('partial', _('Partial')), + ] + STATUS_CHOICES = [ + ('paid', _('Paid')), + ('partial', _('Partial')), + ('unpaid', _('Unpaid')), + ] + + supplier = models.ForeignKey(Supplier, on_delete=models.SET_NULL, null=True, related_name="purchases") + invoice_number = models.CharField(_("Invoice Number"), max_length=50, blank=True) + total_amount = models.DecimalField(_("Total Amount"), max_digits=15, decimal_places=3) + paid_amount = models.DecimalField(_("Paid Amount"), max_digits=15, decimal_places=3, default=0) + balance_due = models.DecimalField(_("Balance Due"), max_digits=15, decimal_places=3, default=0) + payment_type = models.CharField(_("Payment Type"), max_length=20, choices=PAYMENT_TYPE_CHOICES, default='cash') + status = models.CharField(_("Status"), max_length=20, choices=STATUS_CHOICES, default='paid') + due_date = models.DateField(_("Due Date"), null=True, blank=True) + notes = models.TextField(_("Notes"), blank=True) created_at = models.DateTimeField(auto_now_add=True) + def __str__(self): + return f"Purchase #{self.id} - {self.supplier.name if self.supplier else 'N/A'}" + + def update_balance(self): + payments_total = self.payments.aggregate(total=models.Sum('amount'))['total'] or 0 + self.paid_amount = payments_total + self.balance_due = self.total_amount - self.paid_amount + if self.balance_due <= 0: + self.status = 'paid' + elif self.paid_amount > 0: + self.status = 'partial' + else: + self.status = 'unpaid' + self.save() + +class PurchaseItem(models.Model): + purchase = models.ForeignKey(Purchase, on_delete=models.CASCADE, related_name="items") + product = models.ForeignKey(Product, on_delete=models.CASCADE) + quantity = models.PositiveIntegerField(_("Quantity")) + cost_price = models.DecimalField(_("Cost Price"), max_digits=12, decimal_places=3) + 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 PurchasePayment(models.Model): + purchase = models.ForeignKey(Purchase, on_delete=models.CASCADE, related_name="payments") + amount = models.DecimalField(_("Amount"), max_digits=15, decimal_places=3) + payment_date = models.DateField(_("Payment Date"), default=timezone.now) + payment_method = models.CharField(_("Payment Method"), max_length=50, default="Cash") + notes = models.TextField(_("Notes"), blank=True) + + def __str__(self): + return f"Payment of {self.amount} for Purchase #{self.purchase.id}" + 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="$") + 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) - logo_url = models.URLField(_("Logo URL"), blank=True, null=True) + 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) def __str__(self): return self.business_name diff --git a/core/templates/base.html b/core/templates/base.html index 25a3a05..0602d98 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -32,8 +32,8 @@