diff --git a/tienda_ilusion/don_confiao/admin.py b/tienda_ilusion/don_confiao/admin.py index 0756544..fe34059 100644 --- a/tienda_ilusion/don_confiao/admin.py +++ b/tienda_ilusion/don_confiao/admin.py @@ -1,7 +1,9 @@ from django.contrib import admin -from .models import Sale, SaleLine, Product, ProductCategory +from .models import Sale, SaleLine, Product, ProductCategory, Payment, ReconciliationJar admin.site.register(Sale) admin.site.register(SaleLine) admin.site.register(Product) admin.site.register(ProductCategory) +admin.site.register(Payment) +admin.site.register(ReconciliationJar) diff --git a/tienda_ilusion/don_confiao/forms.py b/tienda_ilusion/don_confiao/forms.py index 72b4b9c..1c739f1 100644 --- a/tienda_ilusion/don_confiao/forms.py +++ b/tienda_ilusion/don_confiao/forms.py @@ -1,7 +1,7 @@ from django import forms -from django.forms.widgets import DateInput +from django.forms.widgets import DateInput, DateTimeInput -from .models import Sale, SaleLine +from .models import Sale, SaleLine, ReconciliationJar class ImportProductsForm(forms.Form): @@ -39,3 +39,17 @@ LineaFormSet = forms.models.inlineformset_factory( extra=1, fields='__all__' ) + +class ReconciliationJarForm(forms.ModelForm): + class Meta: + model = ReconciliationJar + fields = [ + 'date_time', + 'description', + 'reconcilier', + 'cash_taken', + 'cash_discrepancy', + ] + widgets = { + 'date_time': DateTimeInput(attrs={'type': 'datetime-local'}) + } diff --git a/tienda_ilusion/don_confiao/migrations/0012_payment.py b/tienda_ilusion/don_confiao/migrations/0012_payment.py new file mode 100644 index 0000000..474ef16 --- /dev/null +++ b/tienda_ilusion/don_confiao/migrations/0012_payment.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.6 on 2024-07-13 14:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('don_confiao', '0011_alter_product_name'), + ] + + operations = [ + migrations.CreateModel( + name='Payment', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date_time', models.DateTimeField()), + ('type_payment', models.CharField(choices=[('CASH', 'Cash'), ('CONFIAR', 'Confiar'), ('BANCOLOMBIA', 'Bancolombia')], default='CASH', max_length=30)), + ('mount', models.DecimalField(decimal_places=2, max_digits=9)), + ('description', models.CharField(blank=True, max_length=255, null=True)), + ], + ), + ] diff --git a/tienda_ilusion/don_confiao/migrations/0013_rename_mount_payment_amount.py b/tienda_ilusion/don_confiao/migrations/0013_rename_mount_payment_amount.py new file mode 100644 index 0000000..c55facb --- /dev/null +++ b/tienda_ilusion/don_confiao/migrations/0013_rename_mount_payment_amount.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.6 on 2024-07-13 15:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('don_confiao', '0012_payment'), + ] + + operations = [ + migrations.RenameField( + model_name='payment', + old_name='mount', + new_name='amount', + ), + ] diff --git a/tienda_ilusion/don_confiao/migrations/0014_reconciliationjar_payment_reconciliation_jar.py b/tienda_ilusion/don_confiao/migrations/0014_reconciliationjar_payment_reconciliation_jar.py new file mode 100644 index 0000000..9ea211f --- /dev/null +++ b/tienda_ilusion/don_confiao/migrations/0014_reconciliationjar_payment_reconciliation_jar.py @@ -0,0 +1,27 @@ +# Generated by Django 5.0.6 on 2024-07-13 16:35 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('don_confiao', '0013_rename_mount_payment_amount'), + ] + + operations = [ + migrations.CreateModel( + name='ReconciliationJar', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date_time', models.DateTimeField()), + ('description', models.CharField(blank=True, max_length=255, null=True)), + ], + ), + migrations.AddField( + model_name='payment', + name='reconciliation_jar', + field=models.ForeignKey(default=None, on_delete=django.db.models.deletion.RESTRICT, to='don_confiao.reconciliationjar'), + ), + ] diff --git a/tienda_ilusion/don_confiao/migrations/0015_alter_payment_reconciliation_jar.py b/tienda_ilusion/don_confiao/migrations/0015_alter_payment_reconciliation_jar.py new file mode 100644 index 0000000..55d21cb --- /dev/null +++ b/tienda_ilusion/don_confiao/migrations/0015_alter_payment_reconciliation_jar.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.6 on 2024-07-13 16:36 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('don_confiao', '0014_reconciliationjar_payment_reconciliation_jar'), + ] + + operations = [ + migrations.AlterField( + model_name='payment', + name='reconciliation_jar', + field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.RESTRICT, to='don_confiao.reconciliationjar'), + ), + ] diff --git a/tienda_ilusion/don_confiao/migrations/0016_reconciliationjar_cash_discrepancy_and_more.py b/tienda_ilusion/don_confiao/migrations/0016_reconciliationjar_cash_discrepancy_and_more.py new file mode 100644 index 0000000..8bb7bd2 --- /dev/null +++ b/tienda_ilusion/don_confiao/migrations/0016_reconciliationjar_cash_discrepancy_and_more.py @@ -0,0 +1,37 @@ +# Generated by Django 5.0.6 on 2024-07-13 18:10 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('don_confiao', '0015_alter_payment_reconciliation_jar'), + ] + + operations = [ + migrations.AddField( + model_name='reconciliationjar', + name='cash_discrepancy', + field=models.DecimalField(decimal_places=2, default=0, max_digits=9), + preserve_default=False, + ), + migrations.AddField( + model_name='reconciliationjar', + name='cash_float', + field=models.DecimalField(decimal_places=2, default=0, max_digits=9), + preserve_default=False, + ), + migrations.AddField( + model_name='reconciliationjar', + name='cash_taken', + field=models.DecimalField(decimal_places=2, default=0, max_digits=9), + preserve_default=False, + ), + migrations.AddField( + model_name='reconciliationjar', + name='reconciler', + field=models.CharField(default='Jorge', max_length=255), + preserve_default=False, + ), + ] diff --git a/tienda_ilusion/don_confiao/migrations/0017_reconciliationjar_draft.py b/tienda_ilusion/don_confiao/migrations/0017_reconciliationjar_draft.py new file mode 100644 index 0000000..13dc149 --- /dev/null +++ b/tienda_ilusion/don_confiao/migrations/0017_reconciliationjar_draft.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.6 on 2024-07-13 18:15 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('don_confiao', '0016_reconciliationjar_cash_discrepancy_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='reconciliationjar', + name='draft', + field=models.BooleanField(default=False), + ), + ] diff --git a/tienda_ilusion/don_confiao/migrations/0018_rename_draft_reconciliationjar_valid.py b/tienda_ilusion/don_confiao/migrations/0018_rename_draft_reconciliationjar_valid.py new file mode 100644 index 0000000..7228161 --- /dev/null +++ b/tienda_ilusion/don_confiao/migrations/0018_rename_draft_reconciliationjar_valid.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.6 on 2024-07-13 18:19 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('don_confiao', '0017_reconciliationjar_draft'), + ] + + operations = [ + migrations.RenameField( + model_name='reconciliationjar', + old_name='draft', + new_name='valid', + ), + ] diff --git a/tienda_ilusion/don_confiao/migrations/0019_rename_valid_reconciliationjar_is_valid.py b/tienda_ilusion/don_confiao/migrations/0019_rename_valid_reconciliationjar_is_valid.py new file mode 100644 index 0000000..a0d7a02 --- /dev/null +++ b/tienda_ilusion/don_confiao/migrations/0019_rename_valid_reconciliationjar_is_valid.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.6 on 2024-07-13 19:56 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('don_confiao', '0018_rename_draft_reconciliationjar_valid'), + ] + + operations = [ + migrations.RenameField( + model_name='reconciliationjar', + old_name='valid', + new_name='is_valid', + ), + ] diff --git a/tienda_ilusion/don_confiao/migrations/0020_remove_reconciliationjar_cash_float.py b/tienda_ilusion/don_confiao/migrations/0020_remove_reconciliationjar_cash_float.py new file mode 100644 index 0000000..dea5619 --- /dev/null +++ b/tienda_ilusion/don_confiao/migrations/0020_remove_reconciliationjar_cash_float.py @@ -0,0 +1,17 @@ +# Generated by Django 5.0.6 on 2024-07-13 20:13 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('don_confiao', '0019_rename_valid_reconciliationjar_is_valid'), + ] + + operations = [ + migrations.RemoveField( + model_name='reconciliationjar', + name='cash_float', + ), + ] diff --git a/tienda_ilusion/don_confiao/migrations/0021_rename_reconciler_reconciliationjar_reconcilier.py b/tienda_ilusion/don_confiao/migrations/0021_rename_reconciler_reconciliationjar_reconcilier.py new file mode 100644 index 0000000..20e8f6e --- /dev/null +++ b/tienda_ilusion/don_confiao/migrations/0021_rename_reconciler_reconciliationjar_reconcilier.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.6 on 2024-07-13 22:16 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('don_confiao', '0020_remove_reconciliationjar_cash_float'), + ] + + operations = [ + migrations.RenameField( + model_name='reconciliationjar', + old_name='reconciler', + new_name='reconcilier', + ), + ] diff --git a/tienda_ilusion/don_confiao/migrations/0022_alter_payment_reconciliation_jar.py b/tienda_ilusion/don_confiao/migrations/0022_alter_payment_reconciliation_jar.py new file mode 100644 index 0000000..5956a97 --- /dev/null +++ b/tienda_ilusion/don_confiao/migrations/0022_alter_payment_reconciliation_jar.py @@ -0,0 +1,19 @@ +# Generated by Django 5.0.6 on 2024-07-13 22:39 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('don_confiao', '0021_rename_reconciler_reconciliationjar_reconcilier'), + ] + + operations = [ + migrations.AlterField( + model_name='payment', + name='reconciliation_jar', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.RESTRICT, to='don_confiao.reconciliationjar'), + ), + ] diff --git a/tienda_ilusion/don_confiao/models.py b/tienda_ilusion/don_confiao/models.py index a3c2432..9de8188 100644 --- a/tienda_ilusion/don_confiao/models.py +++ b/tienda_ilusion/don_confiao/models.py @@ -1,5 +1,9 @@ from django.db import models from django.utils.translation import gettext_lazy as _ +from django.core.exceptions import ValidationError + +from decimal import Decimal + class Sale(models.Model): @@ -48,3 +52,84 @@ class Product(models.Model): def __str__(self): return self.name + + +class PyamentMethods(models.TextChoices): + CASH = 'CASH', _('Cash') + CONFIAR = 'CONFIAR', _('Confiar') + BANCOLOMBIA = 'BANCOLOMBIA', _('Bancolombia') + + +class ReconciliationJarSummary(): + def __init__(self, payments): + self._validate_payments(payments) + self._payments = payments + + def _validate_payments(self, payments): + pass + + @property + def total(self): + return sum([p.amount for p in self.payments]) + + @property + def payments(self): + return self._payments + + +class ReconciliationJar(models.Model): + is_valid = models.BooleanField(default=False) + date_time = models.DateTimeField() + description = models.CharField(max_length=255, null=True, blank=True) + reconcilier = models.CharField(max_length=255, null=False, blank=False) + cash_taken = models.DecimalField(max_digits=9, decimal_places=2) + cash_discrepancy = models.DecimalField(max_digits=9, decimal_places=2) + + def clean(self): + if not self.is_valid: + payments = Payment.get_reconciliation_jar_summary().payments + else: + payments = self.payment_set.all() + + payments_amount = Decimal(sum([p.amount for p in payments])) + reconciliation_ammount = Decimal(sum([ + self.cash_taken, + self.cash_discrepancy, + ])) + + equal_ammounts = reconciliation_ammount.compare(payments_amount) == Decimal('0') + if not equal_ammounts: + raise ValidationError( + {"cash_taken": _("The taken ammount has discrepancy.")} + ) + + def add_payments(self, payments): + for payment in payments: + self.payment_set.add(payment) + self.is_valid = True + +class Payment(models.Model): + date_time = models.DateTimeField() + type_payment = models.CharField( + max_length=30, + choices=PyamentMethods.choices, + default=PyamentMethods.CASH + ) + amount = models.DecimalField(max_digits=9, decimal_places=2) + reconciliation_jar = models.ForeignKey( + ReconciliationJar, + null=True, + default=None, + blank=True, + on_delete=models.RESTRICT + ) + description = models.CharField(max_length=255, null=True, blank=True) + + @classmethod + def get_reconciliation_jar_summary(cls): + return ReconciliationJarSummary( + cls.objects.filter( + type_payment=PyamentMethods.CASH, + reconciliation_jar=None + ) + ) diff --git a/tienda_ilusion/don_confiao/templates/don_confiao/reconciliate_jar.html b/tienda_ilusion/don_confiao/templates/don_confiao/reconciliate_jar.html new file mode 100644 index 0000000..6771044 --- /dev/null +++ b/tienda_ilusion/don_confiao/templates/don_confiao/reconciliate_jar.html @@ -0,0 +1,30 @@ + +{% if summary.total %} +
+

