Merge pull request 'Generando cuadre del tarro en vuetify' (#83) from streamline_reconciliation_jar_process_#69 into main
Reviewed-on: OneTeam/don_confiao#83
This commit is contained in:
commit
023beaa0ee
27
Rakefile
27
Rakefile
@ -20,7 +20,12 @@ namespace :live do
|
||||
compose('logs', '-f', '-n 50', 'django', compose: DOCKER_COMPOSE)
|
||||
end
|
||||
|
||||
desc 'detener entorno'
|
||||
desc 'iniciar entorno'
|
||||
task :start do
|
||||
compose('start', compose: DOCKER_COMPOSE)
|
||||
end
|
||||
|
||||
desc 'bajar entorno'
|
||||
task :down do
|
||||
compose('down', compose: DOCKER_COMPOSE)
|
||||
end
|
||||
@ -52,6 +57,26 @@ namespace :live do
|
||||
|
||||
end
|
||||
|
||||
desc 'Desarrollo'
|
||||
namespace :dev do
|
||||
|
||||
desc 'correr test de django'
|
||||
task :test do
|
||||
compose('exec', 'django', 'python', '/app/manage.py', 'test', '/app/don_confiao')
|
||||
end
|
||||
|
||||
desc 'crear migraciones'
|
||||
task :makemigrations do
|
||||
compose('exec', 'django', 'python', '/app/manage.py', 'makemigrations')
|
||||
end
|
||||
|
||||
desc 'aplicar migraciones'
|
||||
task :migrate do
|
||||
compose('exec', 'django', 'python', '/app/manage.py', 'migrate')
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def compose(*arg, compose: DOCKER_COMPOSE)
|
||||
sh "docker compose -f #{compose} #{arg.join(' ')}"
|
||||
end
|
||||
|
@ -1,9 +1,13 @@
|
||||
from rest_framework import viewsets
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.status import HTTP_400_BAD_REQUEST
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from .models import Sale, SaleLine, Customer, Product
|
||||
from .serializers import SaleSerializer, ProductSerializer, CustomerSerializer
|
||||
from .models import Sale, SaleLine, Customer, Product, ReconciliationJar
|
||||
from .serializers import SaleSerializer, ProductSerializer, CustomerSerializer, ReconciliationJarSerializer
|
||||
|
||||
from decimal import Decimal
|
||||
import json
|
||||
|
||||
class SaleView(viewsets.ModelViewSet):
|
||||
queryset = Sale.objects.all()
|
||||
@ -46,3 +50,53 @@ class ProductView(viewsets.ModelViewSet):
|
||||
class CustomerView(viewsets.ModelViewSet):
|
||||
queryset = Customer.objects.all()
|
||||
serializer_class = CustomerSerializer
|
||||
|
||||
|
||||
class ReconciliateJarView(APIView):
|
||||
def post(self, request):
|
||||
data = request.data
|
||||
cash_purchases_id = data.get('cash_purchases')
|
||||
serializer = ReconciliationJarSerializer(data=data)
|
||||
if serializer.is_valid():
|
||||
cash_purchases = Sale.objects.filter(pk__in=cash_purchases_id)
|
||||
if not self._is_valid_total(cash_purchases, data.get('total_cash_purchases')):
|
||||
return Response(
|
||||
{'error': 'total_cash_purchases not equal to sum of all purchases.'},
|
||||
status=HTTP_400_BAD_REQUEST
|
||||
)
|
||||
reconciliation = serializer.save()
|
||||
other_purchases = self._get_other_purchases(data.get('other_totals'))
|
||||
|
||||
self._link_purchases(reconciliation, cash_purchases, other_purchases)
|
||||
return Response({'id': reconciliation.id})
|
||||
return Response(serializer.errors, status=HTTP_400_BAD_REQUEST)
|
||||
|
||||
def get(self, request):
|
||||
reconciliations = ReconciliationJar.objects.all()
|
||||
serializer = ReconciliationJarSerializer(reconciliations, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
def _is_valid_total(self, purchases, total):
|
||||
calculated_total = sum(p.get_total() for p in purchases)
|
||||
return calculated_total == Decimal(total)
|
||||
|
||||
def _get_other_purchases(self, other_totals):
|
||||
if not other_totals:
|
||||
return []
|
||||
purchases = []
|
||||
for method in other_totals:
|
||||
purchases.extend(other_totals[method]['purchases'])
|
||||
if purchases:
|
||||
return Sale.objects.filter(pk__in=purchases)
|
||||
return []
|
||||
|
||||
def _link_purchases(self, reconciliation, cash_purchases, other_purchases):
|
||||
for purchase in cash_purchases:
|
||||
purchase.reconciliation = reconciliation
|
||||
purchase.clean()
|
||||
purchase.save()
|
||||
|
||||
for purchase in other_purchases:
|
||||
purchase.reconciliation = reconciliation
|
||||
purchase.clean()
|
||||
purchase.save()
|
||||
|
@ -3,7 +3,7 @@ from django.forms.models import inlineformset_factory
|
||||
|
||||
from django.forms.widgets import DateInput, DateTimeInput
|
||||
|
||||
from .models import Sale, SaleLine, ReconciliationJar, PaymentMethods
|
||||
from .models import Sale, SaleLine, PaymentMethods
|
||||
|
||||
readonly_number_widget = forms.NumberInput(attrs={'readonly': 'readonly'})
|
||||
|
||||
@ -64,18 +64,3 @@ SaleLineFormSet = 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'})
|
||||
}
|
||||
|
@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<span>{{ formattedValue }}</span>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
locale: {
|
||||
type: String,
|
||||
default: 'es-CO',
|
||||
},
|
||||
currency: {
|
||||
type: String,
|
||||
default: 'COP',
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
formattedValue() {
|
||||
return new Intl.NumberFormat(this.locale, { style: 'currency', currency: this.currency }).format(this.value);
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
@ -29,6 +29,7 @@
|
||||
menuItems: [
|
||||
{ title: 'Inicio', route: '/'},
|
||||
{ title: 'Comprar', route:'/comprar'},
|
||||
{ title: 'Cuadrar tarro', route: '/cuadrar_tarro'}
|
||||
],
|
||||
}),
|
||||
watch: {
|
||||
|
@ -0,0 +1,213 @@
|
||||
<template>
|
||||
<v-container>
|
||||
<v-toolbar>
|
||||
<v-toolbar-title> Cuadre del Tarro </v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<v-form ref="taker" v-model="valid">
|
||||
<v-text-field
|
||||
v-model="reconciliation.date_time"
|
||||
label="Fecha"
|
||||
type="datetime-local"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
readonly
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="reconciliation.reconcilier"
|
||||
label="Cajero"
|
||||
:rules="[rules.required]"
|
||||
required
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="reconciliation.total_cash_purchases"
|
||||
label="Total Ventas en efectivo"
|
||||
:rules="[rules.required]"
|
||||
prefix="$"
|
||||
type="number"
|
||||
readonly
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="reconciliation.cash_taken"
|
||||
label="Dinero Recogido"
|
||||
:rules="[rules.required]"
|
||||
prefix="$"
|
||||
type="number"
|
||||
></v-text-field>
|
||||
<v-text-field
|
||||
v-model="reconciliation.cash_discrepancy"
|
||||
label="Descuadre"
|
||||
:rules="[rules.integer]"
|
||||
prefix="$"
|
||||
type="number"
|
||||
></v-text-field>
|
||||
<v-btn @click="submit" color="green">Recoger Dinero</v-btn>
|
||||
</v-form>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
<v-tabs v-model="selectedTab">
|
||||
<v-tab
|
||||
v-for="(purchases, payment_method) in summary.purchases"
|
||||
:key="payment_method"
|
||||
:value="payment_method"
|
||||
>
|
||||
{{ payment_method }} <CurrencyText :value="totalByMethod(payment_method)"</CurrencyText>
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-tabs-window v-model="selectedTab">
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<v-tabs-window-item
|
||||
v-for="(purchases, payment_method) in summary.purchases"
|
||||
:key="payment_method"
|
||||
:value="payment_method"
|
||||
>
|
||||
<v-data-table-virtual
|
||||
:headers="summary.headers"
|
||||
:items="summary.purchases[payment_method]"
|
||||
>
|
||||
<template v-slot:item.id="{ item }">
|
||||
<v-btn @click="openSummaryModal(item.id)">{{ item.id }}</v-btn>
|
||||
</template>
|
||||
<template v-slot:item.total="{ item }">
|
||||
<CurrencyText :value="parseFloat(item.total)"></CurrencyText>
|
||||
</template>
|
||||
</v-data-table-virtual>
|
||||
</v-tabs-window-item>
|
||||
<SummaryPurchaseModal :id="selectedPurchaseId" ref="summaryModal" />
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-tabs-window>
|
||||
</v-container>
|
||||
</template>
|
||||
<script>
|
||||
import CurrencyText from './CurrencyText.vue';
|
||||
import SummaryPurchaseModal from './SummaryPurchaseModal.vue';
|
||||
|
||||
export default {
|
||||
name: 'ReconciliationJar',
|
||||
props: {
|
||||
msg: String,
|
||||
},
|
||||
components: {
|
||||
SummaryPurchaseModal,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
valid: null,
|
||||
selectedPurchaseId: null,
|
||||
selectedTab: 'CASH',
|
||||
reconciliation: {
|
||||
date_time: '',
|
||||
total_cash_purchases: 0,
|
||||
cash_taken: 0,
|
||||
cash_discrepancy: 0,
|
||||
other_totals: {
|
||||
},
|
||||
cash_purchases: [],
|
||||
},
|
||||
summary: {
|
||||
headers: [
|
||||
{title: 'Id', value: 'id'},
|
||||
{title: 'Fecha', value: 'date'},
|
||||
{title: 'Cliente', value: 'customer.name'},
|
||||
{title: 'Total', value: 'total'},
|
||||
],
|
||||
purchases: {},
|
||||
},
|
||||
rules: {
|
||||
required: value => !!value || 'Requerido.',
|
||||
integer: value => !!value || value === 0 || 'Requerido.',
|
||||
},
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.fetchPurchases();
|
||||
this.reconciliation.date_time = this.getCurrentDate();
|
||||
},
|
||||
watch: {
|
||||
'reconciliation.cash_taken'() {
|
||||
this.updateDiscrepancy();
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
totalByMethod(method) {
|
||||
if (method in this.summary.purchases) {
|
||||
return this.summary.purchases[method].reduce((a, b) => a + parseFloat(b.total), 0);
|
||||
}
|
||||
return 0;
|
||||
},
|
||||
idsBymethod(method) {
|
||||
if (method in this.summary.purchases) {
|
||||
return this.summary.purchases[method].map(purchase => purchase.id)
|
||||
}
|
||||
return [];
|
||||
},
|
||||
processOtherMethods() {
|
||||
for (const method of Object.keys(this.summary.purchases)) {
|
||||
if (method !== 'CASH') {
|
||||
this.reconciliation.other_totals[method] = {
|
||||
total: this.totalByMethod(method),
|
||||
purchases: this.idsBymethod(method),
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
updateDiscrepancy() {
|
||||
this.reconciliation.cash_discrepancy = (this.reconciliation.total_cash_purchases || 0 ) - (this.reconciliation.cash_taken || 0);
|
||||
},
|
||||
getCurrentDate() {
|
||||
const today = new Date();
|
||||
const gmtOffSet = -5;
|
||||
const localDate = new Date(today.getTime() + (gmtOffSet * 60 * 60 * 1000));
|
||||
// Formatear la fecha y hora en el formato YYYY-MM-DDTHH:MM
|
||||
const formattedDate = localDate.toISOString().slice(0,16);
|
||||
return formattedDate;
|
||||
},
|
||||
openSummaryModal(id) {
|
||||
this.selectedPurchaseId = id;
|
||||
this.$refs.summaryModal.dialog = true;
|
||||
},
|
||||
fetchPurchases() {
|
||||
const endpoint = '/don_confiao/purchases/for_reconciliation';
|
||||
fetch(endpoint)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
this.summary.purchases = data;
|
||||
this.reconciliation.cash_purchases = this.idsBymethod('CASH');
|
||||
this.reconciliation.total_cash_purchases = this.totalByMethod('CASH');
|
||||
this.processOtherMethods();
|
||||
})
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
});
|
||||
},
|
||||
async submit() {
|
||||
this.$refs.taker.validate();
|
||||
if (this.valid) {
|
||||
try {
|
||||
const response = await fetch('/don_confiao/reconciliate_jar', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(this.reconciliation),
|
||||
});
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
console.log('Cuadre enviado:', data);
|
||||
this.$router.push({path: "/"});
|
||||
} else {
|
||||
console.error('Error al enviar el cuadre', response.statusText);
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error de red:', error);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
@ -0,0 +1,30 @@
|
||||
<template>
|
||||
<v-dialog v-model="dialog" max-width="400">
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<SummaryPurchase :id="id"/>
|
||||
</v-card-text>
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn text @click="dialog = false">Cerrar</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'SummaryPurchase Modal',
|
||||
props: {
|
||||
id: {
|
||||
type: Number,
|
||||
required: true,
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
dialog: false,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
@ -0,0 +1,7 @@
|
||||
<template>
|
||||
<ReconciliationJar />
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
//
|
||||
</script>
|
@ -0,0 +1,29 @@
|
||||
# Generated by Django 5.0.6 on 2024-11-18 03:16
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('don_confiao', '0033_sale_payment_method'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='sale',
|
||||
name='reconciliation',
|
||||
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.RESTRICT, related_name='Sales', to='don_confiao.reconciliationjar'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='payment',
|
||||
name='type_payment',
|
||||
field=models.CharField(choices=[('CASH', 'Efectivo'), ('CONFIAR', 'Confiar'), ('BANCOLOMBIA', 'Bancolombia')], default='CASH', max_length=30),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='sale',
|
||||
name='payment_method',
|
||||
field=models.CharField(choices=[('CASH', 'Efectivo'), ('CONFIAR', 'Confiar'), ('BANCOLOMBIA', 'Bancolombia')], default='CASH', max_length=30),
|
||||
),
|
||||
]
|
@ -0,0 +1,19 @@
|
||||
# Generated by Django 5.0.6 on 2024-12-03 02:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('don_confiao', '0034_sale_reconciliation_alter_payment_type_payment_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='reconciliationjar',
|
||||
name='total_cash_purchases',
|
||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=9),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
@ -0,0 +1,14 @@
|
||||
# Generated by Django 5.0.6 on 2024-12-28 22:12
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('don_confiao', '0034_alter_payment_type_payment_alter_sale_date_and_more'),
|
||||
('don_confiao', '0035_reconciliationjar_total_cash_purchases'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
@ -62,6 +62,31 @@ class Product(models.Model):
|
||||
return products_list
|
||||
|
||||
|
||||
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)
|
||||
total_cash_purchases = models.DecimalField(max_digits=9, decimal_places=2)
|
||||
|
||||
def clean(self):
|
||||
self._validate_taken_ammount()
|
||||
|
||||
def add_payments(self, payments):
|
||||
for payment in payments:
|
||||
self.payment_set.add(payment)
|
||||
self.is_valid = True
|
||||
|
||||
def _validate_taken_ammount(self):
|
||||
ammount_cash = self.cash_taken + self.cash_discrepancy
|
||||
if not self.total_cash_purchases == ammount_cash:
|
||||
raise ValidationError(
|
||||
{"cash_taken": _("The taken ammount has discrepancy.")}
|
||||
)
|
||||
|
||||
|
||||
class Sale(models.Model):
|
||||
customer = models.ForeignKey(Customer, on_delete=models.PROTECT)
|
||||
date = models.DateTimeField("Date")
|
||||
@ -74,6 +99,12 @@ class Sale(models.Model):
|
||||
blank=False,
|
||||
null=False
|
||||
)
|
||||
reconciliation = models.ForeignKey(
|
||||
ReconciliationJar,
|
||||
on_delete=models.RESTRICT,
|
||||
related_name='Sales',
|
||||
null=True
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.date} {self.customer}"
|
||||
@ -122,38 +153,6 @@ class ReconciliationJarSummary():
|
||||
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(
|
||||
|
@ -1,6 +1,6 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from .models import Sale, SaleLine, Product, Customer
|
||||
from .models import Sale, SaleLine, Product, Customer, ReconciliationJar
|
||||
|
||||
|
||||
class SaleLineSerializer(serializers.ModelSerializer):
|
||||
@ -20,7 +20,21 @@ class ProductSerializer(serializers.ModelSerializer):
|
||||
model = Product
|
||||
fields = ['id', 'name', 'price', 'measuring_unit', 'categories']
|
||||
|
||||
|
||||
class CustomerSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Customer
|
||||
fields = ['id', 'name', 'address', 'email', 'phone']
|
||||
|
||||
|
||||
class ReconciliationJarSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ReconciliationJar
|
||||
fields = [
|
||||
'id',
|
||||
'date_time',
|
||||
'reconcilier',
|
||||
'cash_taken',
|
||||
'cash_discrepancy',
|
||||
'total_cash_purchases',
|
||||
]
|
||||
|
@ -10,7 +10,6 @@
|
||||
<li><a href='/don_confiao/lista_productos'>Productos</a></li>
|
||||
<li><a href='/don_confiao/importar_productos'>Importar Productos</a></li>
|
||||
<li><a href='/don_confiao/importar_terceros'>Importar Terceros</a></li>
|
||||
<li><a href='/don_confiao/cuadrar_tarro'>Cuadrar tarro</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
<p id="page_title" class="text-center decoration-solid font-mono font-bold text-lg page_title">Don Confiao - Tienda la Ilusión</p>
|
||||
|
@ -1,34 +0,0 @@
|
||||
{% extends 'don_confiao/base.html' %}
|
||||
{% block content %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% endblock %}
|
@ -1,88 +0,0 @@
|
||||
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]
|
208
tienda_ilusion/don_confiao/tests/test_jar_reconciliation.py
Normal file
208
tienda_ilusion/don_confiao/tests/test_jar_reconciliation.py
Normal file
@ -0,0 +1,208 @@
|
||||
from django.test import TestCase, Client
|
||||
from django.core.exceptions import ValidationError
|
||||
from ..models import Sale, Product, SaleLine, Customer, ReconciliationJar
|
||||
|
||||
import json
|
||||
|
||||
|
||||
class TestJarReconcliation(TestCase):
|
||||
def setUp(self):
|
||||
customer = Customer()
|
||||
customer.name = 'Alejo Mono'
|
||||
customer.save()
|
||||
|
||||
self.client = Client()
|
||||
|
||||
purchase = Sale()
|
||||
purchase.customer = customer
|
||||
purchase.date = "2024-07-30"
|
||||
purchase.payment_method = 'CASH'
|
||||
purchase.clean()
|
||||
purchase.save()
|
||||
|
||||
product = Product()
|
||||
product.name = "cafe"
|
||||
product.price = "72500"
|
||||
product.save()
|
||||
|
||||
line = SaleLine()
|
||||
line.sale = purchase
|
||||
line.product = product
|
||||
line.quantity = "11"
|
||||
line.unit_price = "72500"
|
||||
line.save()
|
||||
self.purchase = purchase
|
||||
|
||||
purchase2 = Sale()
|
||||
purchase2.customer = customer
|
||||
purchase2.date = "2024-07-30"
|
||||
purchase.payment_method = 'CASH'
|
||||
purchase2.clean()
|
||||
purchase2.save()
|
||||
|
||||
line2 = SaleLine()
|
||||
line2.sale = purchase2
|
||||
line2.product = product
|
||||
line2.quantity = "27"
|
||||
line2.unit_price = "72500"
|
||||
line2.save()
|
||||
self.purchase2 = purchase2
|
||||
|
||||
purchase3 = Sale()
|
||||
purchase3.customer = customer
|
||||
purchase3.date = "2024-07-30"
|
||||
purchase3.payment_method = 'CASH'
|
||||
purchase3.clean()
|
||||
purchase3.save()
|
||||
|
||||
line3 = SaleLine()
|
||||
line3.sale = purchase3
|
||||
line3.product = product
|
||||
line3.quantity = "37"
|
||||
line3.unit_price = "72500"
|
||||
line3.save()
|
||||
self.purchase3 = purchase3
|
||||
|
||||
purchase4 = Sale()
|
||||
purchase4.customer = customer
|
||||
purchase4.date = "2024-07-30"
|
||||
purchase4.payment_method = 'CONFIAR'
|
||||
purchase4.clean()
|
||||
purchase4.save()
|
||||
|
||||
line4 = SaleLine()
|
||||
line4.sale = purchase4
|
||||
line4.product = product
|
||||
line4.quantity = "47"
|
||||
line4.unit_price = "72500"
|
||||
line4.save()
|
||||
self.purchase4 = purchase4
|
||||
|
||||
def test_create_reconciliation_jar(self):
|
||||
reconciliation = self._create_simple_reconciliation()
|
||||
self.assertTrue(isinstance(reconciliation, ReconciliationJar))
|
||||
|
||||
def test_get_purchases_for_reconciliation(self):
|
||||
# link purchase to reconciliation to exclude from list
|
||||
reconciliation = self._create_simple_reconciliation()
|
||||
self.purchase3.reconciliation = reconciliation
|
||||
self.purchase3.clean()
|
||||
self.purchase3.save()
|
||||
|
||||
url = '/don_confiao/purchases/for_reconciliation'
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
rawContent = response.content.decode('utf-8')
|
||||
content = json.loads(rawContent)
|
||||
|
||||
self.assertIn('CASH', content.keys())
|
||||
self.assertIn('CONFIAR', content.keys())
|
||||
self.assertEqual(2, len(content.get('CASH')))
|
||||
self.assertEqual(1, len(content.get('CONFIAR')))
|
||||
self.assertNotIn(str(37*72500), rawContent)
|
||||
self.assertIn(str(47*72500), rawContent)
|
||||
|
||||
def test_don_create_reconcialiation_with_bad_numbers(self):
|
||||
reconciliation = ReconciliationJar()
|
||||
reconciliation.date_time = "2024-07-30"
|
||||
reconciliation.total_cash_purchases = 145000
|
||||
reconciliation.cash_taken = 143000
|
||||
reconciliation.cash_discrepancy = 1000
|
||||
with self.assertRaises(ValidationError):
|
||||
reconciliation.clean()
|
||||
reconciliation.save()
|
||||
|
||||
def test_fail_create_reconciliation_with_wrong_total_purchases_purchases(self):
|
||||
url = '/don_confiao/reconciliate_jar'
|
||||
total_purchases = (11 * 72500) + (27 * 72500)
|
||||
bad_total_purchases = total_purchases + 2
|
||||
data = {
|
||||
'date_time': '2024-12-02T21:07',
|
||||
'reconcilier': 'carlos',
|
||||
'total_cash_purchases': bad_total_purchases,
|
||||
'cash_taken': total_purchases,
|
||||
'cash_discrepancy': 0,
|
||||
'cash_purchases': [
|
||||
self.purchase.id,
|
||||
self.purchase2.id,
|
||||
self.purchase.id,
|
||||
],
|
||||
}
|
||||
response = self.client.post(url, data=json.dumps(data).encode('utf-8'),
|
||||
content_type='application/json')
|
||||
rawContent = response.content.decode('utf-8')
|
||||
content = json.loads(rawContent)
|
||||
|
||||
self.assertEqual(response.status_code, 400)
|
||||
self.assertIn('error', content)
|
||||
self.assertIn('total_cash_purchases', content['error'])
|
||||
|
||||
def test_create_reconciliation_with_purchases(self):
|
||||
url = '/don_confiao/reconciliate_jar'
|
||||
total_purchases = (11 * 72500) + (27 * 72500)
|
||||
data = {
|
||||
'date_time': '2024-12-02T21:07',
|
||||
'reconcilier': 'carlos',
|
||||
'total_cash_purchases': total_purchases,
|
||||
'cash_taken': total_purchases,
|
||||
'cash_discrepancy': 0,
|
||||
'cash_purchases': [
|
||||
self.purchase.id,
|
||||
self.purchase2.id,
|
||||
self.purchase.id,
|
||||
],
|
||||
}
|
||||
response = self.client.post(url, data=json.dumps(data).encode('utf-8'),
|
||||
content_type='application/json')
|
||||
|
||||
rawContent = response.content.decode('utf-8')
|
||||
content = json.loads(rawContent)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('id', content)
|
||||
|
||||
purchases = Sale.objects.filter(reconciliation_id=content['id'])
|
||||
self.assertEqual(len(purchases), 2)
|
||||
|
||||
def test_create_reconciliation_with_purchases_and_other_totals(self):
|
||||
url = '/don_confiao/reconciliate_jar'
|
||||
total_purchases = (11 * 72500) + (27 * 72500)
|
||||
data = {
|
||||
'date_time': '2024-12-02T21:07',
|
||||
'reconcilier': 'carlos',
|
||||
'total_cash_purchases': total_purchases,
|
||||
'cash_taken': total_purchases,
|
||||
'cash_discrepancy': 0,
|
||||
'cash_purchases': [
|
||||
self.purchase.id,
|
||||
self.purchase2.id,
|
||||
],
|
||||
'other_totals': {
|
||||
'Confiar': {
|
||||
'total': (47 * 72500) + 1,
|
||||
'purchases': [self.purchase4.id],
|
||||
},
|
||||
},
|
||||
}
|
||||
response = self.client.post(url, data=json.dumps(data).encode('utf-8'),
|
||||
content_type='application/json')
|
||||
|
||||
rawContent = response.content.decode('utf-8')
|
||||
content = json.loads(rawContent)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn('id', content)
|
||||
|
||||
purchases = Sale.objects.filter(reconciliation_id=content['id'])
|
||||
self.assertEqual(len(purchases), 3)
|
||||
|
||||
def _create_simple_reconciliation(self):
|
||||
reconciliation = ReconciliationJar()
|
||||
reconciliation.date_time = "2024-07-30"
|
||||
reconciliation.total_cash_purchases = 0
|
||||
reconciliation.cash_taken = 0
|
||||
reconciliation.cash_discrepancy = 0
|
||||
reconciliation.clean()
|
||||
reconciliation.save()
|
||||
return reconciliation
|
@ -1,45 +0,0 @@
|
||||
from django.test import Client, TestCase
|
||||
from django.contrib.auth.models import AnonymousUser, User
|
||||
|
||||
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()
|
@ -23,10 +23,10 @@ urlpatterns = [
|
||||
path("exportar_ventas_para_tryton",
|
||||
views.exportar_ventas_para_tryton,
|
||||
name="exportar_ventas_para_tryton"),
|
||||
path("cuadrar_tarro", views.reconciliate_jar, name="reconciliate_jar"),
|
||||
path("cuadres", views.reconciliate_jar, name="reconciliations"),
|
||||
path("resumen_compra/<int:id>", views.purchase_summary, name="purchase_summary"),
|
||||
path("resumen_compra_json/<int:id>", views.purchase_json_summary, name="purchase_json_summary"),
|
||||
path("payment_methods/all/select_format", views.payment_methods_to_select, name="payment_methods_to_select"),
|
||||
path('purchases/for_reconciliation', views.sales_for_reconciliation, name='sales_for_reconciliation'),
|
||||
path('reconciliate_jar', api_views.ReconciliateJarView.as_view()),
|
||||
path('api/', include(router.urls)),
|
||||
]
|
||||
|
@ -4,13 +4,12 @@ from django.views.generic import ListView
|
||||
from django.db import transaction
|
||||
|
||||
from .models import (
|
||||
Sale, SaleLine, Product, Customer, ProductCategory, Payment, PaymentMethods)
|
||||
Sale, SaleLine, Product, Customer, ProductCategory, Payment, PaymentMethods, ReconciliationJar)
|
||||
from .forms import (
|
||||
ImportProductsForm,
|
||||
ImportCustomersForm,
|
||||
PurchaseForm,
|
||||
SaleLineFormSet,
|
||||
ReconciliationJarForm,
|
||||
PurchaseSummaryForm)
|
||||
|
||||
import csv
|
||||
@ -95,6 +94,7 @@ def import_products(request):
|
||||
{'form': form}
|
||||
)
|
||||
|
||||
|
||||
def import_customers(request):
|
||||
if request.method == "POST":
|
||||
form = ImportCustomersForm(request.POST, request.FILES)
|
||||
@ -109,24 +109,6 @@ def import_customers(request):
|
||||
{'form': form}
|
||||
)
|
||||
|
||||
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>')
|
||||
@ -178,6 +160,24 @@ def payment_methods_to_select(request):
|
||||
return JsonResponse(methods, safe=False)
|
||||
|
||||
|
||||
def sales_for_reconciliation(request):
|
||||
sales = Sale.objects.filter(reconciliation=None)
|
||||
grouped_sales = {}
|
||||
for sale in sales:
|
||||
if sale.payment_method not in grouped_sales.keys():
|
||||
grouped_sales[sale.payment_method] = []
|
||||
grouped_sales[sale.payment_method].append({
|
||||
'id': sale.id,
|
||||
'date': sale.date,
|
||||
'payment_method': sale.payment_method,
|
||||
'customer': {
|
||||
'id': sale.customer.id,
|
||||
'name': sale.customer.name,
|
||||
},
|
||||
'total': sale.get_total(),
|
||||
})
|
||||
return JsonResponse(grouped_sales, safe=False)
|
||||
|
||||
def _mask_phone(phone):
|
||||
digits = str(phone)[-3:] if phone else " " * 3
|
||||
return "X" * 7 + digits
|
||||
|
Loading…
Reference in New Issue
Block a user