Merge pull request 'Modelar cuadre de caja #10' (#12) from modelar_cuadre_de_caja_#10 into main

Reviewed-on: OneTeam/don_confiao#12
This commit is contained in:
mono 2024-07-27 13:42:05 -05:00
commit 9409eeb94a
19 changed files with 532 additions and 8 deletions

View File

@ -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)

View File

@ -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'})
}

View File

@ -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)),
],
),
]

View File

@ -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',
),
]

View File

@ -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'),
),
]

View File

@ -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'),
),
]

View File

@ -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,
),
]

View File

@ -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),
),
]

View File

@ -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',
),
]

View File

@ -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',
),
]

View File

@ -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',
),
]

View File

@ -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',
),
]

View File

@ -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'),
),
]

View File

@ -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
)
)

View File

@ -0,0 +1,30 @@
<!doctype html>
{% if summary.total %}
<div class="reconciliate_jar summary" style="border: solid 1px brown; margin: 10px">
<h2>Pagos No reconciliados</h2>
<table style="border: solid 1px blue; margin: 10px">
<thead>
<tr><th>Fecha</th><th>Monto</th></tr>
</thead>
<tbody>
{% for payment in summary.payments %}
<tr><td>{{ payment.date_time }}</td><td>{{ payment.amount }}</td></tr>
{% endfor %}
</tbody>
<tfoot>
<tr><th>Total</th><td>{{ summary.total }}</td></tr>
</tfoot>
</table>
</div>
<form method="POST">
<table style="border: solid 1px blue; margin: 10px">
{% csrf_token %}
{{ form.as_table }}
</table>
<br/><button name="form" type="submit" >Recoger dinero</button>
</form>
{% else %}
<div class="reconciliate_jar information noform">
<h2>No hay pagos registrados.</h2>
</div>
{% endif %}

View File

@ -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]

View File

@ -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()

View File

@ -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"),
]

View File

@ -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('<h1>Reconciliaciones</h1>')
def _categories_from_csv_string(categories_string, separator="&"):
categories = categories_string.split(separator)
clean_categories = [c.strip() for c in categories]