diff --git a/inventory/signals.py b/inventory/signals.py index 92c117b3..8ada6434 100644 --- a/inventory/signals.py +++ b/inventory/signals.py @@ -73,23 +73,6 @@ def create_car_location(sender, instance, created, **kwargs): except Exception as e: print(f"Failed to create CarLocation for car {instance.vin}: {e}") - -# @receiver(post_save, sender=models.CarReservation) -# def update_car_status_on_reservation(sender, instance, created, **kwargs): -# if created: -# car = instance.car -# car.status = models.CarStatusChoices.RESERVED -# car.save() - - -# @receiver(post_delete, sender=models.CarReservation) -# def update_car_status_on_reservation_delete(sender, instance, **kwargs): -# car = instance.car -# if not car.is_reserved(): -# car.status = models.CarStatusChoices.AVAILABLE -# car.save() - - # Create Entity @receiver(post_save, sender=models.Dealer) def create_ledger_entity(sender, instance, created, **kwargs): @@ -108,16 +91,13 @@ def create_ledger_entity(sender, instance, created, **kwargs): coa = entity.create_chart_of_accounts( assign_as_default=True, commit=True, coa_name=_(f"{entity_name}-COA") ) - if coa: - # entity.populate_default_coa(activate_accounts=True, coa_model=coa) - print(f"Ledger entity created for Dealer: {instance.name}") - + if coa: # Create unit of measures entity.create_uom(name="Unit", unit_abbr="unit") for u in models.UnitOfMeasure.choices: entity.create_uom(name=u[1], unit_abbr=u[0]) - # Create Cash Account + # Cash Account asset_ca_cash = entity.create_account( coa_model=coa, code="1010", @@ -128,7 +108,8 @@ def create_ledger_entity(sender, instance, created, **kwargs): ) asset_ca_cash.role_default = True asset_ca_cash.save() - # Create Accounts Receivable Account + + # Accounts Receivable Account asset_ca_receivables = entity.create_account( coa_model=coa, code="1102", @@ -140,7 +121,7 @@ def create_ledger_entity(sender, instance, created, **kwargs): asset_ca_receivables.role_default = True asset_ca_receivables.save() - # Create Inventory Account + # Inventory Account asset_ca_inventory = entity.create_account( coa_model=coa, code="1103", @@ -149,11 +130,71 @@ def create_ledger_entity(sender, instance, created, **kwargs): balance_type="debit", active=True, ) - asset_ca_inventory.role_default = True asset_ca_inventory.save() - # Create Accounts Payable Account - asset_ca_accounts_payable = entity.create_account( + + # Prepaid Expenses Account + asset_ca_prepaid = entity.create_account( + coa_model=coa, + code="1104", + role=roles.ASSET_CA_PREPAID, + name=_("Prepaid Expenses"), + balance_type="debit", + active=True, + ) + asset_ca_prepaid.role_default = True + asset_ca_prepaid.save() + + # Notes Receivable Account + asset_lti_notes_receivable = entity.create_account( + coa_model=coa, + code="1201", + role=roles.ASSET_LTI_NOTES_RECEIVABLE, + name=_("Notes Receivable"), + balance_type="debit", + active=True, + ) + asset_lti_notes_receivable.role_default = True + asset_lti_notes_receivable.save() + + # Land Account + asset_lti_land = entity.create_account( + coa_model=coa, + code="1202", + role=roles.ASSET_LTI_LAND, + name=_("Land"), + balance_type="debit", + active=True, + ) + asset_lti_land.role_default = True + asset_lti_land.save() + + # Buildings Account + asset_ppe_buildings = entity.create_account( + coa_model=coa, + code="1301", + role=roles.ASSET_PPE_BUILDINGS, + name=_("Buildings"), + balance_type="debit", + active=True, + ) + asset_ppe_buildings.role_default = True + asset_ppe_buildings.save() + + # Buildings Accumulated Depreciation Account + asset_ppe_buildings_accum_depreciation = entity.create_account( + coa_model=coa, + code="1302", + role=roles.ASSET_PPE_BUILDINGS_ACCUM_DEPRECIATION, + name=_("Buildings - Accum. Depreciation"), + balance_type="credit", + active=True, + ) + asset_ppe_buildings_accum_depreciation.role_default = True + asset_ppe_buildings_accum_depreciation.save() + + # Accounts Payable Account + liability_cl_acc_payable = entity.create_account( coa_model=coa, code="2101", role=roles.LIABILITY_CL_ACC_PAYABLE, @@ -161,33 +202,83 @@ def create_ledger_entity(sender, instance, created, **kwargs): balance_type="credit", active=True, ) + liability_cl_acc_payable.role_default = True + liability_cl_acc_payable.save() - # add Bank - - # asset_ca_accounts_payable = entity.create_account( - # coa_model=coa, - # code="2101", - # role=roles.LIABILITY_CL_ACC_PAYABLE, - # name=_("Accounts Payable"), - # balance_type="credit", - # active=True, - # ) - asset_ca_accounts_payable.role_default = True - asset_ca_accounts_payable.save() - # Create Equity Account - asset_ca_equity = entity.create_account( + # Deferred Revenue Account + liability_cl_def_rev = entity.create_account( coa_model=coa, - code="3101", - role=roles.EQUITY_CAPITAL, - name=_("Partners Current"), + code="2103", + role=roles.LIABILITY_CL_DEFERRED_REVENUE, + name=_("Deferred Revenue"), balance_type="credit", active=True, ) - asset_ca_equity.role_default = True - asset_ca_equity.save() + liability_cl_def_rev.role_default = True + liability_cl_def_rev.save() - # Create Sales Revenue Account - asset_ca_revenue = entity.create_account( + # Wages Payable Account + liability_cl_wages_payable = entity.create_account( + coa_model=coa, + code="2102", + role=roles.LIABILITY_CL_WAGES_PAYABLE, + name=_("Wages Payable"), + balance_type="credit", + active=True, + ) + liability_cl_wages_payable.role_default = True + liability_cl_wages_payable.save() + + # Long-Term Notes Payable Account + liability_ltl_notes_payable = entity.create_account( + coa_model=coa, + code="2201", + role=roles.LIABILITY_LTL_NOTES_PAYABLE, + name=_("Long-Term Notes Payable"), + balance_type="credit", + active=True, + ) + liability_ltl_notes_payable.role_default = True + liability_ltl_notes_payable.save() + + # Mortgage Payable Account + liability_ltl_mortgage_payable = entity.create_account( + coa_model=coa, + code="2202", + role=roles.LIABILITY_LTL_MORTGAGE_PAYABLE, + name=_("Mortgage Payable"), + balance_type="credit", + active=True, + ) + liability_ltl_mortgage_payable.role_default = True + liability_ltl_mortgage_payable.save() + + # Common Stock Account + equity_common_stock = entity.create_account( + coa_model=coa, + code="3101", + role=roles.EQUITY_COMMON_STOCK, + name=_("Common Stock"), + balance_type="credit", + active=True, + ) + equity_common_stock.role_default = True + equity_common_stock.save() + + # Retained Earnings Account + equity_retained_earnings = entity.create_account( + coa_model=coa, + code="3102", + role=roles.EQUITY_ADJUSTMENT, + name=_("Retained Earnings"), + balance_type="credit", + active=True, + ) + equity_retained_earnings.role_default = True + equity_retained_earnings.save() + + # Sales Revenue Account + income_operational = entity.create_account( coa_model=coa, code="4101", role=roles.INCOME_OPERATIONAL, @@ -195,11 +286,23 @@ def create_ledger_entity(sender, instance, created, **kwargs): balance_type="credit", active=True, ) - asset_ca_revenue.role_default = True - asset_ca_revenue.save() + income_operational.role_default = True + income_operational.save() - # Create Cost of Goods Sold Account - asset_ca_cogs = entity.create_account( + # Interest Income Account + income_interest = entity.create_account( + coa_model=coa, + code="4102", + role=roles.INCOME_INTEREST, + name=_("Interest Income"), + balance_type="credit", + active=True, + ) + income_interest.role_default = True + income_interest.save() + + # Cost of Goods Sold (COGS) Account + expense_cogs = entity.create_account( coa_model=coa, code="5101", role=roles.COGS, @@ -207,42 +310,20 @@ def create_ledger_entity(sender, instance, created, **kwargs): balance_type="debit", active=True, ) - asset_ca_cogs.role_default = True - asset_ca_cogs.save() + expense_cogs.role_default = True + expense_cogs.save() - # Create Rent Expense Account - expense = entity.create_account( + # Rent Expense Account + expense_rent = entity.create_account( coa_model=coa, - code="6101", + code="6102", role=roles.EXPENSE_OPERATIONAL, name=_("Rent Expense"), balance_type="debit", active=True, ) - expense.role_default = True - expense.save() - - # Create Utilities Expense Account - entity.create_account( - coa_model=coa, - code="6020", - role=roles.EXPENSE_OPERATIONAL, - name=_("Utilities Expense"), - balance_type="debit", - active=True, - ) - # Create Deferred Revenue Account - unearned_account = entity.create_account( - coa_model=coa, - code="2060", - role=roles.LIABILITY_CL_DEFERRED_REVENUE, - name=_("Deferred Revenue"), - balance_type="credit", - active=True, - ) - unearned_account.role_default = True - unearned_account.save() - + expense_rent.role_default = True + expense_rent.save() # Create Vendor @receiver(post_save, sender=models.Vendor) @@ -394,13 +475,11 @@ def log_opportunity_update(sender, instance, **kwargs): @receiver(post_save, sender=models.AdditionalServices) def create_item_service(sender, instance, created, **kwargs): - if created: - + if created: entity = instance.dealer.entity uom = entity.get_uom_all().get(unit_abbr=instance.uom) cogs = entity.get_all_accounts().get(role=roles.COGS) - # price = (float(instance.price) * float(vat.rate)) + float(instance.price) if instance.taxable else instance.price service_model = ItemModel.objects.create( name=instance.name, uom=uom, diff --git a/inventory/tests.py b/inventory/tests.py index 7ce503c2..ff0fde3d 100644 --- a/inventory/tests.py +++ b/inventory/tests.py @@ -1,3 +1,235 @@ -from django.test import TestCase +import json +from . import models as m +from django.urls import reverse +from django_ledger import models as lm +from django.test import Client, TestCase +from django.contrib.auth import get_user_model + +User = get_user_model() + # Create your tests here. +class ModelTest(TestCase): + def setUp(self): + email = "RkzgO@example.com" + name = "John Doe" + password = "password" + crn = "123456789" + vrn = "123456789" + phone = "123456789" + address = "123 Main St" + arabic_name = "الاسم بالعربية" + + self.vat = m.VatRate.objects.create(rate=0.15) + + user = User.objects.create(username=email, email=email) + user.set_password(password) + user.save() + + self.dealer = m.Dealer.objects.create( + user=user, + name=name, + arabic_name=arabic_name, + crn=crn, + vrn=vrn, + phone_number=phone, + address=address, + ) + + self.car_make = m.CarMake.objects.create(name="Make") + self.car_model = m.CarModel.objects.create( + name="Model", id_car_make=self.car_make + ) + self.car_serie = m.CarSerie.objects.create( + name="Serie", id_car_model=self.car_model + ) + self.trim = m.CarTrim.objects.create(name="Trim", id_car_serie=self.car_serie) + self.car = m.Car.objects.create( + vin="123456789", + dealer=self.dealer, + id_car_make=self.car_make, + id_car_model=self.car_model, + id_car_serie=self.car_serie, + year=2020, + id_car_trim=self.trim, + receiving_date="2020-01-01", + ) + + self.car_finances = m.CarFinance.objects.create( + car=self.car, selling_price=1000, cost_price=500, discount_amount=200 + ) + + def test_dealer_creation_creates_user_and_entity(self): + dealer = self.dealer + + self.assertEqual(User.objects.count(), 1) + self.assertEqual(m.Dealer.objects.count(), 1) + self.assertEqual(User.objects.first().username, "RkzgO@example.com") + self.assertEqual(User.objects.first().email, "RkzgO@example.com") + self.assertTrue(User.objects.first().check_password("password")) + self.assertEqual(dealer.user, User.objects.first()) + self.assertEqual(dealer.name, "John Doe") + self.assertEqual(dealer.arabic_name, "الاسم بالعربية") + self.assertEqual(dealer.crn, "123456789") + self.assertEqual(dealer.vrn, "123456789") + self.assertEqual(dealer.phone_number, "123456789") + self.assertEqual(dealer.address, "123 Main St") + + self.assertIsNotNone(dealer.entity) + self.assertEqual(dealer.entity.name, dealer.name) + + self.assertEqual(dealer.entity.get_all_accounts().count(), 19) + self.assertEqual(dealer.entity.get_uom_all().count(), 16) + + def test_car_creation_creates_product(self): + dealer = self.dealer + + self.assertEqual(m.Car.objects.count(), 1) + self.assertEqual(self.car.vin, "123456789") + self.assertEqual(self.car.dealer, dealer) + self.assertEqual(self.car.id_car_make, self.car_make) + self.assertEqual(self.car.id_car_model, self.car_model) + self.assertEqual(self.car.id_car_serie, self.car_serie) + self.assertEqual(self.car.year, 2020) + self.assertEqual(self.car.id_car_trim, self.trim) + + product = dealer.entity.get_items_all().filter(name=self.car.vin).first() + self.assertEqual(product.name, self.car.vin) + + def test_car_finances_creation(self): + self.assertEqual(m.CarFinance.objects.count(), 1) + self.assertEqual(self.car_finances.car, self.car) + self.assertEqual(self.car_finances.selling_price, 1000) + self.assertEqual(self.car_finances.cost_price, 500) + self.assertEqual(self.car_finances.discount_amount, 200) + + def test_car_finance_total(self): + self.assertEqual(m.CarFinance.objects.count(), 1) + self.assertEqual(self.car_finances.total, 1000) + self.assertEqual(self.car_finances.total_discount, 800) + self.assertEqual(self.car_finances.total_vat, 920) + + def test_car_additional_services_create_item_service(self): + m.AdditionalServices.objects.create( + name="Service", + price=100, + description="Description", + dealer=self.dealer, + taxable=True, + uom=m.UnitOfMeasure.PIECE, + ) + + self.assertEqual( + m.ItemModel.objects.filter( + name="Service", + default_amount=100, + is_product_or_service=True, + item_role="service", + ).count(), + 1, + ) + + +class AuthenticationTest(TestCase): + def setUp(self): + self.client = Client() + self.url = reverse("account_signup") + def test_login(self): + url = reverse("account_login") + response = self.client.post(url, {"email": "RkzgO@example.com", "password": "password"}) + self.assertEqual(response.status_code, 200) + + + def test_valid_data(self): + # Create valid JSON data + data = { + "wizardValidationForm1": { + "email": "test@example.com", + "password": "password123", + "confirm_password": "password123" + }, + "wizardValidationForm2": { + "name": "John Doe", + "arabic_name": "جون دو", + "phone_number": "1234567890" + }, + "wizardValidationForm3": { + "crn": "123456", + "vrn": "789012", + "address": "123 Main St" + } + } + + # Send a POST request with the JSON data + response = self.client.post( + self.url, + data=json.dumps(data), + content_type='application/json' + ) + + # Check the response + self.assertEqual(response.status_code, 200) + self.assertEqual(response.json(), {'message': 'User created successfully.'}) + + + def test_passwords_do_not_match(self): + # Create JSON data with mismatched passwords + data = { + "wizardValidationForm1": { + "email": "test@example.com", + "password": "password123", + "confirm_password": "differentpassword" + }, + "wizardValidationForm2": { + "name": "John Doe", + "arabic_name": "جون دو", + "phone_number": "1234567890" + }, + "wizardValidationForm3": { + "crn": "123456", + "vrn": "789012", + "address": "123 Main St" + } + } + + # Send a POST request with the JSON data + response = self.client.post( + self.url, + data=json.dumps(data), + content_type='application/json' + ) + + # Check the response + self.assertEqual(response.status_code, 400) + self.assertEqual(response.json(), {"error": "Passwords do not match."}) + + def test_missing_required_fields(self): + # Create JSON data with missing required fields + data = { + "wizardValidationForm1": { + "email": "test@example.com", + "password": "password123", + # Missing "confirm_password" + }, + "wizardValidationForm2": { + "name": "John Doe", + "arabic_name": "جون دو", + "phone_number": "1234567890" + }, + "wizardValidationForm3": { + "crn": "123456", + "vrn": "789012", + "address": "123 Main St" + } + } + + # Send a POST request with the JSON data + response = self.client.post( + self.url, + data=json.dumps(data), + content_type='application/json' + ) + + # Check the response + self.assertEqual(response.status_code, 400) + self.assertIn("error", response.json()) # Assuming the view returns an error for missing fields \ No newline at end of file diff --git a/inventory/urls.py b/inventory/urls.py index ef31d36b..3d083a63 100644 --- a/inventory/urls.py +++ b/inventory/urls.py @@ -144,6 +144,10 @@ urlpatterns = [ path('sales/invoices//', views.InvoiceDetailView.as_view(), name='invoice_detail'), path('sales/invoices//preview/', views.InvoicePreviewView.as_view(), name='invoice_preview'), path('sales/invoices//invoice_mark_as/', views.invoice_mark_as, name='invoice_mark_as'), + path('sales/invoices//draft_invoice_update/', views.DraftInvoiceModelUpdateFormView.as_view(), name='draft_invoice_update'), + path('sales/invoices//approved_invoice_update/', views.ApprovedInvoiceModelUpdateFormView.as_view(), name='approved_invoice_update'), + path('sales/invoices//paid_invoice_update/', views.PaidInvoiceModelUpdateFormView.as_view(), name='paid_invoice_update'), + # path('sales/estimates//preview/', views.EstimatePreviewView.as_view(), name='estimate_preview'), # path('send_email/', views.send_email, name='send_email'), @@ -158,11 +162,13 @@ urlpatterns = [ # # Journal # path('sales/journal//create/', views.JournalEntryCreateView.as_view(), name='journal_create'), - #Items + # Items path('items/services/', views.ItemServiceListView.as_view(), name='item_service_list'), path('items/services/create/', views.ItemServiceCreateView.as_view(), name='item_service_create'), - - + # Expanese + path('items/expeneses/', views.ItemExpenseListView.as_view(), name='item_expense_list'), + path('items/expeneses/create/', views.ItemExpenseCreateView.as_view(), name='item_expense_create'), + path('items/expeneses//update/', views.ItemExpenseUpdateView.as_view(), name='item_expense_update'), ] diff --git a/inventory/utils.py b/inventory/utils.py index 18bd52ad..4965d647 100644 --- a/inventory/utils.py +++ b/inventory/utils.py @@ -9,16 +9,18 @@ from django.core.mail import send_mail from django.utils.translation import gettext_lazy as _ from inventory.utilities.financials import get_financial_value from django_ledger.models.items import ItemModel +from django_ledger.models import InvoiceModel, EstimateModel + def get_jwt_token(): - url = 'https://carapi.app/api/auth/login' + url = "https://carapi.app/api/auth/login" headers = { - 'accept': 'text/plain', - 'Content-Type': 'application/json', + "accept": "text/plain", + "Content-Type": "application/json", } data = { "api_token": "f5204a00-6f31-4de2-96d8-ed998e0d230c", - "api_secret": "8c11320781a5b8f4f327b6937e6f8241" + "api_secret": "8c11320781a5b8f4f327b6937e6f8241", } try: response = requests.post(url, headers=headers, json=data) @@ -30,9 +32,9 @@ def get_jwt_token(): def localize_some_words(): - success = _('success') - error = _('error') - forget = _('Forgot Password?') + success = _("success") + error = _("error") + forget = _("Forgot Password?") return None @@ -42,9 +44,19 @@ def get_calculations(quotation): qc_len = quotation.quotation_cars.count() cars = [x.car for x in quotation.quotation_cars.all()] finances = models.CarFinance.objects.filter(car__in=cars) - + services = ItemModel.objects.filter(additional_finances__in=finances).all() - data = [{"name":x.name,"price":x.default_amount,"total_price":x.default_amount * qc_len,"vated":float(x.default_amount) * 0.15 * float(qc_len),"total_price_vat":float(x.default_amount) + (float(x.default_amount) * 0.15 * float(qc_len))} for x in services] + data = [ + { + "name": x.name, + "price": x.default_amount, + "total_price": x.default_amount * qc_len, + "vated": float(x.default_amount) * 0.15 * float(qc_len), + "total_price_vat": float(x.default_amount) + + (float(x.default_amount) * 0.15 * float(qc_len)), + } + for x in services + ] context["services"] = data context["total_cost"] = 0 context["total_vat"] = 0 @@ -52,7 +64,9 @@ def get_calculations(quotation): for k in context["services"]: context["total_cost"] += k["total_price"] context["total_vat"] += k["vated"] - context["total_cost_vat"] = float(context["total_cost"])+float(context["total_vat"]) + context["total_cost_vat"] = float(context["total_cost"]) + float( + context["total_vat"] + ) return context @@ -66,12 +80,13 @@ def send_email(from_, to_, subject, message): def get_user_type(request): dealer = "" - if hasattr(request.user, 'dealer'): + if hasattr(request.user, "dealer"): dealer = request.user.dealer - elif hasattr(request.user, 'staff'): + elif hasattr(request.user, "staff"): dealer = request.user.staff.dealer return dealer + def get_dealer_from_instance(instance): if instance.dealer.staff: return instance.dealer @@ -79,7 +94,7 @@ def get_dealer_from_instance(instance): return instance.dealer -def reserve_car(car,request): +def reserve_car(car, request): try: reserved_until = timezone.now() + timezone.timedelta(hours=24) models.CarReservation.objects.create( @@ -97,5 +112,70 @@ def reserve_car(car,request): def calculate_vat_amount(amount): vat = models.VatRate.objects.filter(is_active=True).first() if vat: - return ((amount * Decimal(vat.rate)).quantize(Decimal('0.01')),vat.rate) - return amount \ No newline at end of file + return ((amount * Decimal(vat.rate)).quantize(Decimal("0.01")), vat.rate) + return amount + + +def get_financial_values(model): + vat = models.VatRate.objects.filter(is_active=True).first() + + if not model.get_itemtxs_data()[0].exists(): + return { + "vat_amount": 0, + "total": 0, + "grand_total": 0, + "discount_amount": 0, + "vat": 0, + "car_and_item_info": [], + "additional_services": [], + } + + data = model.get_itemtxs_data()[0].all() + + if isinstance(model, InvoiceModel): + data = model.ce_model.get_itemtxs_data()[0].all() + + car_and_item_info = [ + { + "car": models.Car.objects.get(vin=x.item_model.name), + "total": models.Car.objects.get( + vin=x.item_model.name + ).finances.selling_price + * Decimal(x.ce_quantity), + "itemmodel": x, + } + for x in data + ] + total = sum( + Decimal(models.Car.objects.get(vin=x.item_model.name).finances.total) + * Decimal(x.ce_quantity) + for x in data + ) + discount_amount = sum( + models.CarFinance.objects.get(car__vin=i.item_model.name).discount_amount + for i in data + ) + + additional_services = [] + + for i in data: + cf = models.CarFinance.objects.get(car__vin=i.item_model.name) + if cf.additional_services.exists(): + additional_services.extend( + [ + {"name": x.name, "price": x.price} + for x in cf.additional_services.all() + ] + ) + + grand_total = Decimal(total) - Decimal(discount_amount) + vat_amount = round(Decimal(grand_total) * Decimal(vat.rate), 2) + return { + "car_and_item_info": car_and_item_info, + "total": total, + "discount_amount": discount_amount, + "additional_services": additional_services, + "grand_total": grand_total + vat_amount, + "vat_amount": vat_amount, + "vat": vat.rate, + } diff --git a/inventory/views.py b/inventory/views.py index 38532065..333c06ee 100644 --- a/inventory/views.py +++ b/inventory/views.py @@ -16,10 +16,15 @@ from django_ledger.forms.bank_account import ( BankAccountCreateForm, BankAccountUpdateForm, ) +from django_ledger.forms.invoice import ( + DraftInvoiceModelUpdateForm, + ApprovedInvoiceModelUpdateForm, + PaidInvoiceModelUpdateForm, +) from django_ledger.forms.account import AccountModelCreateForm, AccountModelUpdateForm from django_ledger.forms.estimate import EstimateModelCreateForm from django_ledger.forms.invoice import InvoiceModelCreateForm -from django_ledger.forms.item import ServiceCreateForm +from django_ledger.forms.item import ServiceCreateForm,ExpenseItemCreateForm,ExpenseItemUpdateForm from django_ledger.forms.journal_entry import JournalEntryModelCreateForm from django_ledger.io import roles from django.contrib.admin.models import LogEntry @@ -63,6 +68,7 @@ from django.contrib.auth.models import Group from .utils import ( calculate_vat_amount, get_calculations, + get_financial_values, reserve_car, send_email, get_user_type, @@ -1670,7 +1676,7 @@ class AccountListView(LoginRequiredMixin, ListView): def get_queryset(self): entity = self.request.user.dealer.entity qs = entity.get_all_accounts() - paginator = Paginator(qs, 10) + paginator = Paginator(qs, 20) page_number = self.request.GET.get("page", 1) # Default to page 1 page_obj = paginator.get_page(page_number) return page_obj @@ -1904,51 +1910,15 @@ class EstimateDetailView(LoginRequiredMixin, DetailView): def get_context_data(self, **kwargs): estimate = kwargs.get("object") - vat = models.VatRate.objects.filter(is_active=True).first() - if estimate.get_itemtxs_data(): - car_and_item_info = [ - { - "car": models.Car.objects.get(vin=x.item_model.name), - "total": models.Car.objects.get( - vin=x.item_model.name - ).finances.selling_price - * Decimal(x.ce_quantity), - "itemmodel": x, - } - for x in estimate.get_itemtxs_data()[0].all() - ] - total = sum( - float(models.Car.objects.get(vin=x.item_model.name).finances.total) - * float(x.ce_quantity) - for x in estimate.get_itemtxs_data()[0].all() - ) - discount_amount = sum( - models.CarFinance.objects.get( - car__vin=i.item_model.name - ).discount_amount - for i in estimate.get_itemtxs_data()[0].all() - ) - additional_services = [] - for i in estimate.get_itemtxs_data()[0].all(): - cf = models.CarFinance.objects.get(car__vin=i.item_model.name) - if cf.additional_services.exists(): - additional_services.extend( - [ - {"name": x.name, "price": x.price} - for x in cf.additional_services.all() - ] - ) + data = get_financial_values(estimate) - grand_total = float(total) - float(discount_amount) - vat_amount = round(float(grand_total) * float(vat.rate), 2) - - kwargs["vat_amount"] = vat_amount - kwargs["car_and_item_info"] = car_and_item_info - kwargs["total"] = grand_total + vat_amount - kwargs["discount_amount"] = discount_amount - kwargs["vat"] = vat.rate - kwargs["additional_services"] = additional_services + kwargs["vat_amount"] = data["vat_amount"] + kwargs["total"] = data["grand_total"] + kwargs["discount_amount"] = data["discount_amount"] + kwargs["vat"] = data["vat"] + kwargs["car_and_item_info"] = data["car_and_item_info"] + kwargs["additional_services"] = data["additional_services"] kwargs["invoice"] = ( InvoiceModel.objects.all().filter(ce_model=estimate).first() ) @@ -1976,51 +1946,15 @@ class EstimatePreviewView(LoginRequiredMixin, DetailView): def get_context_data(self, **kwargs): estimate = kwargs.get("object") - vat = models.VatRate.objects.filter(is_active=True).first() if estimate.get_itemtxs_data(): - car_and_item_info = [ - { - "car": models.Car.objects.get(vin=x.item_model.name), - "total": models.Car.objects.get( - vin=x.item_model.name - ).finances.selling_price - * Decimal(x.ce_quantity), - "itemmodel": x, - } - for x in estimate.get_itemtxs_data()[0].all() - ] - total = sum( - float(models.Car.objects.get(vin=x.item_model.name).finances.total) - * float(x.ce_quantity) - for x in estimate.get_itemtxs_data()[0].all() - ) - discount_amount = sum( - models.CarFinance.objects.get( - car__vin=i.item_model.name - ).discount_amount - for i in estimate.get_itemtxs_data()[0].all() - ) + data = get_financial_values(estimate) - additional_services = [] - - for i in estimate.get_itemtxs_data()[0].all(): - cf = models.CarFinance.objects.get(car__vin=i.item_model.name) - if cf.additional_services.exists(): - additional_services.extend( - [ - {"name": x.name, "price": x.price} - for x in cf.additional_services.all() - ] - ) - - grand_total = float(total) - float(discount_amount) - vat_amount = round(float(grand_total) * float(vat.rate), 2) - - kwargs["vat_amount"] = vat_amount - kwargs["total"] = grand_total + vat_amount - kwargs["vat"] = vat.rate - kwargs["car_and_item_info"] = car_and_item_info - kwargs["additional_services"] = additional_services + kwargs["vat_amount"] = data["vat_amount"] + kwargs["total"] = data["grand_total"] + data["vat_amount"] + kwargs["discount_amount"] = data["discount_amount"] + kwargs["vat"] = data["vat"] + kwargs["car_and_item_info"] = data["car_and_item_info"] + kwargs["additional_services"] = data["additional_services"] return super().get_context_data(**kwargs) @@ -2077,53 +2011,16 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView): def get_context_data(self, **kwargs): invoice = kwargs.get("object") - vat = models.VatRate.objects.filter(is_active=True).first() if invoice.get_itemtxs_data(): - car_and_item_info = [ - { - "car": models.Car.objects.get(vin=x.item_model.name), - "total": models.Car.objects.get( - vin=x.item_model.name - ).finances.selling_price - * Decimal(x.quantity), - "itemmodel": x, - } - for x in invoice.get_itemtxs_data()[0].all() - ] - total = sum( - float(models.Car.objects.get(vin=x.item_model.name).finances.total) - * float(x.quantity) - for x in invoice.get_itemtxs_data()[0].all() - ) - discount_amount = sum( - models.CarFinance.objects.get( - car__vin=i.item_model.name - ).discount_amount - for i in invoice.get_itemtxs_data()[0].all() - ) + data = get_financial_values(invoice) - additional_services = [] - - for i in invoice.get_itemtxs_data()[0].all(): - cf = models.CarFinance.objects.get(car__vin=i.item_model.name) - if cf.additional_services.exists(): - additional_services.extend( - [ - {"name": x.name, "price": x.price} - for x in cf.additional_services.all() - ] - ) - - grand_total = float(total) - float(discount_amount) - vat_amount = round(float(grand_total) * float(vat.rate), 2) - - kwargs["vat_amount"] = vat_amount - kwargs["total"] = grand_total + vat_amount - kwargs["discount_amount"] = discount_amount - kwargs["vat"] = vat.rate - kwargs["car_and_item_info"] = car_and_item_info - kwargs["additional_services"] = additional_services + kwargs["vat_amount"] = data["vat_amount"] + kwargs["total"] = data["grand_total"] + kwargs["discount_amount"] = data["discount_amount"] + kwargs["vat"] = data["vat"] + kwargs["car_and_item_info"] = data["car_and_item_info"] + kwargs["additional_services"] = data["additional_services"] kwargs["payments"] = JournalEntryModel.objects.filter( ledger=invoice.ledger ).all() @@ -2131,6 +2028,65 @@ class InvoiceDetailView(LoginRequiredMixin, DetailView): return super().get_context_data(**kwargs) +class DraftInvoiceModelUpdateFormView(LoginRequiredMixin, UpdateView): + model = InvoiceModel + form_class = DraftInvoiceModelUpdateForm + template_name = "sales/invoices/draft_invoice_update.html" + success_url = reverse_lazy("invoice_list") + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + dealer = get_user_type(self.request.user.dealer) + kwargs["entity_slug"] = dealer.entity + kwargs["user_model"] = dealer.entity.admin + return kwargs + + +class ApprovedInvoiceModelUpdateFormView(LoginRequiredMixin, UpdateView): + model = InvoiceModel + form_class = ApprovedInvoiceModelUpdateForm + template_name = "sales/invoices/approved_invoice_update.html" + success_url = reverse_lazy("invoice_list") + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + dealer = get_user_type(self.request.user.dealer) + kwargs["entity_slug"] = dealer.entity + kwargs["user_model"] = dealer.entity.admin + return kwargs + + def get_success_url(self): + return reverse_lazy("invoice_detail", kwargs={"pk": self.object.pk}) + + +class PaidInvoiceModelUpdateFormView(LoginRequiredMixin, UpdateView): + model = InvoiceModel + form_class = PaidInvoiceModelUpdateForm + template_name = "sales/invoices/paid_invoice_update.html" + success_url = reverse_lazy("invoice_list") + + def get_form_kwargs(self): + kwargs = super().get_form_kwargs() + dealer = get_user_type(self.request.user.dealer) + kwargs["entity_slug"] = dealer.entity + kwargs["user_model"] = dealer.entity.admin + return kwargs + + def get_success_url(self): + return reverse_lazy("invoice_detail", kwargs={"pk": self.object.pk}) + + def form_valid(self, form): + invoice = form.save() + + if invoice.get_amount_open() > 0: + messages.error(self.request, "Invoice is not fully paid") + return redirect("invoice_detail", pk=invoice.pk) + else: + invoice.post_ledger() + invoice.save() + return super().form_valid(form) + + @login_required def invoice_mark_as(request, pk): invoice = get_object_or_404(InvoiceModel, pk=pk) @@ -2143,6 +2099,7 @@ def invoice_mark_as(request, pk): messages.error(request, "invoice is not ready for approval") return redirect("invoice_detail", pk=invoice.pk) invoice.mark_as_approved(entity_slug=entity.slug, user_model=user) + # invoice.post_ledger() invoice.save() ledger = ( entity.get_ledgers().filter(name=f"Invoice {str(invoice.pk)}").first() @@ -2168,18 +2125,11 @@ def invoice_create(request, pk): ) if form.is_valid(): invoice = form.save(commit=False) - invoice_model = entity.create_invoice( - customer_model=invoice.customer, - terms=invoice.terms, - cash_account=invoice.cash_account, - prepaid_account=invoice.prepaid_account, - coa_model=entity.get_default_coa(), - ) - ledger = entity.create_ledger(name=f"Invoice {str(invoice_model.pk)}") - invoice_model.ledgar = ledger - ledger.invoicemodel = invoice_model + ledger = entity.create_ledger(name=str(invoice.pk)) + invoice.ledgar = ledger + ledger.invoicemodel = invoice ledger.save() - invoice_model.save() + invoice.save() unit_items = estimate.get_itemtxs_data()[0] @@ -2205,18 +2155,18 @@ def invoice_create(request, pk): for i in itemtxs } - invoice_itemtxs = invoice_model.migrate_itemtxs( + invoice_itemtxs = invoice.migrate_itemtxs( itemtxs=invoice_itemtxs, commit=True, operation=InvoiceModel.ITEMIZE_APPEND, ) - invoice_model.bind_estimate(estimate) - invoice_model.mark_as_review() + invoice.bind_estimate(estimate) + invoice.mark_as_review() estimate.mark_as_completed() estimate.save() - invoice_model.save() + invoice.save() messages.success(request, "Invoice created successfully!") - return redirect("invoice_detail", pk=invoice_model.pk) + return redirect("invoice_detail", pk=invoice.pk) form.initial["customer"] = estimate.customer context = { "form": form, @@ -2232,51 +2182,15 @@ class InvoicePreviewView(LoginRequiredMixin, DetailView): def get_context_data(self, **kwargs): invoice = kwargs.get("object") - vat = models.VatRate.objects.filter(is_active=True).first() if invoice.get_itemtxs_data(): - car_and_item_info = [ - { - "car": models.Car.objects.get(vin=x.item_model.name), - "total": models.Car.objects.get( - vin=x.item_model.name - ).finances.selling_price - * Decimal(x.ce_quantity), - "itemmodel": x, - } - for x in invoice.ce_model.get_itemtxs_data()[0].all() - ] - total = sum( - float(models.Car.objects.get(vin=x.item_model.name).finances.total) - * float(x.ce_quantity) - for x in invoice.ce_model.get_itemtxs_data()[0].all() - ) - discount_amount = sum( - models.CarFinance.objects.get( - car__vin=i.item_model.name - ).discount_amount - for i in invoice.ce_model.get_itemtxs_data()[0].all() - ) + data = get_financial_values(invoice) - additional_services = [] - - for i in invoice.ce_model.get_itemtxs_data()[0].all(): - cf = models.CarFinance.objects.get(car__vin=i.item_model.name) - if cf.additional_services.exists(): - additional_services.extend( - [ - {"name": x.name, "price": x.price} - for x in cf.additional_services.all() - ] - ) - - grand_total = float(total) - float(discount_amount) - vat_amount = round(float(grand_total) * float(vat.rate), 2) - - kwargs["vat_amount"] = vat_amount - kwargs["total"] = grand_total + vat_amount - kwargs["vat"] = vat.rate - kwargs["car_and_item_info"] = car_and_item_info - kwargs["additional_services"] = additional_services + kwargs["vat_amount"] = data["vat_amount"] + kwargs["total"] = data["grand_total"] + data["vat_amount"] + kwargs["discount_amount"] = data["discount_amount"] + kwargs["vat"] = data["vat"] + kwargs["car_and_item_info"] = data["car_and_item_info"] + kwargs["additional_services"] = data["additional_services"] return super().get_context_data(**kwargs) @@ -2310,8 +2224,8 @@ def PaymentCreateView(request, pk=None): ledger = None try: ledger = LedgerModel.objects.filter( - name=f"Invoice {str(invoice.pk)}", entity=entity - ).first() + name=str(invoice.pk), entity=entity + ).first() journal = JournalEntryModel.objects.create( posted=False, description=f"Payment for Invoice {invoice.invoice_number}", @@ -2338,7 +2252,9 @@ def PaymentCreateView(request, pk=None): tx_type="credit", description="Payment Received", ) + journal.posted = True invoice.make_payment(amount) + journal.save() invoice.save() if invoice.amount_due == invoice.amount_paid: @@ -2346,6 +2262,8 @@ def PaymentCreateView(request, pk=None): entity_slug=entity.slug, user_model=entity.admin ) invoice.save() + ledger.post() + ledger.save() messages.success(request, "Payment created successfully!") return redirect("invoice_detail", pk=invoice.pk) except Exception as e: @@ -2532,12 +2450,6 @@ class ItemServiceCreateView(CreateView): template_name = "items/service/service_create.html" success_url = reverse_lazy("item_service_list") - # def get_form_kwargs(self): - # kwargs = super().get_form_kwargs() - # kwargs["entity_slug"] = self.request.user.dealer.entity.slug - # kwargs["user_model"] = self.request.user.dealer.entity.admin - # return kwargs - def form_valid(self, form): vat = models.VatRate.objects.get(is_active=True) form.instance.dealer = get_user_type(self.request.user.dealer) @@ -2557,6 +2469,54 @@ class ItemServiceListView(ListView): return items +class ItemExpenseCreateView(CreateView): + model = ItemModel + form_class = ExpenseItemCreateForm + template_name = "items/expenses/expense_create.html" + success_url = reverse_lazy("item_expense_list") + + def get_form_kwargs(self): + dealer = get_user_type(self.request.user.dealer) + kwargs = super().get_form_kwargs() + kwargs["entity_slug"] = dealer.entity.slug + kwargs["user_model"] = dealer.entity.admin + return kwargs + + + def form_valid(self, form): + dealer = get_user_type(self.request.user.dealer) + form.instance.entity = dealer.entity + return super().form_valid(form) + +class ItemExpenseUpdateView(UpdateView): + model = ItemModel + form_class = ExpenseItemUpdateForm + template_name = "items/expenses/expense_update.html" + success_url = reverse_lazy("item_expense_list") + + def get_form_kwargs(self): + dealer = get_user_type(self.request.user.dealer) + kwargs = super().get_form_kwargs() + kwargs["entity_slug"] = dealer.entity.slug + kwargs["user_model"] = dealer.entity.admin + return kwargs + + + def form_valid(self, form): + dealer = get_user_type(self.request.user.dealer) + form.instance.entity = dealer.entity + return super().form_valid(form) + +class ItemExpenseListView(ListView): + model = ItemModel + template_name = "items/expenses/expenses_list.html" + context_object_name = "expenses" + + def get_queryset(self): + dealer = get_user_type(self.request) + items = dealer.entity.get_items_expenses() + return items + class SubscriptionPlans(ListView): model = models.SubscriptionPlan template_name = "subscriptions/subscription_plan.html" @@ -2616,6 +2576,8 @@ def send_email_view(request, pk): ) + + # errors def custom_page_not_found_view(request, exception): return render(request, "errors/404.html", {}) @@ -2631,3 +2593,6 @@ def custom_permission_denied_view(request, exception=None): def custom_bad_request_view(request, exception=None): return render(request, "errors/400.html", {}) + + + diff --git a/templates/items/expenses/expense_create.html b/templates/items/expenses/expense_create.html new file mode 100644 index 00000000..236e08de --- /dev/null +++ b/templates/items/expenses/expense_create.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% load crispy_forms_filters %} +{% load static %} +{% load i18n %} +{% block title %}{{ _("Expenses") }}{% endblock title %} +{% block content %} +
+
+
+
+
{{ _("Add Expense") }}
+
+
+ {% csrf_token %} + {{ form|crispy }} + +
+
+
+
+
+
+{% endblock content %} + diff --git a/templates/items/expenses/expense_update.html b/templates/items/expenses/expense_update.html new file mode 100644 index 00000000..e3da11f3 --- /dev/null +++ b/templates/items/expenses/expense_update.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% load crispy_forms_filters %} +{% load static %} +{% load i18n %} +{% block title %}{{ _("Expenses") }}{% endblock title %} +{% block content %} +
+
+
+
+
{{ _("Update Expense") }}
+
+
+ {% csrf_token %} + {{ form|crispy }} + +
+
+
+
+
+
+{% endblock content %} + diff --git a/templates/items/expenses/expenses_list.html b/templates/items/expenses/expenses_list.html new file mode 100644 index 00000000..0c708094 --- /dev/null +++ b/templates/items/expenses/expenses_list.html @@ -0,0 +1,46 @@ +{% extends "base.html" %} +{% load i18n static %} + +{% block title %}{{ _("Expenses") }}{% endblock title %} + +{% block content %} +
+

