diff --git a/sage_invoice/models/invoice_total.py b/sage_invoice/models/invoice_total.py index 83c1503..42d0c85 100644 --- a/sage_invoice/models/invoice_total.py +++ b/sage_invoice/models/invoice_total.py @@ -1,57 +1,63 @@ +from decimal import Decimal + from django.db import models from django.utils.translation import gettext_lazy as _ class Expense(models.Model): subtotal = models.DecimalField( + verbose_name=_("Subtotal"), max_digits=10, decimal_places=2, - verbose_name=_("Subtotal"), + default=Decimal("0.00"), help_text=_("The sum of all item totals."), db_comment="Sum of all item totals in the invoice", ) tax_percentage = models.DecimalField( + verbose_name=_("Tax Percentage"), max_digits=5, decimal_places=2, - default=0, - verbose_name=_("Tax Percentage"), + default=Decimal("0.00"), help_text=_("The tax percentage applied to the invoice."), db_comment="The percentage of tax applied to the invoice", ) discount_percentage = models.DecimalField( + verbose_name=_("Discount Percentage"), max_digits=5, decimal_places=2, - default=0, - verbose_name=_("Discount Percentage"), + default=Decimal("0.00"), help_text=_("The discount percentage applied to the invoice."), db_comment="The percentage of discount applied to the invoice", ) tax_amount = models.DecimalField( + verbose_name=_("Tax Amount"), max_digits=10, decimal_places=2, - verbose_name=_("Tax Amount"), + default=Decimal("0.00"), help_text=_("The calculated tax amount."), db_comment="The total tax amount calculated based on the subtotal and tax percentage", ) discount_amount = models.DecimalField( + verbose_name=_("Discount Amount"), max_digits=10, decimal_places=2, - verbose_name=_("Discount Amount"), + default=Decimal("0.00"), help_text=_("The calculated discount amount."), db_comment="The total discount amount calculated based on the subtotal and discount percentage", ) total_amount = models.DecimalField( + verbose_name=_("Total Amount"), max_digits=10, decimal_places=2, - verbose_name=_("Total Amount"), + default=Decimal("0.00"), help_text=_("The final total after applying tax and discount."), db_comment="The final total amount after tax and discount are applied", ) invoice = models.OneToOneField( "Invoice", + verbose_name=_("Invoice"), on_delete=models.CASCADE, related_name="total", - verbose_name=_("Invoice"), help_text=_("The invoice associated with this total."), db_comment="Reference to the associated invoice", ) diff --git a/sage_invoice/service/total.py b/sage_invoice/service/total.py index a630fcc..e22ed6e 100644 --- a/sage_invoice/service/total.py +++ b/sage_invoice/service/total.py @@ -4,11 +4,11 @@ class ExpenseService: """Service class to handle calculations and saving for Expense.""" - def calculate_and_save(self, invoice_total: dict, *args, **kwargs): + def calculate_and_save(self, invoice_total, *args, **kwargs): """Calculate the subtotal, tax amount, discount amount, and total - amount and then save the instance. + amount, and then save the instance. """ - # Calculate subtotal from related items + # Calculate subtotal from related items, ensure it's not None invoice_total.subtotal = sum( item.total_price for item in invoice_total.invoice.items.all() ) @@ -34,5 +34,5 @@ def calculate_and_save(self, invoice_total: dict, *args, **kwargs): - invoice_total.discount_amount ) - # Save the Expense instance - invoice_total.save_base(*args, **kwargs) + # Save the Expense instance using the standard save method + invoice_total.save(*args, **kwargs) diff --git a/sage_invoice/tests/test_service.py b/sage_invoice/tests/test_service.py index d8c5fe3..ed78228 100644 --- a/sage_invoice/tests/test_service.py +++ b/sage_invoice/tests/test_service.py @@ -147,82 +147,82 @@ def test_render_quotation_with_items(self, template_discovery): assert rendered_content == "Rendered content" -# class TestExpenseService: - -# @pytest.fixture -# def invoice_category(self, db): -# """Fixture to create a real InvoiceCategory instance.""" -# return InvoiceCategory.objects.create( -# title="Default Category", description="A default category for testing." -# ) - -# @pytest.fixture -# def invoice(self, db, invoice_category): -# """Fixture to create a real Invoice instance with items.""" -# invoice = Invoice.objects.create( -# title="Test Invoice", -# invoice_date="2024-08-25", -# customer_name="John Doe", -# customer_email="john.doe@example.com", -# status="unpaid", -# category=invoice_category, -# due_date="2024-09-01", -# ) -# invoice.items.create( -# description="Item 1", quantity=1, unit_price=Decimal("100.00") -# ) -# invoice.items.create( -# description="Item 2", quantity=2, unit_price=Decimal("50.00") -# ) -# return invoice - -# @pytest.fixture -# def invoice_total(self, invoice): -# """Fixture to create an Expense instance.""" -# return Expense.objects.create( -# tax_percentage=Decimal("10.00"), -# discount_percentage=Decimal("5.00"), -# invoice=invoice, -# ) - -# def test_calculate_and_save(self, invoice_total): -# """Test the calculate_and_save method of ExpenseService.""" -# service = ExpenseService() - -# with mock.patch.object(Expense, "save_base") as mock_save_base: -# service.calculate_and_save(invoice_total) - -# assert invoice_total.subtotal == Decimal("200.00") -# assert invoice_total.tax_amount == Decimal("20.00") -# assert invoice_total.discount_amount == Decimal("10.00") -# assert invoice_total.total_amount == Decimal("210.00") -# mock_save_base.assert_called_once() - -# def test_calculate_and_save_with_no_items(self, db, invoice_category): -# """Test calculate_and_save when the invoice has no items.""" -# invoice = Invoice.objects.create( -# title="Empty Invoice", -# invoice_date="2024-08-25", -# customer_name="John Doe", -# customer_email="john.doe@example.com", -# status="unpaid", -# category=invoice_category, -# due_date="2024-09-01", -# ) - -# invoice_total = Expense.objects.create( -# tax_percentage=Decimal("10.00"), -# discount_percentage=Decimal("5.00"), -# invoice=invoice, -# ) - -# service = ExpenseService() - -# with mock.patch.object(Expense, "save_base") as mock_save_base: -# service.calculate_and_save(invoice_total) - -# assert invoice_total.subtotal == Decimal("0.00") -# assert invoice_total.tax_amount == Decimal("0.00") -# assert invoice_total.discount_amount == Decimal("0.00") -# assert invoice_total.total_amount == Decimal("0.00") -# mock_save_base.assert_called_once() +class TestExpenseService: + + @pytest.fixture + def invoice_category(self, db): + """Fixture to create a real InvoiceCategory instance.""" + return InvoiceCategory.objects.create( + title="Default Category", description="A default category for testing." + ) + + @pytest.fixture + def invoice(self, db, invoice_category): + """Fixture to create a real Invoice instance with items.""" + invoice = Invoice.objects.create( + title="Test Invoice", + invoice_date="2024-08-25", + customer_name="John Doe", + customer_email="john.doe@example.com", + status="unpaid", + category=invoice_category, + due_date="2024-09-01", + ) + invoice.items.create( + description="Item 1", quantity=1, unit_price=Decimal("100.00") + ) + invoice.items.create( + description="Item 2", quantity=2, unit_price=Decimal("50.00") + ) + return invoice + + @pytest.fixture + def invoice_total(self, invoice): + """Fixture to create an Expense instance.""" + return Expense.objects.create( + tax_percentage=Decimal("10.00"), + discount_percentage=Decimal("5.00"), + invoice=invoice, + ) + + def test_calculate_and_save(self, invoice_total): + """Test the calculate_and_save method of ExpenseService.""" + service = ExpenseService() + + with mock.patch.object(Expense, "save") as mock_save: + service.calculate_and_save(invoice_total) + + assert invoice_total.subtotal == Decimal("200.00") + assert invoice_total.tax_amount == Decimal("20.00") + assert invoice_total.discount_amount == Decimal("10.00") + assert invoice_total.total_amount == Decimal("210.00") + mock_save.assert_called_once() + + def test_calculate_and_save_with_no_items(self, db, invoice_category): + """Test calculate_and_save when the invoice has no items.""" + invoice = Invoice.objects.create( + title="Empty Invoice", + invoice_date="2024-08-25", + customer_name="John Doe", + customer_email="john.doe@example.com", + status="unpaid", + category=invoice_category, + due_date="2024-09-01", + ) + + invoice_total = Expense.objects.create( + tax_percentage=Decimal("10.00"), + discount_percentage=Decimal("5.00"), + invoice=invoice, + ) + + service = ExpenseService() + + with mock.patch.object(Expense, "save") as mock_save: + service.calculate_and_save(invoice_total) + + assert invoice_total.subtotal == Decimal("0.00") + assert invoice_total.tax_amount == Decimal("0.00") + assert invoice_total.discount_amount == Decimal("0.00") + assert invoice_total.total_amount == Decimal("0.00") + mock_save.assert_called_once()