Pagos No reconciliados

+ + + + + + {% for payment in summary.payments %} + + {% endfor %} + + + + +
FechaMonto
{{ payment.date_time }}{{ payment.amount }}
Total{{ summary.total }}
+
+
+ + {% csrf_token %} + {{ form.as_table }} +
+
+
+{% else %} +
+

No hay pagos registrados.

+
+{% endif %} diff --git a/tienda_ilusion/don_confiao/test_billing.py b/tienda_ilusion/don_confiao/test_billing.py new file mode 100644 index 0000000..89a5a43 --- /dev/null +++ b/tienda_ilusion/don_confiao/test_billing.py @@ -0,0 +1,88 @@ +from django.test import TestCase +from django.core.exceptions import ValidationError +from .models import Payment, ReconciliationJar + + +class TestBilling(TestCase): + + def test_reconciliation_jar_summary(self): + cash_payment1, cash_payment2 = self._create_two_cash_payments() + jar_summary = Payment.get_reconciliation_jar_summary() + self.assertEqual(164000, jar_summary.total) + self.assertSetEqual( + {cash_payment1, cash_payment2}, + set(jar_summary.payments) + ) + + def test_reconciliation_jar_summary_use_only_cash(self): + cash_payment1, cash_payment2 = self._create_two_cash_payments() + + confiar_payment = Payment() + confiar_payment.date_time = '2024-07-07 16:00:00' + confiar_payment.type_payment = 'CONFIAR' + confiar_payment.amount = 85000 + confiar_payment.save() + + bancolombia_payment = Payment() + bancolombia_payment.date_time = '2024-07-07 12:30:00' + bancolombia_payment.type_payment = 'BANCOLOMBIA' + bancolombia_payment.amount = 12000 + bancolombia_payment.save() + + jar_summary = Payment.get_reconciliation_jar_summary() + self.assertEqual(164000, jar_summary.total) + self.assertSetEqual( + {cash_payment1, cash_payment2}, + set(jar_summary.payments) + ) + + def test_fail_validate_reconciliation_jar_with_discrepancy_values(self): + cash_payment1, cash_payment2 = self._create_two_cash_payments() + + jar_summary = Payment.get_reconciliation_jar_summary() + + reconciliation_jar = ReconciliationJar() + reconciliation_jar.date_time = '2024-07-13 13:02:00' + reconciliation_jar.description = "test reconcialiation jar" + reconciliation_jar.reconcilier = 'Jorge' + reconciliation_jar.cash_float = 0 + reconciliation_jar.cash_taken = 0 + reconciliation_jar.cash_discrepancy = 0 + reconciliation_jar.save() + + reconciliation_jar.add_payments(jar_summary.payments) + with self.assertRaises(ValidationError): + reconciliation_jar.clean() + + def test_validate_reconciliation_jar_with_cash_float(self): + cash_payment1, cash_payment2 = self._create_two_cash_payments() + jar_summary = Payment.get_reconciliation_jar_summary() + + reconciliation_jar = ReconciliationJar() + reconciliation_jar.date_time = '2024-07-13 13:02:00' + reconciliation_jar.description = "test reconcialiation jar" + reconciliation_jar.reconcilier = 'Jorge' + reconciliation_jar.cash_taken = jar_summary.total + reconciliation_jar.cash_discrepancy = 0 + reconciliation_jar.save() + + reconciliation_jar.add_payments(jar_summary.payments) + reconciliation_jar.clean() + reconciliation_jar.save() + self.assertTrue(reconciliation_jar.is_valid) + + def _create_two_cash_payments(self): + cash_payment1 = Payment() + cash_payment1.date_time = '2024-07-07 12:00:00' + cash_payment1.type_payment = 'CASH' + cash_payment1.amount = 132000 + cash_payment1.description = 'Saldo en compra' + cash_payment1.save() + + cash_payment2 = Payment() + cash_payment2.date_time = '2024-07-07 13:05:00' + cash_payment2.type_payment = 'CASH' + cash_payment2.amount = 32000 + cash_payment2.save() + + return [cash_payment1, cash_payment2] diff --git a/tienda_ilusion/don_confiao/test_reconciliation_jar_client.py b/tienda_ilusion/don_confiao/test_reconciliation_jar_client.py new file mode 100644 index 0000000..ad58d0e --- /dev/null +++ b/tienda_ilusion/don_confiao/test_reconciliation_jar_client.py @@ -0,0 +1,47 @@ +from django.test import Client, TestCase +from django.contrib.auth.models import AnonymousUser, User +#from django.conf import settings + +#from .views import import_products, products +from .models import Payment + + +class TestReconciliationJarClient(TestCase): + def setUp(self): + self.client = Client() + + def test_get_summary_info_on_view(self): + self._generate_two_cash_payments() + response = self.client.get("/don_confiao/cuadrar_tarro") + self.assertEqual(response.status_code, 200) + self.assertEqual(response.context["summary"].total, 160000) + self.assertIn('160000', response.content.decode('utf-8')) + + def test_create_reconciliation_jar(self): + self._generate_two_cash_payments() + response = self.client.post( + "/don_confiao/cuadrar_tarro", + { + "date_time": "2024-07-20T00:00", + "description": "Cuadre de prueba", + "reconcilier": "Jorge", + "cash_taken": "100000", + "cash_discrepancy": "60000", + } + ) + self.assertRedirects(response, '/don_confiao/cuadres') + + + def _generate_two_cash_payments(self): + cash_payment1 = Payment() + cash_payment1.date_time = '2024-07-07 12:00:00' + cash_payment1.type_payment = 'CASH' + cash_payment1.amount = 130000 + cash_payment1.description = 'Saldo en compra' + cash_payment1.save() + + cash_payment2 = Payment() + cash_payment2.date_time = '2024-07-07 13:05:00' + cash_payment2.type_payment = 'CASH' + cash_payment2.amount = 30000 + cash_payment2.save() diff --git a/tienda_ilusion/don_confiao/urls.py b/tienda_ilusion/don_confiao/urls.py index a9b716f..bdea574 100644 --- a/tienda_ilusion/don_confiao/urls.py +++ b/tienda_ilusion/don_confiao/urls.py @@ -8,5 +8,7 @@ urlpatterns = [ path("comprar", views.buy, name="buy"), path("compras", views.purchases, name="purchases"), path("productos", views.products, name="products"), - path("importar_productos", views.import_products, name="import_products") + path("importar_productos", views.import_products, name="import_products"), + path("cuadrar_tarro", views.reconciliate_jar, name="reconciliate_jar"), + path("cuadres", views.reconciliate_jar, name="reconciliations"), ] diff --git a/tienda_ilusion/don_confiao/views.py b/tienda_ilusion/don_confiao/views.py index 4f61508..37fb174 100644 --- a/tienda_ilusion/don_confiao/views.py +++ b/tienda_ilusion/don_confiao/views.py @@ -1,9 +1,10 @@ from django.shortcuts import render -from django.http import HttpResponseRedirect, JsonResponse -# from django.template import loader +from django.http import HttpResponse, HttpResponseRedirect, JsonResponse +from django.template import loader +from django.core.exceptions import ValidationError -from .models import Sale, Product, ProductCategory -from .forms import ImportProductsForm, PurchaseForm, LineaFormSet +from .models import Sale, Product, ProductCategory, Payment +from .forms import ImportProductsForm, PurchaseForm, LineaFormSet, ReconciliationJarForm import csv import io @@ -75,6 +76,29 @@ def import_products(request): ) +def reconciliate_jar(request): + summary = Payment.get_reconciliation_jar_summary() + if request.method == 'POST': + form = ReconciliationJarForm(request.POST) + if form.is_valid(): + reconciliation = form.save() + reconciliation.add_payments(summary.payments) + reconciliation.clean() + reconciliation.save() + return HttpResponseRedirect('cuadres') + else: + form = ReconciliationJarForm() + return render( + request, + "don_confiao/reconciliate_jar.html", + {'summary': summary, 'form': form} + ) + + +def reconciliations(request): + return HttpResponse('

Reconciliaciones

') + + def _categories_from_csv_string(categories_string, separator="&"): categories = categories_string.split(separator) clean_categories = [c.strip() for c in categories]