{% trans "Expenses" %}

+
+ +
+ + + + + + + + + + + {% for expense in expenses %} + + + + + + + {% empty %} + + + + {% endfor %} + +
{% trans "Item Number" %}{% trans "Name" %}{% trans "Unit of Measure" %}{% trans "Action" %}
{{ expense.item_number }}{{ expense.name }}{{ expense.uom }} + + {% trans "Update" %} + +
{% trans "No Invoice Found" %}
+
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/templates/sales/invoices/approved_invoice_update.html b/templates/sales/invoices/approved_invoice_update.html new file mode 100644 index 00000000..9b06f7fc --- /dev/null +++ b/templates/sales/invoices/approved_invoice_update.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% load i18n %} +{% load crispy_forms_filters %} + +{% block content %} + +
+
+
+
+
{{ _("Update Invoice") }}
+
+
+ {% csrf_token %} + {{ form|crispy }} + +
+
+
+
+
+
+ +{% endblock content %} \ No newline at end of file diff --git a/templates/sales/invoices/draft_invoice_update.html b/templates/sales/invoices/draft_invoice_update.html new file mode 100644 index 00000000..9b06f7fc --- /dev/null +++ b/templates/sales/invoices/draft_invoice_update.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% load i18n %} +{% load crispy_forms_filters %} + +{% block content %} + +
+
+
+
+
{{ _("Update Invoice") }}
+
+
+ {% csrf_token %} + {{ form|crispy }} + +
+
+
+
+
+
+ +{% endblock content %} \ No newline at end of file diff --git a/templates/sales/invoices/invoice_detail.html b/templates/sales/invoices/invoice_detail.html index ef207ad1..7b67ed27 100644 --- a/templates/sales/invoices/invoice_detail.html +++ b/templates/sales/invoices/invoice_detail.html @@ -39,7 +39,10 @@ {% endif %} {% if invoice.invoice_status == 'approved' %} - {% trans 'Record Payment' %} + {% trans 'Record Payment' %} + {% endif %} + {% if invoice.get_amount_open == 0 %} + {% trans 'Mark As Paid' %} {% endif %} {% trans 'Preview' %} @@ -55,6 +58,7 @@

{% trans 'Paid Amount' %}

${{invoice.amount_paid}}

+
Owned ${{invoice.get_amount_open}}
@@ -148,7 +152,7 @@ {{forloop.counter}} {{item.car.id_car_model}} - {{item.itemmodel.quantity}} + {{item.itemmodel.ce_quantity}} {{item.car.finances.selling_price}} {{item.total}} diff --git a/templates/sales/invoices/paid_invoice_update.html b/templates/sales/invoices/paid_invoice_update.html new file mode 100644 index 00000000..9b06f7fc --- /dev/null +++ b/templates/sales/invoices/paid_invoice_update.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} +{% load i18n %} +{% load crispy_forms_filters %} + +{% block content %} + +
+
+
+
+
{{ _("Update Invoice") }}
+
+
+ {% csrf_token %} + {{ form|crispy }} + +
+
+
+
+
+
+ +{% endblock content %} \ No newline at end of file